好吧,让我们来说明一下。首先,这是一个简单的定义doubleMe
:
doubleMe [] = []
doubleMe (x:xs) = x*2 : doubleMe xs
现在,让我们考虑一下严格/急切的语言(不是 Haskell)如何评估doubleMe (doubleMe (doubleMe [1, 2, 3]))
. 这种语言的规则是:评估函数调用,完整地评估所有参数,然后将它们传递给函数。所以我们得到这个:
doubleMe (doubleMe (doubleMe [1, 2, 3]))
-- Expand the list literal into its structure
== doubleMe (doubleMe (doubleMe (1 : 2 : 3 : [])))
-- Eager evaluation requires that we start at the innermost use of doubleMe
-- and work there until we produce the whole list.
== doubleMe (doubleMe (2 : doubleMe (2 : 3 : [])))
== doubleMe (doubleMe (2 : 4 : doubleMe (3 : [])))
== doubleMe (doubleMe (2 : 4 : 6 : doubleMe []))
== doubleMe (doubleMe (2 : 4 : 6 : []))
-- Only now we can move on to the middle use of doubleMe:
== doubleMe (4 : doubleMe (4 : 6 : []))
== doubleMe (4 : 8 : doubleMe (6 : []))
== doubleMe (4 : 8 : 12 : doubleMe [])
== doubleMe (4 : 8 : 12 : [])
== 8 : doubleMe (8 : 12 : [])
== 8 : 16 : doubleMe (12 : [])
== 8 : 16 : 24 : doubleMe []
== 8 : 16 : 24 : []
== [8, 16, 24]
在 Haskell 中,规则更像这样(但不完全是这样):
- 要评估函数应用程序,请在评估其参数之前应用该函数。
- 如果外部函数有多种情况(就像我们的
doubleMe
定义那样),那么我们只评估它的参数来确定使用哪种情况。
所以我们得到类似的东西:
doubleMe (doubleMe (doubleMe [1, 2, 3]))
-- Here we only "pull out" the 1 from the list, because it's all we need to
-- pick which case we want for doubleMe.
== doubleMe (doubleMe (doubleMe (1 : [2, 3])))
== doubleMe (doubleMe (1*2 : doubleMe [2, 3]))
-- Now instead of continuing with the inner doubleMe, we move on immediately
-- to the middle one:
== doubleMe ((1*2)*2 : doubleMe (doubleMe [2, 3]))
-- And now, since we know which case to use for the outer doubleMe, we expand
-- that one:
== (1*2)*2)*2 : doubleMe (doubleMe (doubleMe [2, 3]))
而在 Haskell 中,除非有另一个调用者需要列表头部或尾部的值,否则评估将停止。(请注意,我什至没有进行乘法运算。)例如,head
是返回列表第一个元素的函数:
head (x:xs) = x
假设我们正在评估head (doubleMe (doubleMe (doubleMe [1, 2, 3])))
. 事情是这样的:
head (doubleMe (doubleMe (doubleMe [1, 2, 3])))
-- repeat the steps from above for the doubleMe part
head (((1*2)*2)*2 : doubleMe (doubleMe (doubleMe [2, 3]))
-- By the definition of head:
((1*2)*2)*2
因此doubleMe (doubleMe (doubleMe [2, 3]))
在这种情况下该部分被丢弃,因为head
不需要它来产生结果。如果 Haskell 不是懒惰的,它会计算整个[8, 12, 24]
列表,然后将其8
放在前面。
GHC 比这更聪明。我们可以使用map
函数来写doubleMe
:
doubleMe = map (*2)
GHC,当您使用-O
优化编译程序的选项时,将以下规则纳入其中:
map f (map g xs) = map (f . g) xs
这意味着如果它看到 的嵌套使用map
,它可以将它们减少到遍历列表的一次。使用它:
head (doubleMe (doubleMe (doubleMe [1, 2, 3])))
== head (map (*2) (map (*2) (map (*2) [1, 2, 3])))
== head (map ((*2) . (*2) . (*2)) [1, 2, 3])
== head (map (\x -> ((x*2)*2)*2) [1, 2, 3])
== head (map (\x -> ((x*2)*2)*2) (1 : [2, 3]))
== head (((1*2)*2)*2 : map (\x -> ((x*2)*2)*2) [2, 3])
== ((1*2)*2)*2
编辑:在这个问题的答案中,一通与三通的主题显然有很多混淆。我会把我的很多东西都扔进去。
简单的惰性评估(如我的第二个示例评估所示)不会改变通过次数。如果我们评估类似的东西print (doubleMe (doubleMe (doubleMe [1, 2, 3])))
,惰性和急切的评估将做相同数量的工作,但顺序不同。让我们编写嵌套表达式的值并排列列表元素,如下所示:
doubleMe [1, 2, 3] = [2, 4, 6]
doubleMe (doubleMe [1, 2, 3]) = [4, 8, 12]
doubleMe (doubleMe (doubleMe [1, 2, 3])) = [8, 16, 24]
现在,如果我们执行以下操作print (doubleMe (doubleMe (doubleMe [1, 2, 3])))
:
- 急切的评估将逐行攻击这一点。首先它将计算整个 list
[2, 4, 6]
,然后是 list [4, 8, 12]
,然后是 list [8, 16, 24]
,最后它会打印最后一个 list 。
- 惰性求值逐列攻击。首先它将计算
2
第一个列表中的 ,然后4
是第二个列表中的 ,然后是8
第一个列表中的 ,并打印8
; 然后计算4
第一个列表中的8
,第二个列表中的 ,依此类推。
如果您正在打印列表,因为这需要计算所有元素,所以在任何一种情况下(或多或少)相同数量的工作和内存。然而,在这个head (doubleMe (doubleMe (doubleMe [1, 2, 3])))
例子中,惰性求值做的工作较少。
最后一个“融合”示例(使用 的示例map f . map g == map (f . g)
)比其他两个示例做的工作更少,因为它确实是一次通过。但这不是因为懒惰,而是因为纯度允许更积极的优化。