递归IO为什么/如何工作?

时间:2015-01-28 21:59:01

标签: haskell

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

这个心理模型适用于简单的例子,但是当我在Learn You A Haskell中看到一个递归的main时,它就会为我而烦恼。例如:

main = do
  line <- getLine
  putStrLn line
  main 

或者,如果您愿意:

main = getLine >>= putStrLn >> main

由于main永远不会终止,它实际上永远不会返回IO值,但是程序无休止地读取并回传线条很好 - 所以上面的简单解释并不是很有效。我是否遗漏了一些简单的内容,或者是否有更完整的解释(或者只是简单的编译器魔法)?

2 个答案:

答案 0 :(得分:14)

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

main = getLine >>= putStrLn >> main

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

foo = 1 : 2 : foo

我们可以返回这样的值而无需评估整个事物。事实上,它是一个相当常见的习语。

如果您尝试使用整个内容,

foo 永远循环。但main也是如此:除非你使用一些外部方法来突破它,它永远不会停止循环!但您可以开始从foo中获取元素,或执行main的部分内容,而无需对其进行全部评估。

答案 1 :(得分:7)

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
  ...

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

但就像你可以弄清楚如何开始执行我上面写的无限程序而不等我告诉你“全部”它一样,Haskell的运行时系统也可以弄清楚如何执行main预先展开递归。

Haskell的延迟评估实际上与运行时系统执行main IO程序交错,因此即使对于返回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的懒惰意味着在需要之前不会对任何内容进行评估,然后只是“足够”才能超过当前块。最终,“直到它需要”的所有“需要”的来源都来自运行时系统,需要知道main程序中的下一步是什么,以便它可以执行它。但它只是 next 步骤;在此之后,程序的其余部分可以保持不被评估,直到下一步完全执行完毕。所以infininte程序可以执行并做有用的工作,只要它总是只有一定量的工作来产生“再多一步”。