以下两个公式有什么区别?
cp [] = [[]]
cp (xs:xss) = [x:ys | x <- xs, ys <- cp xss]
----------------------------------------------
cp [] = [[]]
cp (xs:xss) = [x:ys | x <- xs, ys <- yss]
where yss = cp xss
示例输出:cp [[1,2,3],[4,5]] => [[1,4],[1,5],[2,4],[2,5],[3,4],[3,5]]
根据在功能上与Haskell一起思考(p.92),第二个版本是&#34;一个更有效的定义... [这]保证cp xss只计算一次,& #34;虽然作者从未解释过为什么。我原以为他们是等同的。
答案 0 :(得分:11)
这两个定义在某种意义上是等价的,当然它们表示相同的值。
在操作上,他们在按需调用评估下的共享行为方面存在差异。 jcast已经解释了为什么,但是我想添加一个不需要明确地去除列表理解的快捷方式。规则是:每当变量x
绑定到某个值时,语法上处于可依赖于变量x
的位置的任何表达式都将被重新计算,即使表达式实际上不依赖于在x
。
在您的情况下,在第一个定义中,x
位于cp xss
出现的位置范围内,因此将为每个元素cp xss
重新评估x
xs
。在第二个定义中cp xss
出现在x
范围之外,因此它只会被计算一次。
然后通常的免责声明适用,即:
编译器不需要遵循按需调用评估的操作语义,只需遵循指称语义。因此,根据上述规则,它可能会比您预期的更少次数(浮动)或更多次(浮动)。
一般来说,更多分享更好是不正确的。在这种情况下,例如,它可能不会更好,因为cp xss
的大小增长速度与首先计算它的工作量一样快。在这种情况下,从内存中读取值的成本可能超过重新计算值的成本(由于缓存层次结构和GC)。
答案 1 :(得分:7)
嗯,天真的脱糖会是:
cp [] = [[]]
cp (xs:xss) = concatMap (\x -> concatMap (\ ys -> [ x:ys ]) (cp xss)) xs
----------------------------------------------
cp [] = [[]]
cp (xs:xss) = let yss = cp xss in concatMap (\x -> concatMap (\ ys -> [ x:ys ]) yss) xs
如您所见,在第一个版本中,调用cp xss
位于lambda中。除非优化器移动它,否则每次调用函数\x -> concatMap (\ ys -> [ x:ys ]) (cp xss)
时都会重新评估它。通过将其浮出,我们避免了重新计算。
同时,GHC确实有一个优化传递来从这样的循环中浮动昂贵的计算,因此它可以自动将第一个版本转换为第二个版本。你的书上写的第二个版本&#39;保证&#39;只计算一次cp xss
的值,因为如果表达式的计算成本很高,编译器通常会非常犹豫地将其内联(将第二个版本转换回第一个版本)。