【问题标题】:foldr resulting in stack overflowfoldr 导致堆栈溢出
【发布时间】:2015-05-21 16:29:31
【问题描述】:

我正在尝试在无限列表上使用埃拉托色尼筛算法生成素数。我听说 foldr 会懒惰地检查列表,但每次我尝试使用以下算法时都会出现堆栈溢出异常:

getPrimes :: [Int]
getPrimes = foldr getNextPrime [2] [3,5..]
    where
        getNextPrime n primes
            | not $ isAnyDivisibleBy primes n = primes ++ [n]
            | otherwise = primes
        isAnyDivisibleBy primes n = any (\x -> isDivisibleBy n x) primes
        isDivisibleBy x y = x `mod` y == 0

示例:

takeWhile (\x -> x < 10) getPrimes
*** Exception: stack overflow

列表正在某个地方被评估,但我不知道在哪里。

【问题讨论】:

  • 这个标题有两种读法;)

标签: haskell functional-programming


【解决方案1】:

我认为foldr 让你感到困惑,所以让我们用显式递归将其写出来:

getPrimes :: [Int]
getPrimes = getPrimesUsing [3,5..]

getPrimesUsing :: [Int]->[Int]
getPrimesUsing [] = [2]
getPrimesUsing (n:primes)
  | not $ isAnyDivisibleBy primes n = primes ++ [n]
  | otherwise = primes
  where
    primes = getPrimesUsing primes
    isAnyDivisibleBy primes n = any (\x -> isDivisibleBy n x) primes
    isDivisibleBy x y = x `mod` y == 0

你现在能看出问题了吗?

不相关的一点:您似乎在这里尝试实现的算法不是埃拉托色尼筛,而是一种效率低得多的算法,称为试除法。

【讨论】:

    【解决方案2】:

    foldr getNextPrime [2] [3, 5 .. ] 扩展为:

    (getNextPrime 3 (getNextPrime 5 (getNextPrime 7 ...
    

    由于getNextPrime 总是需要检查它的第二个参数,我们只得到一个getNextPrime 调用的非终止链,而最初的[2] 列表从未使用过。

    【讨论】:

      【解决方案3】:

      foldr 定义为

      foldr f z [] = z
      foldr f z (x:xs) = f x (foldr f z xs)
      

      所以当你插入参数时,你会得到

      foldr getNextPrime [2] [3,5..]
          = getNextPrime 3 (foldr getNextPrime [2] [5,7..])
          = getNextPrime 3 (getNextPrime 5 (foldr getNextPrime [2] [7,9..])
          etc...
      

      为了延迟生成值(处理无限列表时您想要的),getNextPrime 需要延迟生成值。查看getNextPrimeprimes ++ [n] 的定义,这意味着您要在primes 列表的末尾附加一个值,但getNextPrime 3primesgetNextPrime 5 (foldr getNextPrime [2] [7,9..])。但是,primes for getNextPrime 5getNextPrime 7 (foldr getNextPrime [2] [9,11..]) 等等。您实际上永远无法为 primes 生成正常形式的值,它始终是一个永远不会返回的计算链。

      另一种看待这个的方法是看这个是用一个运算符替换getNextPrime,我们称之为.:

      foldr (.:) [2] [3,5..9]
          = 3 .: (5 .: (7 .: (9 .: [2])))
      

      (这就是为什么它被称为右折叠,括号嵌套在右边)

      这非常适合在foldr 中使用:

      foldr (:) [2] [3,5..9]
          = 3 : (5 : (7 : (9 : [2])
      

      因为: 只是构建了一个新的数据结构,并且可以检查这个数据结构的第一个元素而无需计算结构的其余部分。但是.:不是很好,它首先需要计算x1 = 9 .: [2],然后是x2 = 7 .: x1,然后是x3 = 5 .: x2,最后是3 .: x3。而对于[3,5..],您永远无法计算出something .: [2] 的最后一次调用,但haskell 一直在尝试计算它,这会破坏堆栈。

      【讨论】: