【问题标题】:Need help in understanding the lazy evaluation behavior of Haskell需要帮助理解 Haskell 的惰性求值行为
【发布时间】:2012-07-01 13:17:50
【问题描述】:

我已经写了两个版本的 nqueens 问题,我认为它们应该有相似的效率,但事实并非如此。我认为这是由于 Haskell 的惰性评估行为。有人可以解释一下它在以下示例中的工作原理吗,

nqueens1 n 1 = [[i] | i <- [1..n]]
nqueens1 n k = [ i:q | i <- [1..n], q <- (nqueens1 n (k - 1)), isSafe i q k]

isSafe i q n  = isSafeHelper i (zip q [(n-1),(n-2)..1]) 
         where isSafeHelper i []  = True
               isSafeHelper i (x:xs) = (i /= fst x) && abs(i - (fst x)) /= abs(n - (snd x)) && 
                                       isSafeHelper i xs
nqueens2 n 1 = [[i] | i <- [1..n]]
nqueens2 n k = [ i:q | i <- [1..n], q <- boards, isSafe i q k]
           where boards = nqueens2  n (k-1)

您可以通过调用 nqueens1 8 8 或 nqueens2 8 8 来评估它们,以评估它是否适合 8 号板。

虽然 nqueens2 的工作效率很高,但 nqueens1 存在性能问题。我相信这是因为递归调用 (nqueens n (k-1)) 被多次评估。根据我对 Haskells 惰性求值的理解,情况并非如此。

请帮助我理解这种行为。

提前致谢。

【问题讨论】:

  • “懒惰的评估”是关于延迟评估——而不是避免多次评估。
  • @DanielWagner 实际上,惰性求值和按名称调用之间的区别恰恰在于,某些将使用按名称调用多次求值的表达式仅使用惰性求值求值一次。不过,这与这里的问题无关。
  • @sepp2k 你说得对,我应该更准确地说,或者说“按名称调用”而不是“惰性评估”,或者说“记忆化”而不是“避免评估某事”很多次”。
  • 请注意,带有 -O2 的 ghc 似乎看穿了差异。

标签: haskell lazy-evaluation


【解决方案1】:

是的,递归调用被多次评估。具体来说,它会为 i 的每个值评估一次。

如果您想避免这种情况,您可以重新排列生成器,使 q &lt;- 部分位于 i &lt;- 部分之前:

[ i:q | q <- nqueens2 n (k - 1), i <- [1..n], isSafe i q k]

但是,这会改变结果的顺序。如果这不可接受,您可以使用let 计算一次递归调用的结果,然后像这样使用它:

[ i:q | let rec = nqueens2 n (k - 1), i <- [1..n], q <- rec, isSafe i q k]

【讨论】:

  • 我猜想递归调用被多次评估,这就是 nqueens1 速度变慢的原因,但 nqueens2 中唯一的变化是为该表达式命名。这可以通过 Haskell 编译器本身轻松完成。我想知道为什么编译器不能这样做。谢谢。
  • 这种“优化”以空间换时间。虽然现在没有多次评估子术语,但只要可能再次需要它,它就必须保存在内存中。因此,GHC 非常小心,一般不会自动进行这种转换。一般的经验法则是:如果你想确保一个术语最多被评估一次,那么就给它一个名字。
猜你喜欢
  • 2022-01-15
  • 1970-01-01
  • 1970-01-01
  • 2016-04-28
  • 2011-02-20
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-12-26
相关资源
最近更新 更多