2012-08-12 50 views
4

我正在通过真实世界Haskell(我在第4章)中工作,并且练习了一下我创建了以下程序来计算第n个素数。当计算素数时堆栈空间溢出

import System.Environment 

isPrime primes test = loop primes test 
    where 
     loop (p:primes) test 
      | test `mod` p == 0 = False 
      | p * p > test = True 
      | otherwise = loop primes test 


primes = [2, 3] ++ loop [2, 3] 5 
    where 
     loop primes test 
      | isPrime primes test = test:(loop primes' test') 
      | otherwise = test' `seq` (loop primes test') 
      where 
       test' = test + 2 
       primes' = primes ++ [test] 

main :: IO() 
main = do 
    args <- getArgs 
    print(last (take (read (head args) :: Int) primes)) 

很明显,既然我保存了素数列表,这不是一个恒定的空间解决方案。问题是,当我试图得到一个非常大的素说./primes 1000000我收到的错误:

Stack space overflow: current size 8388608 bytes. 
    Use `+RTS -Ksize -RTS' to increase 

我相当肯定,我得到了尾递归权利;阅读http://www.haskell.org/haskellwiki/Stack_overflow,这里的各种回应让我相信它是懒惰评价的副产品,而且它正在积累,直到它溢出,但到目前为止,我一直未能解决它。我试过在各个地方使用seq来强制评估,但没有产生效果。我在正确的轨道上吗?还有什么我没有得到?

+2

你说得对,懒惰与尾递归不能很好地融合,但是当使用列表时,解决方案通常是去除尾递归,而不是去除懒惰。 – sepp2k 2012-08-12 22:44:42

+0

@ sepp2k虽然它通常不是彻底删除TR,而是将它变成“尾递归模保留”(不仅仅是* list * cons,* any * cons),也就是守护递归。但要说TRMC根本不是TR,不会觉得IMO是正确的。也许这个概念不如它应该是的。 :)当守卫rec不是TRMC,那正好意味着thunk堆积起来。 TRMC意味着,*常数*递归调用之间的工作量,重新安排*在调用之前完成,而不是*在*之后,如在常规递归方案中那样。 – 2012-08-13 06:53:25

回答

6

正如我在我的评论说,你不应该通过附加一个单一的元素列表建立一个列表到一个非常长的列表末尾(您的行primes' = primes ++ [test])。最好定义无限列表,primes,让懒惰的评估做它的事情。像下面的代码:

primes = [2, 3] ++ loop 5 
    where. 
     loop test 
      | isPrime primes test = test:(loop test') 
      | otherwise = test' `seq` (loop test') 
      where 
       test' = test + 2 

很明显,你不需要通过primes无论是参数化isPrime功能,但是这只是一个挑剔。此外,当你知道所有的数字都是正数时,你应该使用rem而不是mod--这会导致我的机器性能提高30%(找到第一百万个素数时)。

+0

噢,我的解决方案非常有意义。我仍然不完全明白为什么我的解决方案被炸毁,但我认为我有点得到它。我会冥想一下,继续学习。谢谢! – 2012-08-13 00:35:14

+0

由于“(++)”的定义,您正在堆栈上等效于'prime1:prime 2:prime3:...:prime1000000:[]'',所以您的解决方案已经爆炸了。 – 2012-08-13 00:37:00

+0

啊!刚刚点击!所以++实际上通过运行prime1:prime2 ...来创建新列表,所以当列表变长时,它就会被吹起来,因此为什么试图用'seq'强制评估素数并不能帮助我。 – 2012-08-13 00:52:21

0
+1

我很欣赏这种回应,但我已经知道如何增加堆栈,但我的理解是,我不需要这样做,因为它是尾递归。我尝试过使用seq,正如其他人所建议的那样,但它并没有产生任何效果,这意味着要么我没有弄清楚如何正确使用它,要么我无法确定thunk在哪堆积。 – 2012-08-12 22:46:13

+0

你认为这是所有尾递归? [this]怎么样(http://hackage.haskell.org/packages/archive/base/latest/doc/html/src/GHC-Base.html#%2B%2B)。你正在消耗O(n)堆栈,其中'n'是质数。将单个项目放在列表的末尾是一个不好的主意,最好是让表达式构建整个无限列表(显然是懒惰的)。 – 2012-08-13 00:14:00

2

首先,您在这里没有尾递归,但守护递归,又名tail recursion modulo cons

正如其他人评论的那样,堆栈溢出的原因是thunk堆积。但是哪里?一个建议的罪魁祸首是您使用(++)。尽管不是最佳的,但使用(++)不一定导致thunk堆积和堆栈溢出。例如,调用

take 2 $ filter (isPrime primes) [15485860..] 

应该在任何时间产生[15485863,15485867],且无任何堆栈溢出。但它仍然是使用(++)的代码,对吧?

的问题是,你有列出你叫primes。一个(在顶层)是无限的,通过守护(而非尾)递归共同递归产生。另一个(loop的参数)是一个有限列表,通过将每个新发现的素数添加到其末尾来构建,用于测试。

但是,当它用于测试时,它不会被迫通过它的结尾。如果发生这种情况,那么就不存在这样的问题。 它只能通过测试号sqrt。所以(++) thunk克服了这一点。

当调用isPrime primes 15485863时,它会强制顶级primes高达3935,这是547个素数。内部测试素数列表也由547个素数组成,其中只有前19个是强制的。

但是,当您拨打primes !! 1000000时,重复内部列表中的1,000,000个素数中只有547个被强制。其余的都在thunk中。

如果你添加新的素数只有当他们的广场是候选人中看到testing-primes列表的末尾,该testing-primes清单将始终贯穿完全强制,或接近它的结束,就不会有引起SO的thunk pileup。并且附加(++)强制列表的末尾并不是那么糟糕,当下一个访问强制列表到它的结尾并且没有留下thunk时。 (它仍然会复制列表。)

当然,顶级primes列表可以直接使用,正如Thomas M. DuBuisson在他的回答中所示。

但内部列表有其用途。如果正确实施,只有在候选人中看到他们的正方形时才添加新素数,它可能允许您的程序在优化编译时运行O(sqrt(n))空间

+0

谢谢,这使得它更清楚为什么溢出发生。所以如果我正确地理解了守护递归的东西,[[++]']的实现(http://hackage.haskell.org/packages/archive/base/latest/doc/html/src/GHC- Base.html#%2B%2B)也受到保护,只要它被迫不会溢出,那是正确的吗? – 2012-08-13 20:50:02

+0

@WaltonHoops我认为是的,是的。访问'xs ++ [z]'的最后一个元素强制执行,在进程中重新复制列表(除非在非常智能编译器上)。见例如http://stackoverflow.com/q/11656507如何*不*建立一个列表(追加(++),但尾递归)。在quicksort的'qsort small ++ [x] ++ qsort big'中,'++ [x] ++'是无害的。当排序列表的头部被访问时,存在对数数量的thunk(非简并数据)。随着排序列表被访问,thunk的数量保持对数。 – 2012-08-14 05:13:10

+0

@WaltonHoops但是在你的代码中,添加到整个列表(即使完全/部分被强制)会在整个列表上创建新的thunk,并且整个列表需要再次访问直至结束*以强制thunk出来。因此,只要根据需要在内部列表中添加尽可能多的素数,就可以完全(或几乎如此)地访问列表,从而使得Thunk的数量保持最小。你的代码存在的问题是,你在整个列表上(从它的头部)得到一个句柄(一个命名的var),并且反复地从头开始添加它。 – 2012-08-14 05:32:29