Haskell-递归堆栈溢出

Haskell-递归堆栈溢出,haskell,recursion,stack-overflow,Haskell,Recursion,Stack Overflow,我试图将所有n从1求和到一个非常大的数字(目前为10**9),但这会导致堆栈溢出。另外,我不认为在1处停下来,在不同的行中求和n是最有效的方法,但是下面的代码是我对Haskell的全部知识。我真的不太懂函数式编程,我希望尽可能多的解释。(我还尝试将$!strict放在其他地方告诉我的最后一行中,但没有任何改变。如果您能解释一下我执行此递归函数的最有效方法,我将非常高兴。) 这里的问题是,在整个10^9递归调用结束之前,无法开始计算总和。本质上,你是在计算 1/(n**2) + ( 1/((n-1

我试图将所有n从1求和到一个非常大的数字(目前为10**9),但这会导致堆栈溢出。另外,我不认为在1处停下来,在不同的行中求和n是最有效的方法,但是下面的代码是我对Haskell的全部知识。我真的不太懂函数式编程,我希望尽可能多的解释。(我还尝试将$!strict放在其他地方告诉我的最后一行中,但没有任何改变。如果您能解释一下我执行此递归函数的最有效方法,我将非常高兴。)


这里的问题是,在整个10^9递归调用结束之前,无法开始计算总和。本质上,你是在计算

1/(n**2) + ( 1/((n-1)**2) + ( 1/((n-2)**2) + ....
括号阻止开始求和。相反,我们希望

(( 1/(n**2) + 1/((n-1)**2) ) + 1/((n-2)**2) ) + ....
最简单的方法是使用“累加器”附加参数:

summ 1 acc = 1 + acc
summ n acc = summ (n-1) $! acc + 1/(n**2)

main = do
    putStr "%"
    print (errorPercent (summ (10^9) 0))  -- set acc to 0 at the beginning
为了提高性能,我建议在
summ
中添加一个类型签名,例如
summ::Int->Double->Double


下面是完整的程序。这在12秒内运行(
ghc-O2


迟浩田回答了其中一个问题,我认为这是主要问题,但还有其他一些问题困扰着我。当你说
10**9
时,你会得到一个浮点数(因为
**
是“分数”幂运算)。然后使用浮点等式检查递归的基本情况

summ 1 = ...
问题是,这是可能的,而且随着参数变大,很可能由于数值错误,你将几乎不会错过基本情况,永远下降到负值

summ 4 =        ... summ 3
summ 3 =        ... summ 2.000001
summ 2.000001 = ... summ 1.000001 
summ 1.000001 = ... summ 0.000001  -- BASE CASE MISSED!
summ 0.000001 = ... summ (-1.000001)
summ (-1.000001) = ... summ (-2.000001)
等等。如果您没有从109个调用中得到堆栈溢出,那么肯定会有无限多个调用

您应该在整数上定义函数,这样就不会出现舍入错误

summ :: Int -> Double
summ 1 = 1
summ n = 1 / (fromIntegral n ** 2) + summ (n - 1)
--            ^^^^^^^^^^^^
-- conversion necessary to go from Int to Double

main = ... print (summ (10 ^ 9))
--                      ^^^^^^
--      use integral exponentiation (^) instead of (**)
或者使用更宽容的基本情况

summ :: Double -> Double
summ n | n <= 1 = 1
summ n = 1 / (n ** 2) + summ (n - 1)
summ::Double->Double

Summn | n它已经起作用了,我可以从10^9中获得输出,但它真的很慢。我目前正在等待10^10,10^9花了大约20分钟。这个速度是正常的还是有办法让它更快?@Terobero您是否在编译后通过优化运行代码,例如
-O2
?如果您使用GHCi来运行它,通常会非常慢。@Terobero我不知道VSCode。我猜它确实使用GHC编译代码,因为它运行
main
,而不是给您一个REPL提示符(对吗?)。在它的选项中应该有一些地方可以添加
-O2
,如果还没有启用。在命令行中,我将使用
stack ghc--File.hs-O2
@Terobero查看最后一次编辑。如果没有
errorPercent
,并且
-O2
处于打开状态,则此处带有10^9的程序将在12s内运行。您的20分钟太长了。@Terobero对于整数变量,使用
Int
或其他整数类型更快、更安全。使用
Double
可能会导致微妙的问题,例如
x==x+1
x
足够大
Double
时,舍入错误很容易破坏程序。我倾向于避免浮点运算,除非不精确的近似值足够好(例如,当我们需要精确比较时,例如
如果x==1,那么…
)。luqui在他的回答中说明了为什么在这里使用
Double
不起作用。哦,顺便说一句,你可以将
sum
写成
sum n=sum[1/i^2 | i@luqui-我想你会想要
sum[1/i^2 | i@Omnifarious有趣!我从来没有想过这样的问题。我不太明白为什么会发生…@luqui-由于舍入而发生。浮点数在相加的数字大小相似时效果最好。否则,从小数字中添加的位会从la的末尾旋转所以,即使你加上数十万个这样的小数字,它们也不会改变原来的数字。@luqui-作为经验法则,是的。我还可以想象在某些特定情况下,不同的顺序可能更好。其他操作也有其他规则。我不知道它们都是什么,但我知道充分利用浮点精度是需要仔细考虑的。
summ :: Int -> Double
summ 1 = 1
summ n = 1 / (fromIntegral n ** 2) + summ (n - 1)
--            ^^^^^^^^^^^^
-- conversion necessary to go from Int to Double

main = ... print (summ (10 ^ 9))
--                      ^^^^^^
--      use integral exponentiation (^) instead of (**)
summ :: Double -> Double
summ n | n <= 1 = 1
summ n = 1 / (n ** 2) + summ (n - 1)