我正在通过Real World 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以及这里的各种回答让我相信它是懒惰评估的副产品,并且在它溢出之前,thunk正在积累,但到目前为止我还没有成功修复它。我尝试在各个地方使用seq
来强制进行评估,但它没有产生影响。我是在正确的轨道上吗?还有其他我没有得到的东西吗?
答案 0 :(得分: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
显然你不需要isPrime
primes
函数参数化,但这只是一个问题。此外,当您知道所有数字都是正数时,您应该使用rem
而不是mod
- 这会导致我的机器性能提高30%(当找到百万分之一的素数时)。
答案 1 :(得分:2)
首先,你没有尾递归,但守护递归,a.k.a。tail recursion modulo cons。
正如其他人评论的那样,你获得堆栈溢出的原因是一堆垃圾堆积。但是哪里?一个建议的罪魁祸首是你使用(++)
。虽然不是最优的,但(++)
的使用不一定会导致thunk堆积和堆栈溢出。例如,调用
take 2 $ filter (isPrime primes) [15485860..]
应该立即生成[15485863,15485867]
,并且没有任何堆栈溢出。但它仍然是使用(++)
的相同代码,对吧?
问题是,您有两个列表,您调用了primes
。一个(在顶层)是无限的,通过保护(非尾部)递归共同递归地产生。另一个(loop
的一个参数)是一个有限列表,通过将每个新发现的素数添加到其末尾来构建,用于测试。
但是当它用于测试时,它并没有被强制执行到最后。如果发生这种情况,就不会有SO问题。 只强制通过测试号码的sqrt
。所以(++)
thunks积累了那一点。
调用isPrime primes 15485863
时,会强制顶级primes
达到3935
,这是547个素数。内部测试素数列表也包含547个素数,其中只有前19个被强制。
但是当你拨打primes !! 1000000
时,重复内部列表中的1,000,000个素数中只有547个被强制使用。其余的都是砰的一声。
如果您只在候选人中看到 square 时在testing-primes
列表的末尾添加新素数,则testing-primes
列表将始终强制执行,或接近它的结束,并没有导致SO的thunk堆积。并且将(++)
附加到强制列表的末尾并不是那么糟糕,当下一次访问强制列出到它的结尾并且不留下任何thunk。 (它仍然会复制列表。)
当然,顶级primes
列表可以直接使用,正如Thomas M. DuBuisson在他的回答中所示。
但内部列表有其用途。如果正确实现,只有在候选者中看到正方形时才向其添加新素数,当使用优化进行编译时,它可能允许您的程序在 O(sqrt(n))
空间中运行。
答案 2 :(得分:0)