Skip to content

算法Algorithm

Lambda 提升与传递环境参数

Lambda lifting · 函数提升

对只直接调用的局部函数组闭合传递需求,把自由变量变为普通参数,并说明返回函数仍为何需要闭包。

形式陈述 ​

Lambda 提升把局部函数移动到顶层,并把它们原先从词法环境取得的变量变成显式参数。本页使用一个严格受限版本:函数组处于同一外层作用域,组内允许递归与互递归,但函数名只能出现在静态可知的直接调用位置,不能返回、存入对象或传给未知函数;所有调用足额给定普通实参。变量已按声明 ID区分,外层绑定不可变;绑定值可以是指向共享可变单元的引用。

设函数集合为 F,外层变量身份集合为 X。每个函数 f 有普通形参集合 P(f),以及直接读取的外层变量集合 D(f);D 不含组内函数名、形参和函数局部 let 身份。令 f→g 表示 f 的体内直接调用 g。所需额外参数集合满足

Need(f)=D(f)∪⋃f→gNeed(g).

取这组方程的最小解。从 Need(f)=D(f) 开始,反复把每个被调用者需求并入调用者,直到不变。它是有限集合上的单调数据流闭合;环不会造成无限增长,因为至多加入 |F||X| 个“函数、变量”对。

固定 X 的身份顺序,生成顶层 f_lift(extra_f, ordinary_f)。体内直接读到外层 x 时改成对应额外形参;调用 g 时,在原有实参前加入 Need(g) 对应的当前值。组外调用 f 的位置仍处在这些外层变量作用域内,故能够提供参数。每个函数被分配自己的新鲜目标形参 ID,不能让不同函数共享一个裸局部槽。

对于更一般的嵌套作用域,需求传播要区分“调用点已有的局部值”和“必须再向外传入的值”。本页同作用域限制故意消除这层问题;不能对任意嵌套函数机械套无差别并集。原论文和教材的完整 lambda lifting 支持更广的语言,本页只实现上述可独立检查的算法。

直觉

闭包在函数值里带一个定义环境;闭包转换把它落实为记录,调用时递交那份记录;本页提升让每个已知调用点直接传入所需值。只把函数文本搬到顶层不够:f 虽然自己不读 x,却可能调用读取 x 的 g,于是 f 必须把 x 接进来才能继续传下去。

这里传递的是值。如果值是单元地址,各函数接到的是同一个地址,不是把单元内容复制成多个私有整数。因此该变换与共享状态相容,但不能跳过别名证明。

三个函数形成调用环,x的直接需求从g逆调用边传给f和h
例子与边界

沿调用链传入未直接读取的变量 ​

text
outer(x) =
  letrec f(n) = if n==0 then 0 else g(n-1)
         g(n) = if n==0 then x else h(n-1)
         h(n) = if n==0 then 1 else f(n-1)
  in h(3)

x 是唯一外层变量,D(f)=∅, D(g)={x}, D(h)=∅,调用边为 f→g、g→h、h→f。采用同步轮次,第一轮把 x 传给 f,第二轮传给 h,第三轮不再改变,得到三个 Need 集合都为 {x}。

text
f_lift(xf,n) = if n==0 then 0 else g_lift(xf,n-1)
g_lift(xg,n) = if n==0 then xg else h_lift(xg,n-1)
h_lift(xh,n) = if n==0 then 1 else f_lift(xh,n-1)
outer(x) = h_lift(x,3)

outer(7) 的执行是 h(7,3)→f(7,2)→g(7,1)→h(7,0)→1;outer(7) 若改成入口 h(2),则 h(7,2)→f(7,1)→g(7,0)→7。两个入口分别经过不读取 x 和确实读取 x 的基例,防止只测输出 1 就误以为可以把全部 x 参数删掉。

如果仅用 D(f) 而没有闭合 Need,f 被提升后没有 xf,生成 g_lift(?,n-1) 时就无值可传。错误不是“性能差一点”,而是输出程序含未解释的自由变量。对循环 f→g→h→f 只扫描一遍,也可能因遍历顺序而漏掉 h 的需求。

返回函数是当前准入边界 ​

make(x)=fun y -> x+y 返回的是携带 x 的函数值。把内层改成 add_lift(x,y) 后,make(4) 不能只返回 add_lift 的代码地址;未来调用者只给 y,代码地址无法区分 make(4) 与 make(9)。可以返回“代码、已供 x”的部分应用记录,但它已经重新承担闭包转换的环境责任。本页直接调用算法因此拒绝这个输入,而不是把缺失环境隐藏起来。

另设外层 x=ref(4),f、g 都需要这个引用。调用 f 写 x=7 后再调用 g 读取,必须得到 7。额外参数传同一位置保持共享;若每个入口以 ref(!x) 重建私有单元,就会破坏源行为。改变函数位置不改变引用身份。

推论与应用

正确性与成本分开核算 ​

闭合算法的不变量是 D(f) 始终包含在 Need(f) 中,Need 中每个变量都沿某条调用路径来自某个函数的直接需求。终点对每条 f→g 都有 Need(g)⊆Need(f),所以 f 的提升体能提供每次调用所需参数。这既证明输出闭合,也说明得到的是最小需求解:任何满足方程的候选必须包含每轮加入的项。

行为证明令每次提升调用的额外参数与源函数定义环境对应。基例读到相同值;普通实参仍按源顺序求值;递归步把相同额外值传给被调用者,从而保持这一关系。对有限调用推导归纳得到相同结果与存储效果。无穷调用需小步模拟;本变换没有减少或新增源级递归调用,但并不因此承诺空间成本相同。

设 f 个函数、v 个候选外层变量、e 条调用边,若集合为 v 位布尔向量且每轮扫描所有边,每轮还要复制 f 份集合并比较新旧解,因此为 O((f+e)v);至多 fv 次严格增长轮加一次稳定轮,保守上界为 O((fv+1)(f+e)v),空间 O(fv+e),此处 v≥1;v=0 时只须 O(f+e) 做一次空需求检查。这是本页朴素实现的界,不是优化 lambda lifting 的最优复杂度。按变化的变量对使用逆边工作表可改进传播,但输出的额外参数总数和调用处实参总数仍必须计费。

运行时一个调用多传 k 个值,需要 O(k) 参数搬移;闭包转换可能只递交一个环境指针、在体内按需读取。额外实参还可能超出寄存器容量进入栈,提升也可能延长某些值的活动区间。因此“没有环境记录”不推出总内存更小,更不推出总时间更快。[2]

迁移任务:增加 q(n)=f(n)+z,z 是第二个外层变量,且没有函数调用 q。答案为 Need(q)={x,z},原 f、g、h 仍只需 x。若再加 h→q,则整个强连通分量最终都需要 x、z。按同步轮次写出新增项即可验证传播方向,不能把“调用 q”误解成 q 要继承调用者全部变量。

该页内的函数组与两组输入构成独立短任务;前端返回闭包主例应继续采用旧闭包转换,不强行套本页受限提升。

参考资料

[1] Simon L. Peyton Jones、David R. Lester,Implementing Functional Languages: a Tutorial,Prentice Hall,1992,Chapter 6,尤其 §6.3 “Mark 1: A simple lambda lifter”、§6.5 “Mark 3: Johnsson-style lambda lifting”。本页严格求值、同一词法层函数组是另行限定的教学实例,并非照搬教材的非严格执行器。

[2] Olivier Danvy、Ulrik P. Schultz,Lambda-Lifting in Quadratic Time,BRICS RS-03-26,2003,§§1.1–1.2、§3 与 §5.2。其传递参数与闭包空间比较用于核对边界;本文未实现其图优化算法,未借论文标题宣称本检查器复杂度为二次。

关系图谱13 个相邻概念 · 3 类关系

拖动节点调整位置。

显示关系

显示:依赖

  1. 前置三跳
  2. 前置二跳
  3. 前置一跳
  4. 当前条目
  5. 后续一跳
  6. 后续二跳
  7. 后续三跳
文字版关系按与当前条目的最短距离分组
类型化关系