Haskell 试图显示数字时GHCI中的堆栈溢出
在学习Haskell的过程中,为了正确理解函数和递归,我实现了一个pi计算 使用用于计算pi的函数,我得出以下结果,将pi打印到给定参数的公差,以及递归函数调用的次数,以获得该值:Haskell 试图显示数字时GHCI中的堆栈溢出,haskell,ghci,Haskell,Ghci,在学习Haskell的过程中,为了正确理解函数和递归,我实现了一个pi计算 使用用于计算pi的函数,我得出以下结果,将pi打印到给定参数的公差,以及递归函数调用的次数,以获得该值: reverseSign :: (Fractional a, Ord a) => a -> a reverseSign num = ((if num > 0 then -1 else 1) * (abs(
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)
但如果我的容忍度设置得太好,就会发生这种情况:
*Main> piCalc 0.0000001
(3.1415927035898146,*** Exception: stack overflow
这在我看来完全违反直觉;实际的计算工作正常,但只是尝试打印有多少递归调用失败
为什么会这样?在计算过程中从未计算过计数,因此直到最后都会留下大量的Thunk(溢出堆栈) 您可以通过启用
BangPatterns
扩展并写入piCalc'denom prevPi容差,在计算过程中强制对其求值!计数=…
那么为什么我们只需要强制计算
count
?那么,所有其他参数都在if
中计算。实际上,在再次调用piCalc'
之前,我们需要检查它们,这样thunks就不会出现;我们需要的是实际值,而不仅仅是“可以计算的承诺”<另一方面,在计算过程中不需要code>count,它可以作为一系列重击一直持续到最后。这是传统的foldl(+)0[1..1000000]
堆栈溢出的变体。问题是在计算piCalc'
的过程中从未计算过计数值。这意味着它只携带一组不断增长的thunk,表示需要添加的内容。当需要时,计算它需要与thunk数成比例的堆栈深度这一事实会导致溢出
最简单的解决方案是使用BangPatterns
扩展,将piCalc'
的开头改为
piCalc' denom prevPi tolerance !count = ...
这将强制在模式匹配时计算count
的值,这意味着它永远不会产生巨大的thunk链
等价地,并且不使用扩展,您可以将其编写为
piCalc' denom prevPi tolerance count = count `seq` ...
这在语义上与上述解决方案完全相同,但它显式地使用seq
,而不是通过语言扩展隐式地使用。这使它更便于携带,但有点冗长
至于为什么pi的近似值不是一长串嵌套的thunk,但计数是:
piCalc'
在需要newPi
、prevPi
和容差
值的计算结果上分支。在决定是否完成或是否需要运行另一个迭代之前,它必须检查这些值。正是该分支导致执行求值(当执行函数应用程序时,这通常意味着函数结果上存在模式匹配)。另一方面,piCalc'
的计算中没有任何内容取决于count
的值,因此,在计算过程中不会对其进行评估。如果您不知道thunk是什么(我在开始Haskell时不知道!),那么它基本上是一个未解决的计算。在第一个示例中,在打印count
之前,它的值不是2000
,而是…+1)+1)+1)+1)
(我在开头省略了2000个左括号:P)。当你打印它时,它实际上是加起来的。我只想补充一下@DanielBuckmaster所说的,重要的一点是thunks会不断累积,占用越来越多的内存,而人们天真地期望count
类似于Int
(空间常数)。你会习惯这一点,但这肯定会对你造成伤害。你能解释一下为什么在这个例子中,pi的计算值不会发生重击,而只会发生在计数上吗?@DanielBuckmaster这是因为piCalc'
在需要newPi
,prevPi
值的计算结果上分支,和公差
。在决定是否完成或是否需要运行另一个迭代之前,它必须检查这些值。正是该分支导致执行评估(当执行函数应用程序时,这通常意味着函数结果的模式匹配)。谢谢!我认为这是非常有价值的答案。这是count
导致堆栈溢出而不是实际计算的原因。
piCalc' denom prevPi tolerance count = count `seq` ...