Haskell - 递归堆栈溢出
Haskell - Recursion Stack Overflow
我正在尝试将所有 n 从 1 加到一个非常大的数字(目前为 10**9),但它会导致堆栈溢出。此外,我不认为在 1 处停止并在不同行中求和 n 是最有效的方法,但下面的代码是我对 Haskell 的全部了解。我真的不太了解函数式编程,我希望尽可能多地解释。 (我也试过把 $!strict 放在最后一行,这在其他地方被告知但它没有改变。如果你解释我可以执行这个递归函数的最有效方法,我会很高兴。)
main :: IO()
summ 1 = 1
summ n = 1/(n**2) + summ (n-1)
expected_value = pi*pi/6
errorPercent n = n / expected_value * 100
main = do
putStr "%"
print (errorPercent (summ $! (10**9)))
这里的问题是在整个 10^9 递归调用结束之前无法开始计算总和。本质上,你是在计算
1/(n**2) + ( 1/((n-1)**2) + ( 1/((n-2)**2) + ....
括号阻止开始求和。相反,我们希望
(( 1/(n**2) + 1/((n-1)**2) ) + 1/((n-2)**2) ) + ....
最简单的方法是使用 "accumulator" 附加参数:
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
)。
summ :: Int -> Double -> Double
summ 1 acc = 1 + acc
summ n acc = summ (n-1) $! acc + 1 / (fromIntegral n**2)
main :: IO ()
main = do
putStr "%"
print (summ (10^9) 0) -- set acc to 0 at the beginning
chi 已经回答了问题的一部分,我认为这是主要问题,但还有其他问题困扰着我。当你说 10**9
时,你会得到一个 浮点数 数字(因为 **
是 "fractional" 指数)。然后您使用浮点相等性来检查递归的基本情况。
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)
在任何一种情况下,您都应该采纳 chi 的建议,以累加器样式来执行此操作,并且您还应该添加类型签名。
如果你好奇的话,这里是more on how you get stack overflows in Haskell。
我正在尝试将所有 n 从 1 加到一个非常大的数字(目前为 10**9),但它会导致堆栈溢出。此外,我不认为在 1 处停止并在不同行中求和 n 是最有效的方法,但下面的代码是我对 Haskell 的全部了解。我真的不太了解函数式编程,我希望尽可能多地解释。 (我也试过把 $!strict 放在最后一行,这在其他地方被告知但它没有改变。如果你解释我可以执行这个递归函数的最有效方法,我会很高兴。)
main :: IO()
summ 1 = 1
summ n = 1/(n**2) + summ (n-1)
expected_value = pi*pi/6
errorPercent n = n / expected_value * 100
main = do
putStr "%"
print (errorPercent (summ $! (10**9)))
这里的问题是在整个 10^9 递归调用结束之前无法开始计算总和。本质上,你是在计算
1/(n**2) + ( 1/((n-1)**2) + ( 1/((n-2)**2) + ....
括号阻止开始求和。相反,我们希望
(( 1/(n**2) + 1/((n-1)**2) ) + 1/((n-2)**2) ) + ....
最简单的方法是使用 "accumulator" 附加参数:
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
)。
summ :: Int -> Double -> Double
summ 1 acc = 1 + acc
summ n acc = summ (n-1) $! acc + 1 / (fromIntegral n**2)
main :: IO ()
main = do
putStr "%"
print (summ (10^9) 0) -- set acc to 0 at the beginning
chi 已经回答了问题的一部分,我认为这是主要问题,但还有其他问题困扰着我。当你说 10**9
时,你会得到一个 浮点数 数字(因为 **
是 "fractional" 指数)。然后您使用浮点相等性来检查递归的基本情况。
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)
在任何一种情况下,您都应该采纳 chi 的建议,以累加器样式来执行此操作,并且您还应该添加类型签名。
如果你好奇的话,这里是more on how you get stack overflows in Haskell。