给定:
λ: let f = putStrLn "foo" in 42
42
f
的类型是什么?为什么"foo"
在显示42
的结果之前没有打印出来?
最后,为什么以下不起作用?
λ: :t f
<interactive>:1:1: Not in scope: ‘f’
f
的类型是什么?
正如您正确识别的那样,IO ()
可以被认为是一个不返回任何有用信息的IO操作(()
)
为什么在显示
42
的结果之前不打印"foo"?
Haskell被延迟求值,但在这种情况下,即使是seq
也是不够的。只有当表达式返回IO操作时,才会在REPL中执行IO操作。IO操作只有在main
返回的情况下才会在程序中执行。然而,有一些方法可以绕过这一限制。
最后,为什么以下不起作用?
Haskell的let
命名表达式范围内的值,因此在对表达式求值后,f
将超出范围。
let f = ...
只是定义了f
,并不"运行"任何内容。它与命令式编程中一个新函数的定义有点相似。
您的完整代码let f = putStrLn "foo" in 42
可以松散地翻译为
{
function f() {
print("foo");
}
return 42;
}
你不会指望上面会打印出任何东西,对吧?
相比之下,let f = putStrLn "foo" in do f; f; return 42
与相似
{
function f() {
print("foo");
}
f();
f();
return 42;
}
信件并不完美,但希望你能明白。
f
的类型将为IO ()
。
"foo"未打印,因为f
未"绑定"到现实世界。(我不能说这是一个友好的解释。如果这听起来很无稽之谈,你可能想参考一些教程来了解Monad和懒惰评估的想法)。
let name = value in (scope)
使该值在范围内可用,但不在范围外,因此:t
不会在ghci的顶级范围内找到它。
不带in
的let
可用于:t
(此代码仅在ghci中有效):
> let f = putStrLn "foo"
> :t f
f :: IO ()
这里有两件事。
首先,考虑
let x = sum [1..1000000] in 42
Haskell是懒惰的。由于我们实际上没有对x
做任何事情,所以它永远不会被计算出来。(这也不错,因为它会稍微慢一点。)事实上,如果你编译它,编译器会发现x
从未被使用过,并将其删除(即,不为它生成任何编译代码)。
其次,调用putStrLn
实际上并没有打印任何内容。相反,它返回IO ()
,您可以将其视为一种"I/O命令对象"。仅仅拥有一个命令对象与执行it不同。根据设计,"执行"I/O命令对象的唯一方法是从main
返回它。至少,它是在一个完整的程序中;GHCi有一个有用的功能,如果您输入一个返回I/O命令对象的表达式,GHCi将为您执行它。
您的表达式返回42;同样,f
没有被使用,所以它没有任何作用。
正如chi
正确指出的那样,这有点像声明一个本地(零参数)函数,但从不调用它。你不会期望看到任何输出。
你也可以做一些类似的事情
actions = [print 5, print 6, print 7, print 8]
这将创建一个I/O命令对象列表。但是,它不会执行任何一个。
通常,当您编写一个执行I/O的函数时,它是一个do块,将所有内容链接到一个巨大的I/O命令对象中,并将其返回给调用者。在这种情况下,您不需要真正理解定义命令对象和执行命令对象之间的区别。但区别仍然存在。
使用具有显式运行函数的monad可能更容易看到这一点。例如,runST
获取一个ST命令对象,运行它,并返回答案。但是(比如)newSTVar
本身除了构造ST命令之外什么也不做;你必须runST
,在任何事情真正"发生"之前。