简体   繁体   English

Haskell 中的 I/O 功能正常吗?

[英]I/O in Haskell is Functional?

I'm just starting to take a look at Haskell (my previous FP experience is in Scheme), and I came across this code :刚开始看 Haskell(我以前的 FP 经验是在 Scheme 中),我遇到了这段代码

do { putStrLn "ABCDE" ; putStrLn "12345" }

To me, this is procedural programming, if anything -- especially because of the consecutive nature of side effects.对我来说,这就是过程编程,如果有的话——尤其是因为副作用的连续性。

Would someone please explain how this code is "functional" in any respect?有人可以解释这段代码在任何方面是如何“起作用”的吗?

While it appears to be a procedural program, the above syntax is translated into a functional program, like so:虽然它看起来是一个过程程序,但上面的语法被翻译成一个函数式程序,如下所示:

   do { putStrLn "ABCDE" ; putStrLn "12345" }
=>
   IO (\ s -> case (putStrLn "ABCDE" s) of
                  ( new_s, _ ) -> case (putStrLn "12345" new_s) of
                                      ( new_new_s, _) -> ((), new_new_s))

That is, a series of nested functions that have a unique world parameter threaded through them, sequencing calls to primitive functions "procedurally".也就是说,一系列具有唯一世界参数的嵌套函数贯穿其中,“按程序”对原始函数的调用进行排序。 This design supports an encoding of imperative programming into a functional language.此设计支持将命令式编程编码为函数式语言。

The best introduction to the semantic decisions underlying this design is "The Awkward Squad" paper,对这种设计背后的语义决策的最好介绍是“The Awkward Squad”论文,

在此处输入图像描述

I don't think we can answer this question clearly, because "functional" is a fuzzy notion, and there are contradictory ideas out there of what it means.我不认为我们可以清楚地回答这个问题,因为“功能”是一个模糊的概念,关于它的含义存在相互矛盾的想法。 So I prefer Peter Landin's suggested replacement term "denotative", which is precise and substantive and, for me, the heart & soul of functional programming and what makes it good for equational reasoning.所以我更喜欢 Peter Landin 建议的替代术语“指称”,它是精确和实质性的,对我来说,它是函数式编程的核心和灵魂,以及它对等式推理有好处的原因。 See these comments for some pointers to Landin's definition.有关 Landin 定义的一些指示,请参阅这些评论 IO is not denotative. IO不是指示性的。

Think about it this way.这样想。 It doesn't actually "execute" the IO instructions.它实际上并不“执行” IO 指令。 The IO monad is a pure value that encapsulates the "imperative computation" to be done (but it doesn't actually carry it out). IO monad 是一个纯值,它封装了要完成的“命令式计算”(但实际上并不执行)。 You can put monads (computations) together into a bigger "computation" in a pure way using the monad operators and constructs like "do".您可以使用 monad 运算符和“do”等结构以纯粹的方式将 monad(计算)组合成一个更大的“计算”。 Still, nothing is "executed" per se.尽管如此,本身并没有“执行”任何内容。 In fact, in a way the whole purpose of a Haskell program is to put together a big "computation" that is its main value (which has type IO a ).事实上,在某种程度上, Haskell 程序的全部目的是将一个大的“计算”放在一起,这是它的main价值(其类型为IO a )。 And when you run the program, it is this "computation" that is run.当你运行程序时,运行的就是这个“计算”。

This is amonad .这是一个单子 Read about the do-notation for an explanation of what goes on behind the covers.阅读do-notation以了解幕后发生的事情。

Would someone please explain how this code有人可以解释一下这段代码吗

do { putStrLn "ABCDE"; putStrLn "12345" }

is "functional" in any respect?在任何方面都是“功能性的”吗?

This is how I see the current situation with I/O in Haskell;这就是我对 Haskell 中 I/O 的现状的看法; the usual disclaimers apply >_<通常的免责声明适用 >_<

Right now (2020 Jun), how "functional" I/O is depends on your Haskell implementation .现在(2020 年 6 月),I/O 的“功能”如何取决于您的 Haskell实现 But that wasn't always the case - in fact, the Haskell language 's original model of I/O really was functional!但情况并非总是如此 - 事实上,Haskell语言的原始 I/O model确实是功能性的!

Time for a trip back to the early days of Haskell, helped along by Philip Wadler's How to Declare an Imperative :是时候回到 Haskell 的早期了,Philip Wadler 的How to Declare an Imperative帮助了我们:

import Prelude hiding (IO)
import qualified Prelude (IO)

import Control.Concurrent.Chan(newChan, getChanContents, writeChan) 
import Control.Monad((<=<))


 -- pared-back emulation of retro-Haskell I/O
 --
runDialogue :: Dialogue -> Prelude.IO ()
runDialogue d =
  do ch <- newChan
     l <- getChanContents ch
     mapM_ (writeChan ch <=< respond) (d l)

respond :: Request -> Prelude.IO Response
respond Getq     = fmap Getp getChar
respond (Putq c) = putChar c >> return Putp

main = runDialogue (retro_main :: Dialogue)

{-
          implementation side
  -----------------------------------
  ========== retro-Haskell ==========
  -----------------------------------
             language side
-}

 -- pared-back definitions for retro-Haskell I/O
 -- from page 14 of Wadler's paper
 --
data Request = Getq | Putq Char
data Response = Getp Char | Putp

type Dialogue = [Response] -> [Request]

(Extending it to all of retro-Haskell I/O is left as an exercise for very keen readers;-) (将它扩展到所有复古 Haskell I/O 留给非常敏锐的读者作为练习;-)

There you go: plain " ol' school " functional I/O! go:普通的“老”功能 I/O! The responses are streamed to main retro_main , which then streams the requests back:响应流式传输到 main retro_main ,然后将请求流式传输回来:

与周围环境交互的复古 Haskell 程序

With all that classic elegance, you could happily define:凭借所有经典优雅,您可以愉快地定义:

 -- from page 15 of Wadler's paper
echoD :: Dialogue
echoD p =
  Getq :
    case p of
      Getp c : p' ->
        if (c == '\n') then
          []
        else
          Putq c :
            case p' of
              Putp : p'' -> echoD p''

You look confused - that's alright;你看起来很困惑——没关系; you'll get the hang of it:-D你会掌握它的窍门:-D

Here's a more-sophisticated example from page 24 of A History of Haskell :是 Haskell 的历史第 24 页中的一个更复杂的示例:

{-

main ~(Success : ~((Str userInput) : ~(Success : ~(r4 : _))))
  = [ AppendChan stdout "enter filename\n",
      ReadChan stdin,
      AppendChan stdout name,
      ReadFile name,
      AppendChan stdout
          (case r4 of
              Str contents -> contents
              Failure ioerr -> "can't open file")
    ] where (name : _) = lines userInput

-}

Are you still there?你还在吗?

Is that a garbage bin next to you?你旁边那个是垃圾桶吗? Huh?嗯? You were ill?你生病了吗? Darn.该死的。

Alright then - perhaps you'll find it a bit easier with a more-recognisable interface :好吧 - 也许你会发现使用更易识别的界面会更容易一些:

 -- from page 12 of Wadler's paper
 --
echo  :: IO ()
echo  =  getc >>= \ c ->
         if (c == '\n') then
           done
         else
           putc c >>
           echo


 -- from pages 3 and 7
 --
puts  :: String -> IO ()
puts []    = done
puts (c:s) = putc c >> puts s

done :: IO ()
done = return ()


 -- based on pages 16-17
 --
newtype IO a = MkIO { enact :: Reality -> (Reality, a) }
type Reality = ([Response], [Request])

bindIO    :: IO a -> (a -> IO b) -> IO b
bindIO m k =  MkIO $ \ (p0, q2) -> let ((p1, q0), x) = enact m     (p0, q1)
                                       ((p2, q1), y) = enact (k x) (p1, q2)
                                   in
                                       ((p2, q0), y)


unitIO :: a -> IO a
unitIO x = MkIO $ \ w -> (w, x)

putc :: Char -> IO ()
putc c  = MkIO $ \ (p0, q1) -> let q0        = Putq c : q1
                                   Putp : p1 = p0
                               in
                                   ((p1, q0), ())

getc :: IO Char
getc    = MkIO $ \ (p0, q1) -> let q0          = Getq : q1
                                   Getp c : p1 = p0
                               in
                                   ((p1, q0), c)

mainD :: IO a -> Dialogue
mainD main = \ p0 -> let ((p1, q0), x) = enact main (p0, q1)

                         q1            = []
                     in
                         q0

 -- making it work
instance Monad IO where
    return = unitIO
    (>>=)  = bindIO

I've also included your sample code;我还包含了您的示例代码; maybe that'll help:也许这会有所帮助:

 -- local version of putStrLn
putsl :: String -> IO ()
putsl s = puts s >> putc '\n'

 -- bringing it all together
retro_main :: Dialogue
retro_main = mainD $ do { putsl "ABCDE" ; putsl "12345" }

Yes: this is all still simple functional I/O;是的:这仍然是简单的函数式 I/O; check the type of retro_main .检查retro_main的类型。

Apparently, dialogue-based I/O ended up being about as popular as a skunk in a space station.显然,基于对话的 I/O 最终像空间站中的臭鼬一样流行。 Stuffing it inside a monadic interface only confined the stench (and its source) to one small section of the station - by then, Haskellers wanted that lil' stinker gone!将它塞进一个单一的界面中只会将恶臭(及其来源)限制在空间站的一小部分 - 到那时,Haskellers 希望那个小臭味消失了!

So the abstract monadic interface for I/O in Haskell was made the standard - that small section and its pungent occupant was detached from the space station and hauled back to Earth, where fresh air is more plentiful.因此,Haskell 中用于 I/O 的抽象单子接口成为标准 - 那个小部分及其刺鼻的乘客从空间站分离并被运回地球,那里的新鲜空气更充足。 The atmosphere on the space station improved, and most Haskellers went on to do other things.空间站的气氛有所改善,大多数哈斯克勒人都开始做其他事情了。

But a few had some questions about this new, abstract model of I/O:但是有些人对这个新的、抽象的 I/O model 有一些疑问:


Regarding Haskell being functional - if the model is based on an abstraction, in this case:关于 Haskell 的功能 - 如果 model 基于抽象,在这种情况下:

  • an abstract type of I/O actions: IO I/O 操作的抽象类型: IO
  • an abstract function for constructing simple I/O actions: return用于构建简单 I/O 操作的抽象 function: return
  • the abstract functions for combining I/O actions: (>>=) , catch , etc用于组合 I/O 操作的抽象函数: (>>=)catch
  • the abstract functions for specific I/O actions: getArgs , getEnv , etc特定 I/O 操作的抽象函数: getArgsgetEnv

then how these entities are actually defined will be specific to each implementation of Haskell. What should now be asked is this:那么这些实体的实际定义方式将特定于 Haskell 的每个实现。现在应该问的是:

So the answer to your question:所以你的问题的答案是:

Would someone please explain how this code有人可以解释一下这段代码吗

do { putStrLn "ABCDE"; putStrLn "12345" }

is "functional" in any respect?在任何方面都是“功能性的”吗?

now depends on which implementation of Haskell you're using.现在取决于您使用的 Haskell 的实现。


As for Haskell being denotative - moving effects from the language into the implementation (and under the control of algorithms) has worked in the past:至于 Haskell 是指示性的——将效果从语言转移到实现中(并在算法的控制下)在过去是有效的:

[...] Underneath the implementation of our current functional abstractions (numbers, strings, trees, functions, etc), there are imperative mechanisms, such as memory allocation & deallocation, stack frame modification, and thunk overwriting (to implement laziness). [...] 在我们当前的功能抽象(数字、字符串、树、函数等)的实现之下,存在命令式机制,例如 memory 分配和释放、堆栈帧修改和 thunk 覆盖(以实现惰性)。 [...] [...]

Stack and register munging and jump/ GOTO are implementations of the semantically simpler notion of function application.堆栈和寄存器修改和跳转/ GOTO是 function 应用程序语义上更简单概念的实现。 [...] [...]

Conal Elliott. 康纳艾略特。

...so also relocating the effects of I/O in that way seems entirely reasonable. ...因此也以这种方式重新定位 I/O 的影响似乎是完全合理的。

But there's a crucial difference: unlike those other mechanisms which use the computer's memory, the simplest of I/O is device-based and the vast majority of I/O devices do not behave like the memory of a computer eg turning off your computer after printing an SVG file doesn't erase the image from the paper.但有一个关键的区别:与使用计算机 memory 的其他机制不同,最简单的 I/O 是基于设备的,绝大多数 I/O 设备的行为不像计算机的 memory,例如在之后关闭计算机打印SVG 文件不会从纸上擦除图像。

Haskell was intended to be a stable foundation for real applications development - presumably that includes applications which use I/O, and need it to work reliably. Haskell 旨在成为实际应用程序开发的稳定基础——大概包括使用 I/O 并需要它可靠工作的应用程序。 Whether a future version Haskell could be made completely denotative remains a subject of study ...未来的版本 Haskell 是否可以完全具有外延性仍然是一个研究课题......

It isn't functional code.它不是功能代码。 Why would it be?为什么会这样?

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM