haskell 中 IO 的无限循环如何工作

How infinite loop with IO work in haskell

在haskell中,我们写的所有IO代码都只是一个动作(很多人建议把它看成是正在生成的脚本)。最终执行它们的是main方法(执行构造好的脚本)。那么下面的程序是如何工作的呢? infi 函数永远不会 return。那么为什么字符串会无限打印?

infi = 
    do
     print "hello"
     infi

main = 
    do
     infi

因为infi设置为递归调用自身。 main 所期望的 'return' 将永远不会满足,因为对 infi 的第一次调用永远不会 return。

# The first call to infi will never return as the calls to infi
# will just continue to add more calls to the stack, until you exceed
# the size :)

main ->
  infi ->
    infi ->
      infi -> ..

您似乎对Haskell实际实现IO的方式有误解。关于这个主题还有很多其他文献以及本网站上处理它的其他几个答案,因此我将重点关注您的具体示例而不是一般示例。

infi = 
  do
   print "hello"
   infi

main = 
  do
    infi

首先,你可以简化你的主要动作(不需要do):

main = infi 

在 haskell 中,"return" 与命令式 "return" 不同。它只是意味着注入一个 monadic 动作,即 return :: Monad m => a -> m a。所以我们来谈谈这里评估什么东西,而不是什么时候return。

您的 main 函数所做的就是调用 infi :: IO () 类型的值 infi。由于 infi 是一个 IO 动作,它可以执行打印。与任何其他值一样,它也可以引用其他值(在这种情况下,它是递归的,因此它会调用自身)。如果没有基本情况,infi 将继续执行以下序列(就像它在 do 块中的布局一样!):

  1. 打印"hello"到标准输出
  2. 评估价值infi
    1. 打印"hello"到标准输出
    2. 评估值 infi
      ... 堆栈永远持续下去,因为递归中没有基本情况。

这可以工作的主要原因是 Haskell 的懒惰评估。 Haskell 除非您需要,否则不会真正计算值。这就是为什么你也可以用无限列表做纯粹的动作:

let x = [1..] -- x is an infinite list. If you told haskell to print every element, it would run forever since it would evaluate the whole thing. 
let y = x !! 3 -- y = 2. This is not infinite because you are only evaluating the first three elements, instead of the whole value.

你的无限值也是如此infi。 Haskell 可以创建一个包含无限动作的 "run-time-script" 因为 infi 的 value 有一个有限的表示(它的名字),但是它无限地求值,因为它没有基本情况。

与程序非常相似

ones = 1 : ones

上面是递归的,没错。它无限次地调用自己,是的。但它 确实 return。 return 是一个无限列表。相比之下,

noList = noList

将永远循环,而无需 returning 列表。 (实际上,GHC 运行时会检测到这一点并抛出异常,但这与讨论无关。)

同样,

printOnes = print 1 >> printOnes
-- or, equivalently
printOnes = do
   print 1
   printOnes

构建一个将永远打印 1 的 IO 操作,即使它会无限递归多次。相反,

noPrint = noPrint

将永远循环并且永远不会 return IO 操作。

infi 本质上是相同 IO 动作的无限流,与单子序列 (>>) 而不是缺点 (:):

fives = 5                :  fives
infi  = putStrLn "hello" >> infi

事实上,我们可以使用一系列操作来抽象出 monadic 绑定 infi':

infi' :: [IO ()]
infi' = putStrLn "hello" : infi'

然后用sequence_恢复infi,可以实现为foldr (>>) (return ()).

infi = sequence_ infi'
infi = (foldr (>>) (return ()) infi')
infi = putStrLn "hello" >> (foldr (>>) (return ()) infi')
infi = putStrLn "hello" >> (putStrLn "hello" >> (foldr (>>) (return ()) infi'))
infi = putStrLn "hello" >> (putStrLn "hello" >> (putStrLn "hello" >> ...))

像这样将操作存储在流中还可以让您将它们作为第一个 class 值进行操作:

> sequence_ (take 3 infi')
hello
hello
hello

当 Haskell 运行时执行您的 main 操作时,它计算 infi,找到 >> 表达式,计算其左侧参数以生成操作 putStrLn "hello", 执行 那个动作,然后继续进行右边的参数——恰好又是 infiIO.

Monad 实例中的内部模式匹配懒惰地驱动评估