Haskell中`seq`操作符的时间成本

12

这个常见问题解答说:

seq 操作符是

seq :: a -> b -> b

x seq y将评估x,仅检查它是否不是底部,然后丢弃结果并评估y。这可能看起来没有用,但它意味着在考虑y之前保证了评估x。

x `seq` f x

评估x的成本会被支付两次(“丢弃结果”)吗?


也许 "丢弃结果" 太过强烈。它以与 const 丢弃其第二个参数相同的方式丢弃结果。如果参数已被评估,它不会以某种方式重新评估它或丢弃结果,而是忽略它。“x seq y 将评估 x,足以检查它不是底部,然后 忽略 结果并评估 y”可能是更好的表述方式。 - Tyler
我开始意识到Haskell的计算模型与我的核心编程语言(C ++)有多么不同。 - quant_dev
5个回答

19

seq函数会丢弃x的值,但是由于该值已经被求值,所有对x的引用都被“更新”,不再指向未求值的版本,而是指向已求值的版本。因此,即使seq评估和丢弃了xx的其他用户也已经求值,从而不会出现重复的求值。


12
不,它不是计算并忘记,而是计算-强制缓存。
例如,考虑以下代码:
 let x = 1 + 1
 in x + 1

由于 Haskell 是惰性的,所以这个表达式被求值为 ((1 + 1) + 1)。一个 thunk 包含了一个 thunk 和一个数字的和,内部的 thunk 是 1 加上 1。
让我们使用非惰性语言 JavaScript 来展示这是什么样子:
 function(){
   var x = function(){ return 1 + 1 };
   return x() + 1;
 }

像这样链接thunk可能会导致堆栈溢出,如果重复执行, 因此seq来解救。

let x = 1 + 1
in x `seq` (x + 1)

我撒谎了,当我告诉你这个计算结果为(2+1)时,但这几乎是真的——只是2的计算被强制在其他计算之前发生(但是2仍然是惰性计算)。
回到Javascript:
 function(){
   var x = function(){ return 1 + 1 };
   return (function(x){
     return x + 1;
   })( x() );
 }

4
我相信x只会被评估一次(并将结果保留以供将来使用,这是懒操作的典型行为)。这种行为使seq变得有用。

1

你可以随时使用 unsafePerformIOtrace 进行检查...

import System.IO.Unsafe (unsafePerformIO)

main = print (x `seq` f (x + x))
  where
    f = (+4)
    x = unsafePerformIO $ print "Batman!" >> return 3

1

当然 seq本身并不会“评估”任何内容。它只记录强制顺序依赖关系。强制本身是通过模式匹配触发的。当强制seq x(f x)时,将首先强制x(记忆结果值),然后将强制f x。Haskell的惰性评估意味着它会记忆表达式强制的结果,因此不会执行重复的“评估”(在这里加上可怕的引号)。

我加了引号的“评估”,因为它意味着完全的评估。用Haskell wikibook的话来说,

"Haskell值高度分层;'评估' Haskell值可以意味着评估到其中任何一层。"

让我再强调一遍:seq本身不会评估任何东西。在任何情况下,seq x x都不会评估x。当f=id时,seq x (f x)不会评估任何东西,这与报告所说的相反。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接