【问题标题】:Lazy evaluation and nested thunks eating up memory惰性求值和嵌套的 thunk 占用内存
【发布时间】:2014-08-13 06:27:15
【问题描述】:

我正在开发一个小型 lambda 演算引擎,我希望它像 Haskell 一样懒惰。我正在努力,至少现在,坚持 Haskell 的规则,这样我就不必重新考虑所有事情,但我不想盲目地这样做。

我知道 Haskell 不会评估一个术语,直到需要它的值。这是我的第一个疑问。我知道当是内置函数的参数时,值是“需要”的(所以在(func x) 中,如果func 是内置函数并且需要(func x),则需要x)或者因为它是一个函数被调用(所以在(x y)中,如果需要(x y),则需要x)。

我的第二个问题是,假设我有这个递归函数:

let st = \x -> (st x)

到目前为止,我实现它的方式是,如果我将其称为 (st "hi")"hi" 将不会被评估,而是包装在包含术语及其范围的 thunk 中,它将添加为“ x" 到 st 的主体范围内。然后,当再次评估st 时,将围绕(st x) 中的x 创建另一个thunk,其中将包含术语x 及其范围,其中包含x 的另一个定义,即“hi ”。这样,嵌套的 thunk 将继续建立,直到内存不足。

我在 GHCI 中测试了上面的代码,内存正常。然后我测试了这个:

let st = \x -> (st (id x))

和内存建立,直到应用程序崩溃。所以显然 GHCI(或 Haskell?)仅在参数是函数调用时才使用 thunk;在所有其他情况下,它都使用该术语的值。这是我可以轻松实现的。

我想到的另一个选项是不允许嵌套 thunk,在评估函数调用并为参数创建 thunk 之前,评估整个当前范围以确保没有新的 thunk 将包含另一个 thunk。我认为这仍然可以让我创建无限列表并获得惰性评估的一些(或全部?)好处,甚至可以防止应用程序在调用 let st = \x.(st (id x)) 函数时崩溃。

我确信有很多方法可以实现惰性求值,但很难弄清楚每种方法的优缺点。是否有一些列表包含最常见的惰性评估实现及其优缺点?还有,Haskell 是怎么做到的?

【问题讨论】:

标签: haskell lazy-evaluation lambda-calculus


【解决方案1】:

这远不是一个完整的答案,但也许它足以取得进展。

一、功能

let st = \x -> (st x)

st 绑定到一个 lambda。这可能是指向 lambda 的 thunk,也可能不是(Haskell 报告仅指定非严格评估。如果编译器可以证明早期评估 thunk 不会改变程序的语义,它是免费的所以;证明源代码中的 lambda 可以在不改变语义的情况下评估为 WHNF 是微不足道的)。

无论如何,假设您强制评估st "hi"。应用 lambda(beta 缩减)后,下一步是 st "hi"。因此,这种 beta 缩减无限循环,但它永远不会创建新数据。也就是说,没有必要在 thunk 中包装任何东西。因此,尽管它永远循环,但此应用程序不会分配内存。

比较一下

let st = \x -> (st (id x))

这里,如果我们 beta 减少:

st "hi"
st (id "hi")
st (id (id "hi"))
st (id (id (id "hi")))

等等。在这里,因为 st 的参数从未被评估过,它会构建一个无限的 thunk 链来包装一个新的 id 应用程序,从而消耗越来越多的内存。

我认为您在实现中遇到的问题是您将“hi”包装在 lambda 下方。相反,任何产生“hi”的东西都应该创建一个thunk,然后它会被传递直到它被评估。然后“hi”只被包裹一次,而不是在每一步。

编辑:忘了回答你的第一个问题,但我不能比@leftaroundabout 的建议更好地阅读弱头范式。还有其他关于 SO 的问题,例如Haskell: What is Weak Head Normal Form?Weak head normal form and order of evaluation

【讨论】:

  • 我这样做的方式甚至没有检查参数,只是盲目地将它们包装在一个也包含范围的 thunk 中,所以是的,是包装“hi”,然后是 x,其中包含“hi”,然后是另一个x,其中包含前面的x,依此类推。但是你说的是我应该检查这个术语是否是一个变量,如果是的话,使用它所代表的 thunk 而不是将它包装到另一个 thunk 中,对吧?这是 GHCi 所做的(至少基本上是这样)吗?
  • @JuanLuisSoldi 在 ghc 中,thunk 是值的属性; (几乎)任何值都可以被评估或不被评估(重击)。在第二个示例中,ghc 将应用 id x(这是创建新值/thunk 的函数应用行为)并将该值传递给递归调用。听起来您的编译器做了其他事情,我认为我对它的理解不够好,无法提出任何建议。但是您根据术语结构决定是否创建新的 thunk 的想法可能会奏效。
  • 是否适用id x?不应该因为懒惰而将其放入一个thunk而不应用它吗?这不是我运行 GHCi 时崩溃的原因吗?
  • @JuanLuisSoldi 抱歉,我不清楚。当我说应用了函数时,我的意思是 ghc 做了类似let x' = id x in st x' 的事情,x' 是传递到下一阶段的 thunk。 (函数应用与将结果值减少到 WHNF 不同)。我不确定 ghc 如何决定不需要包装的内容,这部分发生在我不太熟悉的管道的 STG 阶段。完整的细节可能在research.microsoft.com/apps/pubs/default.aspx?id=67083
猜你喜欢
  • 2020-06-24
  • 1970-01-01
  • 1970-01-01
  • 2023-04-05
  • 1970-01-01
  • 2021-12-23
  • 1970-01-01
  • 2018-06-02
  • 1970-01-01
相关资源
最近更新 更多