解释器:每次调用都把函数自己带进去
本节阅读量:前一章的普通函数在调用时只需要一个参数绑定:
|
|
递归函数还要多一个绑定。以 sum 为例,每次进入函数体时都必须同时看见:
|
|
第二行让函数处理这一次的参数,第一行让函数体里的 (sum ...) 能再次找到同一个函数。第五章解释器真正增加的,就是这条受限制的自绑定。
先运行贯穿示例
本节继续使用:
|
|
运行解释器:
|
|
结果是:
|
|
求值过程可以先压缩成:
|
|
递归调用不断建立新的调用环境;到达 n == 0 后,结果才沿调用链逐层返回。
sub1 仍然先求值子表达式
sub1 只是本章用来让参数逐步靠近 0 的辅助操作。它与加法一样,先递归求出子表达式,再检查结果是不是数字:
|
|
因此:
|
|
得到 42,而把函数值交给 sub1 会由解释器报告 sub1 expected a number。可以单独验证:
|
|
函数值只多记一个递归名字
第四章的 Function 保存参数名和函数体。第五章在此基础上增加 self_name:
|
|
三个字段的含义分别是:
| 字段 | 保存的内容 | 什么时候使用 |
|---|---|---|
param |
参数名,例如 n |
调用时绑定 argument |
body |
函数体 AST 的借用指针 | 调用时求值函数体 |
self_name |
递归名字,例如 sum |
调用递归函数时建立自绑定 |
普通 (lambda x body) 的 self_name 保持为空,所以普通函数的行为没有改变。letrec 创建的函数才会填写这个字段。
这里没有保存定义处的整个环境。self_name 只说明“调用时要把当前函数值绑定到哪个名字”,不能让函数读取任意外层变量。这正是第五章与后续闭包能力之间的边界。
body 仍然是不拥有 AST 的裸指针。AST 由 CLI 中解析得到的 std::unique_ptr<Expr> 拥有,并且在整次 eval() 期间一直存在,所以函数调用时这个指针仍然有效。
创建 letrec 函数时不执行函数体
解释器的 letrec 分支是:
|
|
按执行顺序看,它做了四件事:
|
|
第三步让下面最后一行里的 sum 可见:
|
|
此时 function-body 还没有执行。它和普通 lambda 的函数体一样,只有真正调用函数时才会求值。
注意,Function 没有保存一个指向自己的 shared_ptr。它只保存字符串 self_name,真正的 sum -> function_value 会在调用时临时建立。这样既能递归,也不会产生“函数值通过环境永久拥有自己”的引用环。
调用时建立全新的自绑定环境
函数调用仍然先求 callee,再求 argument。变化发生在 call_env 的建立过程:
|
|
若 callee 是普通 lambda,self_name 为空,call_env 仍然只有参数绑定。若 callee 来自 letrec,解释器先加入自绑定,再加入参数:
|
|
环境查找从后往前进行,因此参数是当前函数体中最内层的名字。函数体内部的 let 还可以继续压入更内层的绑定,求值完成后再弹出。
最重要的一行是:
|
|
它创建的是全新环境,而不是复制调用点的 env。随后加入的也只有递归函数自身和参数。于是第五章的名字边界可以准确写成:
| 位置 | 能看到的名字 |
|---|---|
letrec 的 body |
原来的外层绑定,加上递归名字 |
| 递归函数刚开始执行时 | 递归名字和本次参数 |
函数体内部的 let |
上一行的名字,再加内部绑定 |
| 递归函数体 | 不能直接看到 letrec 外面的其他名字 |
跟踪 sum(3) 的四份调用环境
把贯穿示例暂时想成 (sum 3)。第一次调用建立:
|
|
条件为假,求值 (sum (sub1 n))。callee 查到同一个函数值,argument 得到 2,随后建立另一份环境:
|
|
继续调用会得到:
|
|
这四份环境同时处在尚未完成的 C++ 递归求值调用中,但它们彼此独立。call_env D 从基例得到 0 后,结果按相反顺序返回:
| 返回到哪次调用 | 当前环境里的 n |
计算 | 返回值 |
|---|---|---|---|
sum(1) |
1 | 1 + 0 |
1 |
sum(2) |
2 | 2 + 1 |
3 |
sum(3) |
3 | 3 + 3 |
6 |
每一层都从自己的环境读取 n,所以不会被下一层的参数覆盖。
函数换了变量名,递归能力仍然存在
递归信息属于函数值,而不是调用点使用的变量名。下面的示例让 letrec 返回函数值,再把它绑定为 f:
|
|
运行:
|
|
仍然得到:
|
|
外层调用使用的名字是 f,但函数值中的 self_name 仍是 sum。进入函数体时,解释器因此建立 sum -> callee,递归调用不受函数值被转存或改名影响。
自绑定不是普通环境捕获
下面的程序越过了本章边界:
|
|
adddown 能看到自己和参数 n,却没有保存外层的 base。运行:
|
|
会得到:
|
|
这不是递归绑定失效,而是程序要求了另一种能力:保存函数定义位置的普通外层绑定。第五章只解决“函数怎样找到自己”,不会把 call_env 扩大为调用点环境,也不会让 Function 保存任意外层环境。
到这里,解释器侧的变化可以压缩为:
|
|