语言:修改绑定,但不改变词法作用域
本节阅读量:可变状态最容易让人误以为“变量名现在可以随意指向任何东西”。本章采用的规则更具体:词法作用域仍然决定名字对应哪个绑定,set! 只替换这个绑定的位置里保存的 Value。
因此,第七章确定的“名字由定义位置决定”没有被推翻。本章改变的是绑定的内容能否更新。
set! 修改已经存在的绑定
语法是:
|
|
它按下面的顺序求值:
- 按词法作用域找到
name对应的 location; - 求值右侧
expr,得到一个 Value; - 把这个 Value 写入刚才找到的 location;
- 返回这个 Value,作为整个
set!的结果。
set! 不会创建新名字。下面的程序没有任何绑定可以提供 missing:
|
|
解释器会报告:
|
|
对应示例可以直接运行:
|
|
右侧先算完,写入随后发生
一个很常见的更新写法是:
|
|
右侧的 x 读取更新前的 Value。假设 location 中原来是 40,过程是:
|
|
不会先清空 location,也不会在计算右侧的过程中创建另一个 x。
set! 本身也是有结果的表达式
本书的源语言以表达式为主,所以 set! 不是只能单独出现的语句。运行:
|
|
源码是:
|
|
set! 写入并返回 40,外层加法得到 42。
这条规则让解释器和 IR 都有清楚的返回值,不需要额外发明 unit、void 或未指定结果。
begin 明确两个表达式的先后顺序
语法是:
|
|
求值规则只有两步:
- 求值
first,丢弃它的结果; - 求值并返回
second。
例如:
|
|
第二个表达式一定在写入以后读取 x,所以结果是 42。
运行一个更能体现顺序的例子:
|
|
源码连续更新同一个 location:
|
|
输出是 42。若两个更新顺序颠倒,结果就会不同。
本章的 begin 固定为二元
下面是合法写法:
|
|
三个表达式要通过嵌套表示:
|
|
本章不加入可变长度 begin。二元结构已经足以表达任意有限序列,同时让 AST、parser 和 lowering 保持与其他二元节点相近的形状。
空 begin 和只有一个子表达式的 begin 都不是本章语法。
状态让已有的求值顺序变得可观察
前几章已经按源码顺序求值子表达式,只是纯整数程序往往看不出顺序差异。现在某个子表达式可以修改 location,因此这些既有规则成为语言行为的一部分:
let先在旧环境中求右侧,再分配并加入新 location;+先求左 operand,再求右 operand;- record 字段从左到右求值;
- call 先求并检查 callee,再求 argument;
if仍然只求被选中的分支。
例如:
|
|
第一个字段先写入并返回 40,第二个字段随后读取同一个 location,所以结果是:
|
|
解释器、IR lowering 和汇编生成必须保持同一顺序。状态没有改变词法结构,却让错误地交换两个步骤真正能够改变程序结果。
遮蔽仍然选择最近的词法绑定
两个同名 let 会建立两个不同 location:
|
|
内层 set! 找到最近的 x,只把内层 location 从 0 改为 2。离开内层 let 后,外层 location 仍保存 40,最终结果是 42。
运行:
|
|
这说明 mutation 没有把词法作用域变成动态作用域,也没有把所有同名变量合并成一个全局槽。
闭包共享的是 location
下面两个闭包都使用外层 x:
|
|
put 只写 x,read 只读 x。它们捕获的是同一个 location,所以 put 的修改会被 read 看见。
|
|
输出:
|
|
如果创建 closure 时复制当前 Value,这两个函数会各自持有 40 的副本,程序就无法得到规定结果。
只写不读也必须捕获
变量只出现在 set! 的目标位置时,仍然使用了那个绑定:
|
|
函数体没有普通的 Var(x),但 put 必须找到外层 x 的 location。运行:
|
|
结果是 42。下一节会看到自由变量分析为何必须单独处理 SetExpr::name。
共享不等于所有调用共用一个 cell
闭包需要共享同一个词法绑定;不同的绑定仍必须彼此独立。运行:
|
|
程序创建两个 counter:
|
|
两次调用 make 必须为参数 start 分配两个新 location:
|
|
若错误地让所有调用复用一个参数 cell,两个 counter 会互相干扰。共享的边界由词法绑定决定,不是由源码名字 start 决定。
参数修改只影响本次调用建立的位置
参数也是普通可变绑定:
|
|
结果是 42。每次调用都会为参数建立新的 location;函数返回后,这次参数位置仍可能被函数体创建的内层 closure 保留,但不会自动变成其他调用的参数位置。
对应示例:
|
|
set! 可以写入任意已有 Value
location 保存的是统一 Value,不只保存整数。下面先把 f 绑定到整数,再写入 closure:
|
|
运行:
|
|
结果是 42。源语言仍然是动态类型;本章不会给变量加静态类型标注,也不会规定一个 location 一生只能保存某一种 Value。
letrec 的名字同样可以修改
letrec 建立的函数名也是词法绑定,因此也服从 set!。这个例子特意保留旧 closure 的别名:
|
|
求值过程是:
old保存原来的递归 closure Value;set!把loop的共享 location 改为一个总是返回42的新 closure;- 调用
old 1进入原来的函数体; - 函数体递归读取名字
loop时,从共享 location 得到新 closure; - 新 closure 返回
42。
|
|
输出是 42。这条语义要求第八章的递归 closure 保存 self location,而不能只在每次调用时临时绑定当前 callee。
self 与参数同名时,参数仍然遮蔽 self
已有的词法规则继续成立:
|
|
调用时,参数 location 比 self location 更靠内,函数体中的 f 读取参数,结果仍为 42。可变状态不会改变遮蔽优先级。
if 仍然只执行选中的分支
副作用让条件表达式原有的短路规则更容易观察:
|
|
只有 then 分支执行,所以结果是 42。编译器虽然会为两个分支都生成代码,运行时却只能沿一条控制流路径修改 cell。
set! 不是 record 字段更新
SetExpr 的目标只能是 identifier。下面不是合法语法:
|
|
第六章的 record 继续不可变;本章没有 set-field!。record 可以保存 closure,cell 中也可以保存 record,但这不表示 record payload 获得了原地写入语义。
新增后的语法总表
第八章的表达式集合是:
|
|
普通 identifier 仍然只是一个或多个英文字母。set! 和 eq? 带有特殊字符,只在列表头部作为固定特殊形式识别。
本章的语义边界
本章支持词法变量的修改、顺序执行以及闭包之间共享状态。它不加入:
- record 字段更新;
- 多表达式或空
begin; - 全局变量和动态作用域;
- 静态类型检查;
- 线程、并发或原子操作;
- 主动回收 cell、closure 和 record。
现在语言已经能够形成包含环的对象图。怎样回收不可达对象,是下一章垃圾回收要解决的问题。