延迟求值让我们可以在调用实际求值之前处理函数返回的值。我们可以将其传递给其他调用者,并且仅在稍后需要详细查看该值时才触发评估。
为此,我们创建一个 thunk。 thunk 只不过是内存中的一个小结构,其中包含用于存储参数的字段。
但是应用数据构造函数(例如(,)
) also只是在内存中生成一个小结构,用于存储它所应用的参数的字段。所以事实证明,为数据构造函数的应用程序创建 thunk 确实没有任何意义。
如果我们创建一个 thunk,我们只会将参数存储在 thunk 中而不查看它们,并且稍后当某些模式与值匹配时(触发它被评估),它们将被访问。如果我们跳过 thunk 并真正应用数据构造函数,我们将只存储参数而不查看它们,并且稍后当某些模式与值匹配时将访问它们。无论我们是否使用 thunk 并延迟应用程序,系统的行为基本相同。
因此,在试图帮助学习者建立 Haskell 如何工作的概念模型时,这样说是有道理的:any当变量绑定到应用程序的结果时,它将创建一个 thunk。作为一个概念模型来说这很好;它与该语言的所有通常可观察的行为一致,并且当所应用的东西是数据构造函数时,它比担心特殊异常更简单。我想这就是维基教科书文章所采取的观点。
But remember that thunks aren't actually the goal. There's no hard specification for exactly when GHC must use a thunk to delay evaluation of a value. They're just a means to an end. In cases like this when the observable behaviour will be the same either way, GHC is free to create a thunk or not. It's always safe to skip the thunk for a data constructor application1, but GHC can also do this when strictness analysis reveals that the value was going to be forced soon anyway. GHC simply doesn't always do exactly what textbooks say; it does whatever it thinks will produce the "best" code while still behaving the same as the simpler model you'll get from textbooks.
重要的是要记住这一点:sprint
不是检查 Haskell 良好理论特性的工具。它是一个调试工具,用于查看 GHC 使用的实现。它具体来说让你看到不应该从 Haskell 本身观察到的细节,这就是为什么它必须是一个特殊的 GHCi 命令而不是实际语言的功能。有时:sprint
只会揭示像这样的小细节,这些细节更多地与 GHC 的内部实现细节有关,而不是与 Haskell 作为一种语言有关。
1 Unless it has strict fields, but then you can think about it as syntactic sugar for a function that does examine its arguments before storing them. The "real" underlying constructor still doesn't need a thunk, but that is never actually applied in the user's code, only the wrapper function that does the extra evaluation.