在学习Haskell时,我想知道什么时候会执行IO操作.在几个地方我找到了这样的描述:
"I/O操作的特殊之处在于,如果它们属于主要功能,则执行它们."
但是在下面的示例中,'greet'永远不会返回,因此不应打印任何内容.
import Control.Monad
main = greet
greet = forever $ putStrLn "Hello World!"
Run Code Online (Sandbox Code Playgroud)
或许我应该问:"落入主要功能"是什么意思?
小智 10
首先,main不是一个功能.它确实只是一个常规值,其类型是IO ().该类型可以读作:执行时,生成类型值的操作().
现在,运行时系统扮演解释器的角色,执行您所描述的操作.我们以您的程序为例:
main = forever (putStrLn "Hello world!")
Run Code Online (Sandbox Code Playgroud)
请注意,我已执行转换.那个是有效的,因为Haskell是一种引用透明的语言.运行时系统解析forever并找到:
main = putStrLn "Hello world!" >> MORE1
Run Code Online (Sandbox Code Playgroud)
它还不知道它是什么MORE1,但它现在知道它有一个具有一个已知动作的组合,它被执行.执行后,它解析第二个动作,MORE1并找到:
MORE1 = putStrLn "Hello world!" >> MORE2
Run Code Online (Sandbox Code Playgroud)
它再次执行该合成中的第一个动作,然后继续解析.
当然这是一个高级别的描述.实际代码不是解释器.但这是一种描述Haskell程序如何执行的方法.让我们再看一个例子:
main = forever (getLine >>= putStrLn)
Run Code Online (Sandbox Code Playgroud)
RTS认为:
main = forever MORE1
<< resolving forever >>
MORE1 = getLine >>= MORE2
<< executing getLine >>
MORE2 result = putStrLn result >> MORE1
<< executing putStrLn result (where 'result' is the line read)
and starting over >>
Run Code Online (Sandbox Code Playgroud)
理解这一点,你就会理解一个IO String不是"带有副作用的字符串",而是一个产生字符串的动作的描述.您也理解为什么懒惰对Haskell的I/O系统起作用至关重要.
在我看来,声明"I/O动作的特殊之处在于,如果它们属于主要功能,它们就会被执行." 是IO行动是一等公民.也就是说,IO-actions可以发生在其他数据类型的值可能发生的所有位置Int.例如,您可以定义包含IO如下操作的列表.
actionList = [putStr "Hello", putStr "World"]
Run Code Online (Sandbox Code Playgroud)
列表actionList有类型[IO ()].也就是说,列表包含与世界交互的动作,例如,在控制台上打印或从用户读取输入.但是,在定义此列表时,我们不执行操作,只需将它们放在列表中供以后使用.
如果IO某个程序可能出现在您的程序中某个地方,那么问题就会在这些行为执行时产生并main发挥作用.考虑以下定义main.
main = do
actionList !! 0
actionList !! 1
Run Code Online (Sandbox Code Playgroud)
此main函数投影到列表的第一个和第二个组件,并通过在其定义中使用它们来"执行"相应的操作.请注意,它不一定main是执行IO操作的函数本身.从main函数调用的任何函数也可以执行操作.例如,我们可以定义一个调用动作的函数,actionList并main调用此函数,如下所示.
main = do
caller
putStr "!"
caller = do
actionList !! 0
actionList !! 1
Run Code Online (Sandbox Code Playgroud)
要突出显示它不必是一个简单的重命名,就像main = caller我添加了一个动作,它在从列表中执行动作后打印一个感叹号.