为啥递归`let`使空间有效?
Posted
技术标签:
【中文标题】为啥递归`let`使空间有效?【英文标题】:Why recursive `let` make space effcient?为什么递归`let`使空间有效? 【发布时间】:2013-05-13 23:38:57 【问题描述】:我在学习函数响应式编程时发现了这个声明,来自 Hai Liu 和 Paul Hudak 的 "Plugging a Space Leak with an Arrow"(第 5 页):
Suppose we wish to define a function that repeats its argument indefinitely: repeat x = x : repeat x or, in lambdas: repeat = λx → x : repeat x This requires O(n) space. But we can achieve O(1) space by writing instead: repeat = λx → let xs = x : xs in xs
这里的差异看起来很小,但它极大地提高了空间效率。为什么以及如何发生?我做出的最佳猜测是手动评估它们:
r = \x -> x: r x
r 3
-> 3: r 3
-> 3: 3: 3: ........
-> [3,3,3,......]
如上所述,我们需要为这些递归创建无限的新 thunk。然后我尝试评估第二个:
r = \x -> let xs = x:xs in xs
r 3
-> let xs = 3:xs in xs
-> xs, according to the definition above:
-> 3:xs, where xs = 3:xs
-> 3:xs:xs, where xs = 3:xs
在第二种形式中,xs
出现并且可以在它出现的每个地方之间共享,所以我想这就是为什么我们只能要求 O(1)
空格而不是 O(n)
。但我不确定我是否正确。
顺便说一句:关键字“共享”来自同一篇论文的第 4 页:
这里的问题是标准的按需调用评估规则 无法识别该功能:
f = λdt → integralC (1 + dt) (f dt)
等同于:
f = λdt → let x = integralC (1 + dt) x in x
前一个定义导致递归调用中重复工作 到 f,而在后一种情况下,计算是共享的。
【问题讨论】:
【参考方案1】:用图片最容易理解:
第一个版本
repeat x = x : repeat x
创建以 thunk 结尾的 (:)
构造函数链,它将根据您的需要用更多构造函数替换自身。因此,O(n) 空间。
第二个版本
repeat x = let xs = x : xs in xs
使用let
来“打结”,创建一个引用自身的(:)
构造函数。
【讨论】:
【参考方案2】:简单地说,变量是共享的,但函数应用程序不是。在
repeat x = x : repeat x
(从语言的角度来看)对 repeat 的(共)递归调用具有相同的参数,这是一个巧合。因此,如果没有额外的优化(称为静态参数转换),函数将被一次又一次地调用。
但是当你写的时候
repeat x = let xs = x : xs in xs
没有递归函数调用。你取一个x
,并使用它构造一个循环值xs
。所有分享都是明确的。
如果你想更正式地理解它,你需要熟悉惰性求值的语义,比如A Natural Semantics for Lazy Evaluation。
【讨论】:
【参考方案3】:您对xs
被共享的直觉是正确的。在你写作的时候,用重复而不是积分来重申作者的例子:
repeat x = x : repeat x
该语言无法识别右侧的repeat x
与表达式x : repeat x
产生的值相同。而如果你写
repeat x = let xs = x : xs in xs
你明确地创建了一个结构,当评估时看起来像这样:
hd: x, tl:|
^ |
\________/
【讨论】:
以上是关于为啥递归`let`使空间有效?的主要内容,如果未能解决你的问题,请参考以下文章
为啥 QuickSort 使用 O(log(n)) 额外空间?
如何使对命令提示符窗口颜色的更改永久有效而不是只对本次有效?