问题描述
我试图在Haskell中正确地理解懒惰。
我理解这一点,以便如果我们有一些表达式而实际上不使用表达式的子部分,那么该子部分将永远不会被求值,例如
SendData
永远不会实际评估列表,而只会返回0。
但是,如果我有类似以下内容的情况,其中let x = [1..1000] in 0
是斐波那契函数,并且将为fib(n)
返回错误
n<0
除以零误差let x = div 100 0 + (20 * 100)
n
let x = fib(-3) + fib(7)
和(20 * 100)
会被求值,还是会等待第一个表达式被计算然后在我返回错误后停止?
解决方法
根据一些注释,该语言对于像这样的程序中的子表达式的求值顺序并不能做很多保证:
main = print $ div 100 0 + 20 * 100
因此,可以首先对div 100 0
进行评估,然后在评估20 * 100
之前抛出错误,反之亦然。或者,可以将整个表达式优化为无条件地除以零错误而不进行任何评估,这是如果使用ghc -O2
进行编译时实际发生的情况。
实际上,至少在GHC 8.6.5中,该功能:
foo :: Int -> Int -> Int -> Int
foo x y z = div x y + z * x
用ghc -O2
编译的代码会先尝试除法,如果y == 0
会在尝试乘法之前抛出错误,因此将按子表达式的出现顺序对其求值。
但是功能的顺序相反:
bar :: Int -> Int -> Int -> Int
bar x y z = z * x + div x y
与ghc -O2
ALSO 编译的产生的代码会先尝试除法,如果y == 0
会在尝试乘法之前抛出错误,因此子表达式将以相反的顺序求值。
此外,即使两个版本都在乘法之前尝试除法,它们的评估顺序仍然不同-bar
在尝试除法之前会完全评估z
,而foo
在评估之前在完全评估z
之前进行除法运算,因此,如果为z
传递了一个延迟生成错误的值,则这两个函数将产生不同的行为。特别是
main = print $ foo 1 0 (error "not so fast")
在以下情况下将除以零错误:
main = print $ bar 1 0 (error "not so fast")
说“不是那么快”。不过,两者都没有尝试乘法。
这里没有任何简单的规则。看到这些差异的唯一方法是使用转储中间编译器输出的标志进行编译,例如:
ghc -ddump-stg -dsuppress-all -dsuppress-uniques -fforce-recomp -O2 Test.hs
并检查生成的代码。
如果要保证特定的评估顺序,则需要编写如下内容:
import Control.Parallel (pseq)
foo' :: Int -> Int -> Int -> Int
foo' x y z = let a = div x y
b = z * x
in a `pseq` b `pseq` a + b
bar' :: Int -> Int -> Int -> Int
bar' x y z = let a = z * x
b = div x y
in a `pseq` b `pseq` a + b
函数pseq
与注释中讨论的seq
函数相似。 seq
函数在这里可以使用,但并不总是保证评估顺序。 pseq
函数应该提供有保证的顺序。
如果您的实际目标是了解Haskell的惰性计算,而不是防止在其他子表达式出现错误的情况下对特定的子表达式进行评估,那么我不确定看看这些示例会有所帮助。相反,查看this answer到注释中已链接的相关问题可以使您更好地理解懒惰在概念上是如何工作的。
,在这种情况下,表达式(20 * 100)
和fib(7)
将求值,但这是因为运算符(+)
首先求值其第二个参数。例如,如果您编写(20 * 100) + div 100 0
,则部分(20 * 100)
不会评估。您可以自行检测哪个参数首先求值:例如(error "first") + (error "second")
。