Why/how递归IO有用吗?

Why/how does recursive IO work?

Haskell IO 通常被解释为整个程序是一个纯函数 (main),returns 一个 IO 值(通常被描述为命令式 IO 程序),然后由运行时执行。

这种心智模型对于简单的例子来说效果很好,但是当我在 Learn You A Haskell 中看到递归 main 时,我就崩溃了。例如:

main = do
  line <- getLine
  putStrLn line
  main 

或者,如果您愿意:

main = getLine >>= putStrLn >> main

因为 main 永远不会终止,它实际上从来没有 returns 一个 IO 值,但是程序无休止地读取和回显返回行就好了 - 所以上面的简单解释并不完全有效。我错过了一些简单的东西还是有更完整的解释(或者是 'simply' 编译器魔法)?

在这种情况下,main 是类型 IO ()value 而不是函数。您可以将其视为 IO a 个值的序列:

main = getLine >>= putStrLn >> main

这使它成为一个递归值,与无限列表不同:

foo = 1 : 2 : foo

我们可以 return 这样的值,而无需评估整个事物。事实上,这是一个相当普遍的成语。

foo 如果您尝试使用整个东西,将永远循环。但是 main 也是如此:除非你使用某种外部方法来打破它,否则它永远不会停止循环!但是您可以开始从 foo 中获取元素,或者执行 main 的部分内容,而无需评估所有内容。

main表示是一个无限程序:

main = do
  line <- getLine
  putStrLn line
  line <- getLine
  putStrLn line
  line <- getLine
  putStrLn line
  line <- getLine
  putStrLn line
  line <- getLine
  putStrLn line
  line <- getLine
  putStrLn line
  ...

但它在内存中表示为引用自身的递归结构。该表示是有限的,除非有人试图展开整个事情以获得整个程序的非递归表示 - 永远不会完成。

但是,正如您无需等待我告诉您 "all" 就可以弄清楚如何开始执行我上面写的无限程序一样,Haskell 的运行时系统图也可以了解如何在不预先展开递归的情况下执行 main

Haskell 的惰性计算实际上与运行时系统对 main IO 程序的执行交织在一起,因此即使对于 returns 和 IO 递归调用函数的操作,例如:

main = foo 1

foo :: Integer -> IO ()
foo x = do
  print x
  foo (x + 1)

这里foo 1不是一个递归的(它包含foo 2,而不是foo 1),但它仍然是一个无限程序。然而,这工作得很好,因为 foo 1 表示的程序只是按需延迟生成;它可以随着运行时系统对 main 的执行而产生。

默认情况下 Haskell 的惰性意味着在需要之前不会评估任何内容,然后只有 "just enough" 才能通过当前块。最终,"until it's needed" 中所有 "need" 的来源都来自运行时系统,它需要知道 main 程序的下一步是什么,以便它可以执行它。但这只是 下一步;在下一步完全执行之前,程序的其余部分可以保持未评估状态。因此,只要生成 "one more step".

的工作总是有限的,就可以执行无限程序并做有用的工作