这里涉及到几个不同的问题。
首先,IO 和 State 是非常不同的东西。 State 很容易做到
你自己:只需向每个函数传递一个额外的参数,并返回一个额外的
结果,你有一个“有状态的功能”;例如,将a -> b 变成
a -> s -> (b,s).
这里没有魔法:Control.Monad.State 提供了一个包装器
也可以方便地使用 s -> (a,s) 形式的“状态操作”
作为一堆辅助函数,仅此而已。
I/O 就其本质而言,在其实现中必须具有一些魔力。但这有
很多在 Haskell 中表达 I/O 的方式都不涉及“monad”这个词。
如果我们有一个没有 IO 的 Haskell 子集,并且我们想从
从头开始,在不了解 monad 的情况下,我们可能会做很多事情
做。
例如,如果我们只想打印到标准输出,我们可能会说:
type PrintOnlyIO = String
main :: PrintOnlyIO
main = "Hello world!"
然后有一个 RTS(运行时系统)来评估字符串并打印它。
这让我们可以编写任何 I/O 完全由打印组成的 Haskell 程序
到标准输出。
然而,这并不是很有用,因为我们需要交互性!所以让我们发明
一种允许它的新型IO。想到的最简单的事情是
type InteractIO = String -> String
main :: InteractIO
main = map toUpper
这种 IO 方法让我们可以编写任何从标准输入读取并写入
stdout (Prelude 自带函数interact :: InteractIO -> IO ()
顺便说一句,它是这样做的)。
这要好得多,因为它可以让我们编写交互式程序。但它是
与我们想要做的所有 IO 相比,仍然非常有限,而且相当
容易出错(如果我们不小心尝试将标准输入读得太远,程序
只会阻塞,直到用户输入更多内容)。
我们希望能够做的不仅仅是读取标准输入和写入标准输出。这是如何做
早期版本的 Haskell 进行 I/O,大致如下:
data Request = PutStrLn String | GetLine | Exit | ...
data Response = Success | Str String | ...
type DialogueIO = [Response] -> [Request]
main :: DialogueIO
main resps1 =
PutStrLn "what's your name?"
: GetLine
: case resps1 of
Success : Str name : resps2 ->
PutStrLn ("hi " ++ name ++ "!")
: Exit
当我们写main 时,我们得到一个惰性列表参数并返回一个惰性列表作为
结果。我们返回的惰性列表具有PutStrLn s 和GetLine 之类的值;
在我们产生一个(请求)值之后,我们可以检查下一个元素
(响应)列表,RTS 将安排它作为对我们的响应
请求。
有一些方法可以更好地使用这种机制,但你可以
想象一下,这种方法很快就会变得很尴尬。还有就是
和上一个一样容易出错。
这是另一种不太容易出错的方法,并且在概念上非常
接近 Haskell IO 的实际行为方式:
data ContIO = Exit | PutStrLn String ContIO | GetLine (String -> ContIO) | ...
main :: ContIO
main =
PutStrLn "what's your name?" $
GetLine $ \name ->
PutStrLn ("hi " ++ name ++ "!") $
Exit
关键是不要将响应的“惰性列表”作为一个大
在 main 开头的参数,我们提出接受一个的个别请求
一次争论。
我们的程序现在只是一个常规数据类型——很像一个链表,除了
你不能只是正常遍历它:当 RTS 解释 main 时,有时
它遇到像GetLine 这样的值,其中包含一个函数;那么它必须得到
使用 RTS 魔法从标准输入获取字符串,并将该字符串传递给函数,
在它可以继续之前。练习:写interpret :: ContIO -> IO ()。
请注意,这些实现都不涉及“世界传递”。
“世界传递”并不是 Haskell 中 I/O 的真正工作方式。实际上
GHC 中IO 类型的实现涉及一个名为的内部类型
RealWorld,但这只是一个实现细节。
实际的 Haskell IO 添加了一个类型参数,因此我们可以编写如下操作
“产生”任意值——所以它看起来更像data IO a = Done a |
PutStr String (IO a) | GetLine (String -> IO a) | ...。这给了我们更多
灵活性,因为我们可以创建产生任意的“IO 动作”
价值观。
(作为罗素奥康纳points out,
这种类型只是一个免费的单子。我们可以很容易地为它编写一个Monad 实例。)
那么,monad 是从哪里来的呢?事实证明,我们不需要Monad
I/O,我们不需要Monad 来表示状态,那么为什么我们需要它呢?这
答案是我们没有。类型类 Monad 没有什么神奇之处。
但是,当我们使用 IO 和 State(以及列表和函数以及
Maybe 和解析器和继续传递风格和......)足够长的时间,我们
最终发现它们在某些方面的行为非常相似。我们可能
编写一个打印列表中每个字符串的函数,以及一个运行的函数
列表中的每个有状态计算并通过线程处理状态,它们将
看起来非常相似。
由于我们不喜欢编写很多看起来相似的代码,我们想要一种方法来
抽象它; Monad 原来是一个很棒的抽象,因为它让我们
抽象了许多看起来非常不同,但仍然提供了很多有用的类型
功能(包括Control.Monad 中的所有内容)。
鉴于bindIO :: IO a -> (a -> IO b) -> IO b 和returnIO :: a -> IO a,我们
可以在 Haskell 中编写任何 IO 程序,而无需考虑单子。但
我们可能最终会复制Control.Monad 中的许多功能,
比如mapM和forever和when和(>=>)。
通过实现通用的Monad API,我们可以使用完全相同的代码
像使用解析器和列表一样使用 IO 操作。这真的是唯一的
我们有Monad 类的原因——捕捉它们之间的相似之处
不同的类型。