Haskell 试图显示数字时GHCI中的堆栈溢出

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(

在学习Haskell的过程中,为了正确理解函数和递归,我实现了一个pi计算

使用用于计算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)
但如果我的容忍度设置得太好,就会发生这种情况:

*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` ...