章节目录

语言:名字由定义位置决定

本节阅读量:

第七章没有增加语法,却改变了函数能够表达的程序。函数体现在可以读取 lambda 外层的绑定,并且在函数离开创建位置以后继续使用它们。

这项能力的语义核心是词法作用域:变量名的含义由源码中的定义位置决定,而不是由运行时从哪里调用函数决定。

创建位置和调用位置

先看本章最重要的例子:

1
2
3
4
(let x 40
  (let addx (lambda y (+ x y))
    (let x 100
      (addx 2))))

addx 创建在第一个 x 的作用域里,所以函数体中的 x 指向 40。调用 addx 时,周围虽然又有一个值为 100 的 x,结果仍是:

1
40 + 2 = 42

如果函数体改从调用位置临时寻找 x,结果会变成 102。那是动态作用域,不是本书语言采用的规则。

求值 lambda 不会执行函数体

遇到:

1
(lambda y (+ x y))

解释器或生成代码会创建一个闭包。此时函数体还没有执行,参数 y 也还没有值。闭包先保存两类信息:

1
2
以后要执行的函数体
函数体需要的外层绑定 x 的当前 Value

参数只在调用发生时加入,因此不属于提前捕获的内容。

调用顺序必须说清楚

调用表达式仍然只有一个 callee 和一个 argument。其求值顺序是:

  1. 先求值 callee;
  2. 确认 callee 是 closure;
  3. 再求值 argument;
  4. 从 closure 保存的定义环境开始;
  5. 若它是递归闭包,加入递归函数名;
  6. 加入本次参数绑定;
  7. 求值函数体。

可以把函数体使用的环境概括为:

1
2
3
4
捕获的自由变量
+ 可选的递归 self 绑定
+ 本次参数绑定
= 函数体环境

调用位置的其他绑定不会整份混进来。

捕获的是 Value

本章的普通变量还不能被修改。闭包保存的是创建时查到的 Value:

1
2
(let x 40
  ((lambda y (+ x y)) 2))

这里保存的是整数 Value 40。

若外层变量保存的是 record,闭包捕获的是同一个 record 引用:

1
2
(let box (record 40 2)
  ((lambda x (+ (get box 0) x)) 2))

结果是 42。闭包 payload 中只放一个 tagged record pointer,不会复制或展开整个 record。

反过来,record 也能保存 closure:

1
2
(let box (record (lambda x (+ x 1)))
  ((get box 0) 41))

结果同样是 42。这说明 Value 可以组成对象图,但不表示 record 和 closure 是同一种源语言值。

一个闭包可以创建另一个闭包

闭包可以作为函数结果返回:

1
2
3
4
5
(let x 40
  (((lambda y
      (lambda z (+ x (+ y z))))
    1)
   1))

结果是 42。求值过程分两次调用:

  1. 外层 closure 捕获 x = 40,收到参数 y = 1;
  2. 它创建并返回内层 closure,内层 closure 捕获 x = 40 和 y = 1;
  3. 内层 closure 收到 z = 1,计算 40 + 1 + 1。

外层调用结束以后,y 原来的栈帧已经不再执行,但内层 closure 仍然保存着对应 Value。这正是闭包必须使用堆对象的原因之一。

多个自由变量保持稳定顺序

下面的函数同时读取 x 和 y:

1
2
3
(let x 10
  (let y 20
    ((lambda z (+ x (+ y z))) 12)))

结果是 42。自由变量分析按第一次遇到的顺序得到 [x, y],创建闭包和函数体读取捕获槽都沿用这份顺序。顺序不会改变语言结果,却是编译器内部必须遵守的布局契约。

递归函数也可以捕获外层值

第五章的 letrec 只能依赖参数和自身。现在递归函数也能保存普通外层变量:

1
2
3
4
5
6
(let base 10
  (letrec adddown n
    (if (eq? n 0)
        base
        (+ 1 (adddown (sub1 n))))
    (adddown 5)))

结果是 15。相对于 adddown 的函数体:

1
2
3
n        参数,不捕获
adddown  递归 self 名字,不捕获
base     自由变量,需要捕获

adddown 不会被写进自己的 payload。每次调用时,实际 callee 已经由隐藏的 closure 参数传入,函数入口直接把这个 tagged Value 绑定给 self 名字。这样既能递归,也不会为了表示递归而制造一个对象自环。

self 与参数同名仍按词法规则处理

普通 identifier 可以发生遮蔽。这个程序是合法的:

1
2
3
(letrec f f
  f
  (f 42))

函数名和参数都写成 f。进入函数体后,参数绑定比 self 绑定更靠内,因此函数体中的 f 是参数,结果为 42,不会递归。

编译器不能只按源码字符串判断它们。lowering 会给 self 和参数分配不同的 IR 名字,再按词法环境查找最近的绑定。

closure 与 record 的种类边界

closure 和 record 都是 heap object,但 header kind 不同。下面的 predicate 是安全的:

1
(record? (lambda x x))

结果是 0。

下面两个程序则是运行时错误:

1
(size (lambda x x))
1
(get (lambda x x) 0)

解释器分别报告:

1
2
error: size expected a record value
error: get expected a record value

生成的程序会在读取 payload 前检查 header kind,失败时以状态 70 结束。

调用也检查对象种类

一个 record 不能被当作函数调用:

1
((record 42) 0)

解释器报告:

1
error: function call expected a function

编译后的 call 先检查通用 heap-object tag,再检查 HeapKind::closure。它不会把 record 的第一个字段误当成代码地址。

函数相等仍然没有定义

closure 成为堆对象,不表示语言自动获得函数相等语义。下面的程序仍然是错误:

1
(eq? (lambda x x) (lambda y y))

解释器报告:

1
error: eq? does not support function values

生成代码也会识别 closure operand,并以状态 70 结束。record 继续按对象身份参与 eq?;这两条规则不要混在一起。

语法总表没有新增分支

第七章继续使用第六章的表达式集合:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
expr := integer
      | identifier
      | (+ expr expr)
      | (sub1 expr)
      | (let identifier expr expr)
      | (letrec identifier identifier expr expr)
      | (eq? expr expr)
      | (if expr expr expr)
      | (lambda identifier expr)
      | (expr expr)
      | (record expr ...)
      | (record? expr)
      | (size expr)
      | (get expr non-negative-integer-literal)

普通 identifier 仍然只是一个或多个英文字母。闭包能力来自实现语义的扩展,不来自新增关键字。

本章边界

第七章支持词法作用域下的普通闭包、嵌套闭包和能捕获外层值的 letrec。它仍然不支持:

  • 修改变量;
  • 多参数函数;
  • closure 的源语言相等;
  • 逃逸分析和栈上分配 closure;
  • 主动释放对象或垃圾回收。

本章生成代码继续用 malloc 分配 closure。后续章节会改变可变变量和内存管理方式,但不会改变这里确定的词法作用域语义。


7.0 让函数带着环境离开

上一节

7.2 自由变量:只带走以后真正需要的名字

下一节