怎么写一个解释器(5)

2019-04-09 10:17

[`(lambda (,x) ,e) (Closure exp env)] 注意这里的 exp 就是 `(lambda (,x) ,e) 自己。我们只是把它包装了一下,把它与当前的环境一起放到一个数据结构(闭包)里,并不进行任何复杂的运算。这里我们的闭包用的是一个 Racket 的 struct 结构,也就是一个记录类型(record)。你也可以用其它形式来表示闭包,比如有些解释器教程提倡用函数来表示闭包。其实用什么形式都无所谓,只要能存储 exp和 env 的值。我比较喜欢使用 struct,因为它的界面简单清晰。

为什么需要保存当前的环境呢?因为当这个函数被作为一个值返回的时候,我们必须记住里面的外层函数的参数的绑定。比如,(lambda (y)

(lambda (x) (* y 2)))。当它被作用于 1 之后,我们会得到内层的函

数 (lambda (x) (* y 2))。当这个函数被经过一阵周折之后再被调用的时候,y 应该等于几呢?正确的做法应该是等于1。这种把外层参数的值记录在内层函数的闭包里的做法,叫做“lexical scoping”或者“static scoping”。

如果你不做闭包,而是把函数体直接返回,那么在 (lambda (x) (* y

2)) 被调用的位置,你可能会另外找到一个 y,从而使用它的值。在调

用的时候“动态”解析变量的做法,叫做“dynamic scoping”。事实证明 dynamic scoping 的做法是严重错误的,它导致了早期语言里面出现的各种很难发现的bug。很多早期的语言是 dynamic scoping,就是因为

它们只保存了函数的代码,而没有保存它定义处的环境。这样要简单一些,但是带来太多的麻烦。早期的 Lisp,现在的 Emacs Lisp 和 TeX 就是使用 dynamic scoping 的语言。

为了演示 lexical scoping 和 dynamic scoping 的区别。你可以在我们的解释器里执行以下代码:

(interp '((lambda (y) (((lambda (y) (lambda (x) (* y 2))) 3) 0)) 4)) 其中红色的部分就是上面提到的例子。在这里,(* y 2) 里的 y,其实是最里面的那个 (lambda (y) ...) 里的。当红色部分被作用于 3 之后。 (lambda (x) (* y 2)) 被作为一个值返回。然后它被作用于 0(x 被绑定到 0,被忽略),所以(* y 2) 应该等于 6。但是如果我们的解释器是 dynamic scoping,那么最后的结果就会等于 8。这是因为最外层的 y 开头被绑定到了 4,而 dynamic scoping 没有记住内层的 y 的值,所以使用了外层那个 y 的值。

为什么 Lexical scoping 更好呢?你可以从很简单的直觉来理解。当你构造一个“内部函数”的时候,如果它引用了外面的变量,比如这个例子里的 y,那么从外层的 y 到这个函数的内部,出现了一条“信道”(channel)。你可以把这个内部函数想象成一个电路元件,它的内部有一个节点 y 连接到一根从外部来的电线 y。当这个元件被返回,就像这个元件被挖出来送到别的地方去用。但是在它被使用的地方(调用),这个 y 节点应该从哪里得到输入呢?显然你不应该使用调用处的某

个 y,因为这个 y 和之前的那个 y,虽然都叫 y,却不是“同一个 y”,也就是同名异义。它们甚至可以代表不同的类型的东西。所以这个 y 应该仍然连接原来的那根 y 电线。当这个内部元件移动的时候,就像这跟电线被无限的延长,但是它始终连接到原来的节点。

对函数调用的解释

好,我们终于到了最后的关头,函数调用。函数调用都是 (e1 e2) 这样的形式,所以我们需要先分别求出 e1 和 e2 的值。这跟基本运算的时候需要先求出两个操作数的值相似。

函数调用就像把一个电器的插头插进插座,使它开始运转。比如,当 (lambda (x) (* x 2)) 被作用于 1 时,我们把 x 绑定到 1,然后解释它的函数体 (* x 2)。但是这里有一个问题,如果函数体内有未绑定的变量,它应该取什么值呢?从上面闭包的讨论,你已经知道了,其实操作数 e1 被求值之后应该是一个闭包,所以它的里面应该有未绑定变量的值。所以,我们就把这个闭包中保存的环境(env1)取出来,扩展它,把 x 绑定到 v2,然后用这个扩展后的环境来解释函数体。 所以函数调用的代码如下:

[`(,e1 ,e2) (let ([v1 (interp1 e1 env)] [v2 (interp1 e2 env)]) (match v1 [(Closure `(lambda (,x) ,e) env1) ; 用模式匹配的方式取出闭包里的各个子结构 (interp1 e (ext-env x v2 env1))] ; 在闭包的环境中把 x 绑定到 v2,解释函数体 ))] 你可能会奇怪,那么解释器的环境 env 难道这里就不用了吗?是的。我们通过 env 来计算 e1 和 e2 的值,是因为 e1 和 e2里面的变量存在于“当前环境”。我们把 e1 里面的环境 env1 取出来用于计算函数体,是因为函数体并不是在当前环境定义的,它的代码在别的地方。如果我们用 env 来解释函数体,那就成了 dynamic scoping。

实验:你可以把 (interp1 e (ext-env x v2 env1)) 里面的 env1 改成 env,再试试我们之前讨论过的代码,它的输出就会是 8:

(interp '((lambda (y) (((lambda (y) (lambda (x) (* y 2))) 3) 0)) 4)) 另外在这里我们也看到环境用“函数式数据结构”表示的好处。闭包被调用时它的环境被扩展,但是这并不会影响原来的那个环境,我们得到的是一个新的环境。所以当函数调用返回之后,函数的参数绑定就自动“注销”了。如果你用一个非函数式的数据结构,在绑定参数时不生成新的环境,而是对已有环境进行赋值,那么这个赋值操作就会永久性的改变原来环境的内容。所以你在函数返回之后必须删除参数的绑定。这样不但麻烦,而且在复杂的情况下几乎不可能有效的控制。每一次当我使用赋值操作来修改环境,最后都会出现意想不到的麻烦。所以在写解释器,编译器的时候,我都只使用函数式数据结构来表示环境。

下一步

在懂得了这里讲述的基本的解释器构造之后,下一步可以做什么呢?其实从这个基本的解释器原型,你可以进一步发展出很多内容,比如:

在这个解释器里加一些构造,比如递归和状态,你就可以得到一个完整的程序语言的解释器,比如 Scheme 或者 Python。

?

?

对这个解释器进行“抽象”,你就可以对程序进行类型推导。感兴趣的话可以参考我实现的这个 Hindley-Milner系统,或者 Python 类型推导。

?

对这个解释器进行一些改变,就可以得到一个非常强大的 online partial evaluator,可以用于编译器优化。

另外需要指出的是,学会这个解释器并不等于理解了程序语言的理论。所以在学会了这些之后,还是要看一些语义学的书。


怎么写一个解释器(5).doc 将本文的Word文档下载到电脑 下载失败或者文档不完整,请联系客服人员解决!

下一篇:四年级下册鄂教版品德与社会全册教案

相关阅读
本类排行
× 注册会员免费下载(下载后可以自由复制和排版)

马上注册会员

注:下载文档有可能“只有目录或者内容不全”等情况,请下载之前注意辨别,如果您已付费且无法下载或内容有问题,请联系我们协助你处理。
微信: QQ: