评估和执行IO操作的区别:何时会导致Haskell执行IO操作?

6
Haskell使用什么机制来决定调用以下4个操作?
main :: IO ()
main = getLine >>= putStrLn >> getLine >>= putStrLn

最初我认为这与惰性求值有关,但是...从《Real World Haskell》开始,涉及IO操作时,它们在执行时会产生效果,但在评估时不会产生效果。

执行IO操作时会产生效果,但在评估时不会产生效果

因此,我怀疑这是一种与系统想要“评估”main不同的机制。这是什么机制?或者如果这是评估,Haskell“希望”评估什么导致它执行行动链?


相关的,但可能不是重复的问题:https://dev59.com/2Ww15IYBdhLWcg3wYasx - jkeuhlen
1
相关链接:新手入门:理解main和IO() - duplode
可能相关:https://dev59.com/PJ7ha4cB1Zd3GeqPkH_r. - atravers
3个回答

12
作为第一次近似,Haskell程序中唯一的评估来源是main。这意味着:
  • IO操作可以通过>>=>><*>fmap等组合和组合形成任何其他IO操作,但
  • 只有main IO操作将产生效果。
从某种意义上说,Haskell程序所做的就是运行main :: IO ()。要对任何内容进行评估,它必须挡在运行IO操作的路上(这就是懒惰性的作用)。这引出了一个问题:实际运行IO操作是什么意思?
在底层,IO最终的行为类似于一个(严格的)State单子,其中穿过它一个RealWorld状态(不包含任何信息——它象征着副作用对世界所包含的状态),因此“运行”IO(有点等同于State RealWorld)就像调用runState。自然地,该runState仅能对任何程序运行一次——这正是main所做的事情(也是使其神奇的原因)!

9
似乎很奇怪,但运行IO操作实际上超出了普通的Haskell语言范畴!1 Haskell内置库提供了"基本"IO操作,如getLine :: IO String,返回IO操作的函数,如putStrLn :: String -> IO (),以及通过其他IO操作构建IO操作的方法(主要是通过提供Monad接口来完成的,因此任何适用于任何monad的代码(如Control.Monad中的所有内容)都是处理IO的方式)。所有这些都是纯粹和惰性的,就像非IO的Haskell代码一样。对于IO而言,“执行IO操作不会产生效果”是在说IO操作并不实际执行;它只是从其他IO操作构建新的IO动作。"apple" ++ "banana"这样类型为String的值可以由未计算的thunk表示;当它被计算为"applebanana"时,它仍然代表完全相同的值,系统只是将其记录为存储在内存中的数据,而不是指向某个可以运行它的代码的指针1. 同样,putStrLn "apple" >> putStrLn "banana"这样类型为IO ()的值也可以由未计算的thunk表示,当它被计算时,这只意味着系统现在用数据结构代替代码指针来运行(纯、惰性)函数>>。但是我们只谈到了IO操作在内存中的表示,还没有讨论实际运行它们以产生某些副作用的问题。事实上,Haskell没有任何语言特性涉及如何执行IO操作。运行时系统“只知道”如何从Main模块执行main IO操作3。Haskell语言无法说明如何或是否执行;这都由提供Haskell的系统(GHC或另一个Haskell系统)处理。Haskell语言唯一提供的选项就是将main定义为一个Haskell动作;将IO操作作为main定义的一部分进行合并后,这些IO操作将得到运行。

1 我们在这里讨论时将假装像unsafePerformIO这样的东西不存在。正如其名称所示,它是故意违反正常规则的。它也不旨在将“执行IO操作”作为Haskell语言的正常部分引入,只用于在呈现“正常Haskell”接口的某些内容的内部使用。

2 通常情况下,这种情况仅部分发生:只有基本类型(如Int)是“全部或无”的评估。大多数可以部分地评估为包含更深层次的惰性求值结构的数据结构(可能会稍后被评估)。

3 或者GHCi“知道”如何执行您在其提示符下输入的IO操作。


0
根据https://wiki.haskell.org/IO_inside#Welcome_to_the_RealWorld.2C_baby,有一个代表真实世界的“假”类型RealWorld,而IO(a)实际上是一个函数。
type IO a  =  RealWorld -> (a, RealWorld)

所以,正如您在其他语言中预期的那样,main实际上是一个函数

main :: RealWorld -> ((), RealWorld)

当程序运行时,该函数被调用。因此,为了评估最终输出,即类型为((), RealWorld)的值,Haskell需要获取RealWorld组件的值,为了做到这一点,它必须运行main函数。注意:是运行时引起该函数运行。在Haskell中没有办法触发执行该函数。

在这种情况下

main = getLine >>= putStrLn >> getLine >>= putStrLn

每个操作实际上都是函数,为了计算最终putStrLn输出的RealWorld值,需要运行它以及所有导致它的操作。

因此,它是惰性求值,但是针对隐藏的RealWorld值。


1
"RealWorld"是假的。我不知道Haskell会这么哲学化。 - luqui

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