你不能像处理其他单子那样逃避IO单子。
我不确定你所说的“逃避”是什么意思。如果你指的是,以某种方式展开单子值的内部表示(例如,列表->一系列cons-cell)-这是一种实现细节。事实上,你可以在纯Haskell中定义
一个IO
仿真 - 基本上只是对大量全局可用数据的状态。这将具有所有
IO
的语义,但不会与真实世界进行交互,而只是模拟。
如果你的意思是,你可以从单子内部“提取值” - 不行,在大多数纯Haskell单子中甚至也不可能。例如,你无法从
Maybe a
(可能是
Nothing
)或
Reader b a
(如果
b是未被占用的怎么办?)中提取值。
IO
操作只能由
main
函数运行。
嗯,在某种意义上,所有的内容都只能由main
函数运行。不从main
调用的代码将只是停留在那里,你可以用undefined
替换它而不改变任何东西。
IO始终位于单子变换器链的底部
没错,但对于例如ST
也是如此。
IO
单子的实现不清楚,并且源代码显示了一些Haskell内部
同样:实现只是一个实现细节。 IO
实现的复杂性实际上与其高度优化有很大关系;对于专门的纯单子(例如attoparsec),这也是真的。
正如我已经说过的,更简单的实现是可能的,但它们不会像全功能优化的真实世界IO
类型那样有用。
幸运的是,实现并不需要真正困扰你;IO
的内部可能不清楚,但实际的单子接口——外部却非常简单。
在纯代码中,评估顺序并不重要
首先——在纯代码中,评估顺序可能很重要!
Prelude> take 10 $ foldr (\h t -> h `seq` (h:t)) [] [0..]
[0,1,2,3,4,5,6,7,8,9]
Prelude> take 10 $ foldr (\h t -> t `seq` (h:t)) [] [0..]
^CInterrupted.
但是,实际上由于无序的纯代码求值而导致的错误结果是不可能出现的,这样你可以得到一个正确的,非“非”结果。然而,这并不适用于重排单子操作(
IO
或其他),因为改变顺序会改变结果动作的实际结构,而不仅仅是运行时将用于构造此结构的求值顺序。例如(列表单子):
Prelude> [1,2,3] >>= \e -> [10,20,30] >>= \z -> [e+z]
[11,21,31,12,22,32,13,23,33]
Prelude> [10,20,30] >>= \z -> [1,2,3] >>= \e -> [e+z]
[11,12,13,21,22,23,31,32,33]
所有这些说法,当然,
IO
是非常特殊的,实际上我认为有些人犹豫是否称其为单子(不太清楚
IO
实际上应该满足单子定律的意义)。特别是,
惰性 IO 是一个巨大的麻烦制造者(最好在任何时候都避免使用)。
run*
函数并不真正“执行”任何操作。它们只是展开内部表示。在IO
的情况下,它并没有被正式导出(但是,正如我两次所说,有一些替代实现的IO
可以归结为一个简单的状态单子)。 - leftaroundabout(\x -> x * x) y
替换为y * y
。但是,如果y
是一个不纯的函数,这样的转换会改变语义,因为y
的副作用会发生两次而不是一次。将函数放置在IO
单子中(通过使其返回一个IO
操作)可以防止您编写不安全的代码。您必须编写y >>= (\x -> return (x * x))
,并且特定单子的>>=
定义可以防止不安全的扩展。 - chepnerIO
单子在Haskell代码中没有实现>>=
,而是在解释器/编译器本身中实现的,因为这是唯一允许直接处理外部世界的部分。(像getChar
这样的Haskell代码仅通过返回一个读取字符的承诺来保持纯净,而不是实际返回可以用于“感染”其他纯净代码的字符。) - chepnerbindIO
)。运行时确实知道State# RealWorld
是一个虚拟参数,并且永远不会为它分配内存,在编译期间也不会重新排序此类参数的情况。IO
本身并不是原语 -State#
才是原语。 - user2407038