了解懒惰评估

时间:2012-02-21 19:37:13

标签: haskell lazy-evaluation

我正试图了解Haskell中懒惰评估方面的问题。

如果我有一个函数调用,如:

negate $ 5 * sqrt 16

我的理解是Haskell将首先处理sqrt 16,创建一个thunk,允许在需要时计算值。

sqrt 16是在传递给乘法时还是仅在以某种方式输出到控制台时进行评估?

换句话说,当输入GHCi时,表达式的每个部分将以什么顺序进行评估?(例如)?

5 个答案:

答案 0 :(得分:8)

默认情况下,每个函数和构造函数调用都会成为一个thunk。因此,在这种情况下,评估会发生如下:

evaluate "negate $ 5 * sqrt 16" -> <thunk> $ <thunk>
 evaluate "negate"               -> <func>
 evaluate "5 * sqrt 16"          -> <thunk> * <thunk>
  evaluate "5"                    -> 5.0
  evaluate "sqrt 16"              -> 4.0

<thunk>表示thunk,<func>表示它是一个无法表示为字符串的函数值。

缩进意味着“父级”将在评估“子级”之前对其进行评估。

因此,如果您编写print (negate $ 5 * sqrt 16),运行时将执行以下步骤:

eval thunk:
  <thunk 1> $ <thunk 2>
eval thunk 1:
  <func> $ <thunk 2>
eval thunk 2:
  <func> $ <thunk 3> * <thunk 4>
(cheating a little here, because (*) is strict, so these three are actually
 one step:)
eval thunk 3:
  <func> $ 5 * <thunk 4>
eval thunk 4 by applying sqrt:
  <func> $ 5 * 4
apply (*):
  <func> $ 20
apply ($):
  -20

答案 1 :(得分:6)

您可以将其视为外向内,即调用第一个negate。然后它将强制评估其论点,这可能会迫使评估其他表达式等等。您可以使用Debug.Trace.trace来打印其第一个参数,同时在评估时返回第二个参数,以查看GHCi中发生的事件的确切顺序:

> trace "A" (negate (trace "B" (5 * (trace "C" (sqrt 16)))))
A
B
C
-20.0

但请注意,允许编译器执行可能更改表达式求值顺序的优化,这就是我们在订单重要时使用IO monad的原因。

答案 2 :(得分:4)

当需要该值时,将计算表达式。因此,如果你写:

f = negate $ 5 * sqrt 16

直到您使用f进行评估。 negate需要5 * sqrt 16,而sqrt 16则需要{{1}}。评估继续展开,直到它到达将被评估的基本情况,然后将用于前一个/更高的表达式(现在向后),直到评估整个表达式。

答案 3 :(得分:4)

首先,创建整个表达式的thunk。 *是严格的,因此sqrt 16的thunk可能会也可能不会在其中创建,具体取决于优化(可能会使用对sqrt的直接调用)。然后当它被强制(需要它的值)时,negate将强制参数,即*表达式,并且严格,*将强制它的两个参数并产生值20

顺便说一下,当你谈到Haskell时,你应该谈论的是非严格的语义。 “Thunk”和“lazy”属于实现细节。

答案 4 :(得分:2)

我在这方面读到的真正帮助我的方法就是这样看待它:

  • 每个表达式都会产生 thunk
  • 当他们的价值“需要”时,将会强制
  • “需要”的概念就是你可以称之为“条件强迫”:“假设thunk T 正在被迫,这会导致其他thunks被迫?”如果强制应用该函数的thunk导致其参数的thunk被强制,则函数在其参数中是 strict

因此,通过在控件上调用适当的show方法将值打印到控制台;即,打印到控制台会强制使用show x形式的表达式(对于某些x)。迫使show x部队x。假设xnegate $ 5 * sqrt 16;由于negate在其参数中是严格的,因此强制thunk也会强制5 * sqrt 16的thunk。同样,*sqrt在其参数中都是严格的,因此也必须强制5sqrt 1616的thunk。

要理解的另一件事是数据构造函数和模式匹配如何影响thunking。基本上,除非有特殊的严格性,否则构造函数就像非严格函数一样,强制thunk不会强制构造函数的参数。除非使用特殊的惰性模式匹配语法,否则对构造函数的匹配会强制参数thunk。所以我们有:

identity x = x  -- irrefutable pattern; `x` is not forced

uncons (x:xs) = (x, xs)  -- match against (:) constructor; argument 
                         -- must be forced, but x and xs aren't forced

foo (x:x':xs) = (x, x', xs) -- two matches against (:) constructor;
                            -- the argument thunk is forced, as is its
                            -- tail thunk.