尝试显示数字时,GHCI中的堆栈溢出

时间:2013-01-30 09:41:04

标签: haskell ghci

在尝试学习Haskell时,我已经实现了pi计算,以便正确理解函数和递归。

使用Leibniz Formula来计算pi,我想出了以下内容,它将pi打印到给定参数的容差,以及递归函数调用的数量以获得该值:

reverseSign :: (Fractional a, Ord a) => a -> a 
reverseSign num = ((if num > 0
                        then -1
                        else 1) * (abs(num) + 2))

piCalc :: (Fractional a, Integral b, Ord a) => a -> (a, b)
piCalc tolerance = piCalc' 1 0.0 tolerance 0

piCalc' :: (Ord a, Fractional a, Integral b) => a -> a -> a -> b -> (a, b)
piCalc' denom prevPi tolerance count = if abs(newPi - prevPi) < tolerance
                                        then (newPi, count)
                                        else piCalc' (reverseSign denom) newPi tolerance (count + 1)
                                        where newPi = prevPi + (4 / denom)

因此,当我在GHCI中运行它时,它似乎按预期工作:

*Main> piCalc 0.001
(3.1420924036835256,2000)

但是,如果我将容忍度设置得太好,就会发生这种情况:

*Main> piCalc 0.0000001
(3.1415927035898146,*** Exception: stack overflow

这对我来说似乎完全违反直觉;实际的计算工作正常,但只是试图打印多少递归调用失败??

为什么会这样?

2 个答案:

答案 0 :(得分:10)

在计算过程中不会计算计数,因此它会留下大量的thunk(溢出堆栈)直到最后。

您可以在计算过程中强制执行评估,方法是启用BangPatterns扩展并撰写piCalc' denom prevPi tolerance !count = ...

那么为什么我们只需要强制评估count?好吧,所有其他参数都在if中进行评估。我们实际上需要在再次调用piCalc'之前检查它们,所以thunks没有建立起来;我们需要实际值,而不仅仅是“可以计算它们的承诺”!另一方面,count在计算过程中从不需要,并且可以保持为一系列的thunks直到最后。

答案 1 :(得分:8)

这是传统foldl (+) 0 [1..1000000]堆栈溢出的变体。问题是在评估piCalc'期间永远不会评估计数值。这意味着它只是带有一组不断增长的thunk,表示如果需要可以进行添加。在需要时,评估它需要堆栈深度与thunks数量成比例的事实会导致溢出。

最简单的解决方案是使用BangPatterns扩展名,将piCalc'的开头更改为

piCalc' denom prevPi tolerance !count = ...

这会强制在模式匹配时评估count的值,这意味着它永远不会生成巨大的thunk链。

等效地,不使用扩展名,您可以将其写为

piCalc' denom prevPi tolerance count = count `seq` ...

这与上述解决方案在语义上完全等效,但它明确地使用seq而不是通过语言扩展隐式地使用piCalc'。这使它更便携,但更冗长。

至于为什么pi的近似值不是嵌套thunk的长序列,但count是:newPi分支在计算结果上,需要prevPi,{{1}的值}和tolerance。它必须先检查这些值,然后才能确定它是否已完成或是否需要运行另一次迭代。这是导致执行评估的分支(当执行函数应用程序时,通常意味着函数的结果是模式匹配。)另一方面,piCalc'的计算中没有任何依赖在count的值上,因此在计算过程中不会对其进行评估。