为什么Sierat of Eratosthenes需要额外的辅助函数来合并无限列表?

时间:2014-01-06 01:58:01

标签: haskell ghc lazy-evaluation

我正在使用来自Literate Programming(http://en.literateprograms.org/Sieve_of_Eratosthenes_%28Haskell%29)的Eratosthenes Sieve代码,略微修改以包含合并和差异的边缘情况:

primesInit = [2,3,5,7,11,13]
primes = primesInit ++ [i | i <- diff [15,17..] nonprimes]

nonprimes = foldr1 f . map g $ tail primes
        where g p = [n * p | n <- [p,p+2..]]
              f (x:xt) ys = x : (merge xt ys)

merge :: (Ord a) => [a] -> [a] -> [a]
merge [] ys = ys
merge xs [] = xs
merge xs@(x:xt) ys@(y:yt)
    | x < y  = x : merge xt ys
    | x == y = x : merge xt yt
    | x > y  = y : merge xs yt

diff :: (Ord a) => [a] -> [a] -> [a]
diff [] ys = []
diff xs [] = xs
diff xs@(x:xt) ys@(y:yt)
    | x < y  = x : diff xt ys
    | x == y = diff xt yt
    | x > y  = diff xs yt

合并和差异都是懒惰的。非主流和素数也是如此。但是如果我们改变素数的定义来删除f,如:

nonprimes = foldr1 merge . map g $ tail primes
        where g p = [n * p | n <- [p,p+2..]]

现在非普通人并不懒惰。我也用take 20 $ foldr1 merge [[i*n | n <- [3,7..]] | i <- [5,9..]]重新创建了它(GHCI内存耗尽并退出)。

基于http://www.haskell.org/haskellwiki/Performance/Laziness,在返回数据构造函数之前,一个简单的非惰性源是递归。但合并没有这个问题;它返回一个包含递归调用的cons-cell作为第二个项目。也不应该使用foldr本身就是罪魁祸首(它不能做无限列表的折叠)。

那么,为什么需要将fold与foldr1分开,这实际上是第一次手动合并?所有f都返回一个cons单元格,其中包含要合并的调用作为第二项,对吗?

注意:Stack Overflow上的其他人正在使用类似的代码并遇到了我遇到的同样的问题,但是他们接受了一个看起来像我基本上不同代码的答案。我问为什么,而不是如何,因为看起来懒惰在Haskell中有点重要。

2 个答案:

答案 0 :(得分:3)

让我们再次比较这两个函数:

merge [] ys = ys
merge xs [] = xs
merge xs@(x:xt) ys@(y:yt)
    | x < y  = x : merge xt ys
    | x == y = x : merge xt yt
    | x > y  = y : merge xs yt

f (x:xt) ys = x : (merge xt ys)

让我们忽略两者之间的语义差异,尽管它们很重要 - f在有效调用时受到更多限制。相反,我们只看一下严格属性。

自上而下检查多个方程中的模式匹配。从左到右检查单个方程内的多个模式匹配。所以merge做的第一件事是强制第一个参数的构造函数,以确定第一个方程是否匹配。如果第一个等式不匹配,则强制第二个参数的构造函数,以确定第二个等式是否匹配。只有当两个方程都不匹配时才会移到第三种情况。编译器很聪明,知道它已经在这一点上强制了两个参数,所以它不会再次执行 - 但是那些模式匹配将需要强制参数,如果它还没有。

但重要的是,确定哪个方程匹配的过程会导致在生成任何构造函数之前强制两个参数。

现在,将其与f进行对比。在f的定义中,唯一的模式匹配是在第一个参数上。因此,f不如merge严格。它在检查第二个参数之前生成一个构造函数。

事实证明,如果你仔细检查foldr的行为,它就会在传递给它的函数没有(总是)在生成构造函数之前检查它的第二个参数时精确地处理无限列表。

括号“总是”有趣。我最喜欢使用foldr和懒惰的一个例子是:

dropRWhile :: (a -> Bool) -> [a] -> [a]
dropRWhile p = foldr (\x xs -> if p x && null xs then [] else x:xs) []

这是一个最大惰性函数,其作用类似于dropWhile,但从列表的后面(右侧)开始。如果当前元素与谓词不匹配,则立即返回。如果它与谓词匹配,它会向前看,直到找到不匹配的内容或列表的结尾。这将在无限列表上高效,只要它最终找到与谓词不匹配的元素。这就是上面“始终”括号的来源 - 在生成构造函数之前通常不会检查其第二个参数的函数仍然允许foldr通常在无限列表上工作。

答案 1 :(得分:1)

要确定其输出的第一个元素,merge需要评估两个参数,以确定它们是否为空列表。如果没有该信息,则无法确定函数定义的哪种情况适用。

foldr1结合使用会成为merge尝试评估其第二个参数的问题。 nonprimes表达此形式:

foldr1 merge [a,b,c,...]

为了评估这一点,首先展开`foldr1:

merge a (foldr1 merge [b,c,...])

现在评估merge,检查其功能定义的情况。评估了第一个a,结果证明它不是一个空列表。因此merge的第一个案例不适用。接下来,需要评估merge的第二个参数,以查看它是否为空列表,以及是否适用merge定义的第二个案例。第二个参数是foldr1 merge [b,c,...]

但是为了评估这一点,我们与foldr1 merge [a,b,c,...]处于同样的情况,我们只是merge b (foldr1 merge [c,...])merge再次需要评估它的第二个参数检查它是否为空列表。

等等。对merge的每次评估都需要先对merge进行另一次评估,然后以无限递归结束。

使用f可以避免问题,因为它不需要查看顶级评估的第二个参数。 foldr1 f [a,b,c...]f a (foldr1 f [b,c,...]),其评估为非空列表a0 : merge a' (foldr1 f [b,c,...])。所以foldr1 f ...永远不会是一个空列表。这可以在没有任何无限递归的情况下确定。

现在对merge a' (foldr1 f [b,c,...])的评估也不是问题,因为第二个参数的计算结果为b0 : ...merge需要知道才能开始生成结果。< / p>