Haskell 惰性计算和嵌套thunk消耗内存
我正在研究一个小型的lambda微积分引擎,我希望它像Haskell一样懒惰。我正在努力,至少现在,坚持Haskell的规则,这样我就不必重新思考一切,但我不想盲目地这样做 我知道Haskell不会评估一个术语,直到需要它的值。这是我的第一个疑问。我理解当值是内置函数的参数时是“需要的”(因此在Haskell 惰性计算和嵌套thunk消耗内存,haskell,lazy-evaluation,lambda-calculus,Haskell,Lazy Evaluation,Lambda Calculus,我正在研究一个小型的lambda微积分引擎,我希望它像Haskell一样懒惰。我正在努力,至少现在,坚持Haskell的规则,这样我就不必重新思考一切,但我不想盲目地这样做 我知道Haskell不会评估一个术语,直到需要它的值。这是我的第一个疑问。我理解当值是内置函数的参数时是“需要的”(因此在(func x),如果func是内置函数并且(func x)是需要的),或者因为是要调用的函数(因此在(xy),如果(xy)是需要的,则需要x) 我的第二个问题是,假设我有这个递归函数: let st =
(func x)
,如果func
是内置函数并且(func x)
是需要的),或者因为是要调用的函数(因此在(xy)
,如果(xy)
是需要的,则需要x
)
我的第二个问题是,假设我有这个递归函数:
let st = \x -> (st x)
到目前为止,我实现它的方式是,如果我像(st“hi”)
那样调用它,“hi”
将不会被计算,而是被包装在一个thunk中,它包含术语及其范围,它将作为“x”添加到st
的主体范围中。然后,当再次评估st
时,将围绕(st x)
中的x
创建另一个thunk,该thunk将包含术语x
及其范围,该范围包含x
的另一个定义,即“hi”。这样,嵌套的thunk将不断累积,直到内存耗尽
我在GHCI中测试了上面的代码,内存正常。然后我测试了这个:
let st = \x -> (st (id x))
在应用程序崩溃之前,内存一直在积累。显然,当参数是函数调用时,GHCI(或Haskell?)只使用thunks;在其他情况下,它使用术语的值。这是我可以轻松实现的
我想到的另一个选择是,在计算函数调用和为参数创建thunk之前,不允许嵌套thunk by,计算整个当前范围以确保新thunk不会包含另一个thunk。我认为这仍然可以让我创建无限列表,并获得惰性计算的一些(或全部?)好处,甚至可以防止在调用let st=\x.(st(id x))
函数时应用程序崩溃
我相信有很多方法可以实现惰性评估,但很难找出每种方法的优缺点。是否有一些列表包含最常见的惰性评估实现及其优缺点?还有,哈斯克尔是如何做到的?这远不是一个完整的答案,但也许这足以取得进展 第一,功能
let st = \x -> (st x)
将st
绑定到lambda。这可能是指向lambda的thunk,也可能不是(Haskell报告只指定非严格的求值。如果编译器可以证明早期求值thunk不会改变程序的语义,那么可以自由地这样做;证明源代码中的lambda可以在不改变语义的情况下求值为WHNF是很简单的)
无论如何,假设您强制计算st“hi”
。应用lambda(β降低)后,下一步是st“hi”
。因此,这种beta缩减会无休止地循环,但它永远不会创建新数据。也就是说,没有必要用砰砰声来包装任何东西。因此,尽管它永远循环,但这个应用程序不分配内存
将此与
let st = \x -> (st (id x))
在这里,如果我们减少:
st "hi"
st (id "hi")
st (id (id "hi"))
st (id (id (id "hi")))
等等。在这里,因为st
的参数从未被计算过,所以它建立了一个无休止的thunk链来包装一个新的id
应用程序,消耗了越来越多的内存
我认为实现中存在的问题是在lambda下面包装“hi”。相反,任何产生“hi”的东西都应该创建thunk,然后传递它,直到对其进行评估。然后,“hi”只被包装一次,而不是在每一步
编辑:忘了回答你的第一个问题,但我没有比@leftaroundabout建议阅读弱头范式更好的了。关于SO还有其他问题,例如和阅读弱头范式等。例如,我这样做的方式甚至不是检查参数,只是盲目地将它们包装在一个包含范围的thunk中,所以是的,包装“hi”,然后是包含“hi”的
x
,然后是另一个x
,包含前面的x
,等等。但你的意思是,我应该检查这个术语是否是变量,如果是的话,使用它所代表的thunk,而不是将它包装成另一个thunk,对吗?这就是GHCi所做的吗(至少基本上如此)?@JuanLuisSoldi在ghc中,thunks是一种价值属性;(几乎)任何值都可以评估或不评估(thunk)。在第二个示例中,ghc将应用IDX
(正是这个函数应用程序创建了新值/thunk)并将该值传递给递归调用。听起来你的编译器做了些别的事情,我想我对它的理解还不足以提出任何建议。但是你根据期限结构来决定是否创建一个新thunk的想法是可行的。它是否适用于idx
?难道它不应该把它放在一个重击没有应用它,因为它是懒惰的?这不是我运行GHCi时崩溃的原因吗?@JuanLuisSoldi抱歉,我不清楚。当我说应用了这个函数时,我的意思是ghc做了类似于let x'=id x in st x'
,其中x'
是传递到下一阶段的thunk。(函数应用与将结果值减少为WHNF不同)。我不确定ghc是如何决定什么不需要包装的,这部分发生在我不太熟悉的管道的STG阶段。全部细节可能在