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".
的工作总是有限的,就可以执行无限程序并做有用的工作
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".