Haskell 懒到什么程度?

To what extent is Haskell lazy?

我需要澄清一下 Haskell 的懒惰。

如果我有这个功能:

myFunction arg
  | arg == 1 = a
  | arg == 2 = a*b
  | arg == 3 = b+c
  | otherwise = (a+b)*c
     where
            a = ...
            b = ...
            c = ...
            d = ...

当我调用 myFunction 1 时,Haskell 将只计算 a = ... 函数,b 也不是 c,也不是 d.

但是如果我写

myFunction arg
  | arg == 1 = a
  | arg == 2 = a*b
  | arg == 3 = b+c
  | otherwise = (a+b)*c
     where
            (a,b,c,d) = anotherFunction arg

Haskell 的行为会怎样?

嗯,它只对 a 感兴趣,所以这意味着有一个 隐式函数 :

thea :: (a,b,c,d) -> a
thea (a,_,_,_) = a

换句话说,Haskell 对元组的其他元素 不感兴趣。然而,有时元组的元素共享一些结构。假设另一个函数定义为:

anotherFunction :: Int -> (Int,Int,Int,Int)
anotherFunction x = (z,t,f,g)
    where f = x*x
          g = f+x
          z = g-2
          t = 3

在那种情况下 - 为了计算第一个元素 - 第三个和第四个元素也将被计算。但是由于您不对它们进行任何操作,Haskell 不会特别关注它们的结果。

在这两种情况下,除非需要值,否则它不会计算任何值。要求该值的一种方法是调用 ghci 中的函数(它打印 ghci 中的值并因此要求它)。假设您正在执行该函数,那么在您的第二种情况下,它会将元组计算为 weak head normal form (WHNF),然后计算 (a,b,c,d) 中的第一个元素,因为只需要该值。其他元素 bcd 将采用 thunk 形式。其实你自己可以验证一下:

myFunction arg
  | arg == 1 = a
  | arg == 2 = a*b
  | arg == 3 = b+c
  | otherwise = (a+b)*c
  where
    (a,b,c,d) = anotherFunction arg

anotherFunction x = (x, undefined, undefined, undefined)

ghci 中的演示:

λ> myFunction 1
1

除非需要获取该变量的值,否则不会对其求值。基本上Haskell就是这么懒,除非被告知不要这样。

你可以这样确认一下

Prelude> :set +m
Prelude> let anotherFunction = (100, 1 `div` 0)
Prelude| 
Prelude> let myFunction arg
Prelude|                | arg == 1  = a
Prelude|                | otherwise = b
Prelude|                where
Prelude|                     (a, b) = anotherFunction
Prelude| 

此处,1 `div` 0 将引发 divide by zero 错误。如果它计算所有元素,那么即使你用 1 调用 myFunction,你也会得到那个错误,但是

Prelude> myFunction 1
100

只有当你用任何其他值调用它时,才需要计算元组的第二个值,它会失败并返回 divide by zero 错误。

Prelude> myFunction 2
*** Exception: divide by zero

正如其他人已经指出的那样,只会评估 a

但是请记住,要利用惰性,在评估其组件之前 anotherFunction return 是一个元组是至关重要的。例如,考虑

anotherFunction n = if p > 1000 then (n, p) else (n, 0)
  where p = product [1..n]

即使调用者只需要第一对组件(即 n),上面的代码也会始终计算 product [1..n]。这是因为 if 需要在 returned 对之前进行评估,这会强制 p。相比之下,

anotherFunction n = (n, if p > 1000 then p else 0)
  where p = product [1..n]

将立即 return 这对。如果仅计算其第一个组件,则根本不会计算 p