IO单子有什么特殊之处(如果有的话)?

6

深入研究单子后,我明白它们是一个通用概念,允许在一些上下文中链接计算(失败、非确定性、状态等),没有任何魔法在其中。

尽管IO单子并不像魔法那样神奇,但仍然感觉很特别。

  • 你不能像其他单子一样逃脱IO单子
  • 只有main函数才能运行IO操作
  • IO始终位于单子变换器链的底部
  • IO单子的实现不清楚,源代码显示了一些Haskell内部细节

这些点的原因是什么?是什么使IO如此特殊?

更新:在纯代码评估中,顺序不重要。但在进行IO时,它确实很重要(我们想在读取之前保存客户端)。据我所知,IO单子给了我们这种顺序保证。这是单子的一般属性还是IO单子特有的属性呢?

2个回答

8
你不能像处理其他单子那样逃避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 是一个巨大的麻烦制造者(最好在任何时候都避免使用)。

当我说逃逸或运行时,我的意思是运行*函数,它们允许我们执行状态或读取器操作。我认为没有安全的运行IO函数。了解原因会很好。 - Konstantin Milyutin
这些 run* 函数并不真正“执行”任何操作。它们只是展开内部表示。在 IO 的情况下,它并没有被正式导出(但是,正如我两次所说,有一些替代实现的 IO 可以归结为一个简单的状态单子)。 - leftaroundabout
1
@damluar 引用透明性。使用纯函数,编译器可以自由地将 (\x -> x * x) y 替换为 y * y。但是,如果 y 是一个不纯的函数,这样的转换会改变语义,因为 y 的副作用会发生两次而不是一次。将函数放置在 IO 单子中(通过使其返回一个 IO 操作)可以防止您编写不安全的代码。您必须编写 y >>= (\x -> return (x * x)),并且特定单子的 >>= 定义可以防止不安全的扩展。 - chepner
1
@damluar IO单子在Haskell代码中没有实现>>=,而是在解释器/编译器本身中实现的,因为这是唯一允许直接处理外部世界的部分。(像getChar这样的Haskell代码仅通过返回一个读取字符的承诺来保持纯净,而不是实际返回可以用于“感染”其他纯净代码的字符。) - chepner
1
@chepner,这并不是真的,“>>=”对于IO来说是一个常规函数(请参见bindIO)。运行时确实知道State# RealWorld 是一个虚拟参数,并且永远不会为它分配内存,在编译期间也不会重新排序此类参数的情况。IO本身并不是原语 - State#才是原语。 - user2407038
显示剩余4条评论

2
类似的说法也可以适用于ST单子,或者可以说是STM单子(虽然你实际上可以在IO上面实现它)。
基本上像Reader单子、Error单子、Writer单子等都只是纯代码。ST和IO单子是唯一真正做不纯洁事情(状态突变等)的单子,所以它们不能被定义为纯Haskell。它们必须在编译器中“硬编码”。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接