【问题标题】:where clauses in list comprehensions列表推导中的 where 子句
【发布时间】:2015-10-10 08:21:01
【问题描述】:

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

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]] =&gt; [[1,4],[1,5],[2,4],[2,5],[3,4],[3,5]]

根据 Thinking Functional With Haskell (p. 92),第二个版本是“一个更有效的定义......[它]保证 cp xss 只计算一次”,尽管作者从不解释原因。我会认为它们是等价的。

【问题讨论】:

标签: haskell list-comprehension


【解决方案1】:

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

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

在您的情况下,在第一个定义中,x 位于cp xss 出现位置的范围内,因此将为xs 的每个元素x 重新评估cp xss。在第二个定义中,cp xss 出现在 x 的范围之外,所以它只会被计算一次。

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

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

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

【讨论】:

    【解决方案2】:

    好吧,一个天真的脱糖是:

    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 -&gt; concatMap (\ ys -&gt; [ x:ys ]) (cp xss) 时都会重新评估它。通过将其浮动出来,我们避免了重新计算。

    同时,GHC 确实有一个优化过程,可以将昂贵的计算浮出这样的循环,因此它可能会自动将第一个版本转换为第二个版本。您的书说第二个版本“保证”只计算一次 cp xss 的值,因为如果表达式的计算成本很高,编译器通常会非常犹豫内联它(将第二个版本转换回第一个版本)。

    【讨论】:

      猜你喜欢
      • 2021-12-05
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2014-07-02
      • 2019-09-03
      相关资源
      最近更新 更多