我正在学习Haskell,并且遇到了意外的“堆栈溢出”异常。
代码非常简单:
type Reals = Double
prod :: Reals -> Reals -> Reals
prod a b = a*b
coprod :: Reals -> Reals -> Reals
coprod a b = a + b - (prod a b)
newsum :: Reals -> Reals -> Int -> Reals
newsum a b n =
approxA!!n
where
approxA = a : zipWith coprod approxA approxB
approxB = b : zipWith prod approxA approxB
函数prod,coprod打算在[0,1]中实际进行评估。函数
newsum a b n
对于n趋于无穷大,计算函数(a,b)-> min(1,a + b)。
您可以尝试
newsum 0.5 0.5 10
和
newsum 0.5 0.5 10000
看到这个。但是,通过调用
newsum 0.5 0.5 10000000
我出现堆栈溢出。 newsum函数很简单,并且使Double类型的操作线性(以n为单位)。换句话说,[Reals]类型的(无限)列表的构造第n个元素
约A
和
约B
花费线性时间。
问题1:为什么堆栈出现溢出? | Haskell(或GHCi)的局限性是什么?如何通过上述简单程序(提前)估算它们?
问题2:如果需要的话,Haskell中是否有一个标志/命令来指示解释器自动分页内存?我想编写类似于Math的代码,不想花时间思考内存限制,堆栈溢出之类的问题。
谢谢!
答案 0 :(得分:4)
问题在于(!!)
是惰性的:它不会强制对列表中的较早值进行求值,因此ghci
的结尾是第n个项目的巨大表达式,该表达式溢出了其堆栈
您可以使用运行时选项来增加堆栈大小,例如对于16GB堆栈:
$ ghci +RTS -K16G
> newsum 0.5 0.5 10000000
0.9999999000001789
但是,如果您超出了系统可用的内存(其中可能包括虚拟内存/交换页),则会遇到问题(Linux OOM杀手here将我击倒了):
> newsum 0.5 0.5 100000000
Killed
$
在实践中,可以通过编写一个函数来强制该列表的每个标题在访问其尾部之前进行评估:
strict :: [a] -> [a]
strict [] = []
strict (x:xs) = seq x (x : strict xs)
seq x y
强制要求x
的值时,对Double
进行评估(对WHNF,对于y
而言,它是完全评估的)。
像这样使用strict
:
newsum' :: Reals -> Reals -> Int -> Reals
newsum' a b n =
strict approxA!!n
where
approxA = a : zipWith coprod approxA approxB
approxB = b : zipWith prod approxA approxB
现在,它可以在较小的常量内存中运行。
另一种选择是使用更严格的数据结构(!a
是严格的a
):
data List' a = !a :! List' a
但是您需要为此类型重新实现(!!)
和zipWith
。