在尝试学习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
这对我来说似乎完全违反直觉;实际的计算工作正常,但只是试图打印多少递归调用失败??
为什么会这样?
答案 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
的值上,因此在计算过程中不会对其进行评估。