列表推导中的子句

时间:2015-07-21 02:51:54

标签: haskell list-comprehension

以下两个公式有什么区别?

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;虽然作者从未解释过为什么。我原以为他们是等同的。

2 个答案:

答案 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的值,因为如果表达式的计算成本很高,编译器通常会非常犹豫地将其内联(将第二个版本转换回第一个版本)。