列表理解中的 where 子句

where clauses in list comprehensions

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

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]]

根据 Thinking Functionally With Haskell(第 92 页),第二个版本是 "a more efficient definition...[which] guarantees that cp xss is computed just once,",尽管作者从未解释过原因。我还以为他们是等价的。

嗯,天真的脱糖是:

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 确实有一个优化通道,可以像这样将昂贵的计算从循环中浮出,因此它可能会自动将第一个版本转换为第二个版本。你的书说第二个版本 'guarantees' 只计算一次 cp xss 的值,因为如果表达式的计算成本很高,编译器通常会非常犹豫是否内联它(将第二个版本转换回第一个)。

这两个定义在表示相同值的意义上是等价的,当然。

在操作上,它们在按需评估下的共享行为不同。 jcast 已经解释了原因,但我想添加一个不需要显式取消列表理解糖分的快捷方式。规则是:任何在语法上可能依赖于变量 x 的表达式将在每次变量 x 绑定到一个值时重新计算,即使表达式实际上并不依赖于在 x.

在您的情况下,在第一个定义中,xcp xss 出现位置的范围内,因此 cp xss 将针对每个元素重新计算 xxs。在第二个定义中,cp xss 出现在 x 的范围之外,因此它只会被计算一次。

然后适用通常的免责声明,即:

  • 编译器不需要遵守按需求值的操作语义,只需要遵守指称语义。因此,根据上述规则,它可能会比您预期的计算次数更少(浮动)或更多次(浮动)。

  • 并不是说分享越多越好。例如,在这种情况下,它可能不会更好,因为 cp xss 的大小增长与最初计算它所花费的工作量一样快。在这种情况下,从内存中读回值的成本可能超过重新计算值的成本(由于缓存层次结构和 GC)。