怎么写一个解释器(4)

2019-04-09 10:17

你可以看到它几乎跟刚才写的计算器一模一样,不过现在 interp1 的调用多了一个参数 env 而已。这个 env 是什么,我们下面很快就讲。

变量和函数

我想用两个小节来简单介绍一下变量,函数和环境。稍后的几节我们再来看它们是如何实现的。

变量(variable)的产生是数学史上的最大突破之一。因为变量可以被绑定到不同的值,从而使得函数的实现成为可能。比如数学函数 f(x) = x*2,其中 x 是一个变量,它把输入的值传递到函数的主体 x*2里面。如果没有变量,函数就不可能实现。

对变量的最基本的操作是对它的“绑定(”binding)和“取值(”evaluate)。什么是绑定呢?拿上面的函数 f(x) 作为例子吧。当 x 等于 1 的时候,

f(x) 的值是 2,而当 x 等于 2 的时候,f(x) 的值是 4。在上面的句子

里,我们对 x 进行了两次绑定。第一次 x 被绑定到了 1,第二次被绑定到了 2。你可以把“绑定”理解成这样一个动作,就像当你把插头插进电源插座的那一瞬间。插头的插脚就是 f(x) 里面的那个 x,而 x*2 里面的 x,则是电线的另外一端。所以当你把插头插进插座,电流就通过这根电线到达另外一端。如果电线导电性能良好,两头的电压应该几乎相等。有点跑题了…… 反正只要记住一点:绑定就是插进插座的那个“动作”。

那么“取值”呢?再想一下前面的例子,当我们用伏特表测电线另外一端的电压的时候,我们就是在对这个变量进行取值。有时候这种取值的过程不是那么明显,比如电流如果驱动了风扇的电动机。虽然电线的另外一头没有显示电压,其实电流已经作用于电动机的输入端子,进入线圈。所以你也可以说其实是电动机在对变量进行取值。

环境

我们的解释器是一个挺笨的程序,它只能一步一步的做事情。比如,当它需要求 f(1) 的值的时候,它做以下两步操作:1) 把 x 绑定到 1; 2) 进入 f 的函数体对 x*2 进行求值。这就像一个人做出这两个动作:1)把插头插进插座,2) 走到电线的另外一头测量它的电压,并且把结果乘以 2。在第一步和第二步之间,我们如何记住 x 的值呢?它必须被传递到那个用来处理函数体的递归解释器里面。这就是为什么我们需要“环境”,也就是 interp1 的第二个参数 env。

环境记录变量的值,并且把它们传递到它们的“可见区域”,用术语说就叫做“作用域”(scope)。通常作用域是整个函数体,但是有一个例外,就是当函数体内有嵌套的函数定义的时候,内部的那个函数如果有同样的参数名,那么外层的参数名就会被“屏蔽”(shadow)掉。这样内部的函数体就看不到外层的参数了,只看到它自己的。比如 (lambda (x)

(lambda (x) (* x 2))),里面的那个 x 看到的就是内层函数的 x,而不

是外层的。

在我们的解释器里,用于处理环境的主要部件如下:

;; 空环境 (define env0 '()) ;; 对环境 env 进行扩展,把 x 映射到 v (define ext-env (lambda (x v env) (cons `(,x . ,v) env))) ;; 取值。在环境中 env 中查找 x 的值 (define lookup (lambda (x env) (let ([p (assq x env)]) (cond [(not p) x] [else (cdr p)])))) 这里我们用的是 Scheme 的 association list 来表示环境。Association list 看起来像这个样子:((x . 1) (y . 2) (z . 5))。也就是一个两元组(pair)的链表,左边的元素是 key,右边的元素是 value。写的直观一点就是:

((x . 1) (y . 2) (z . 5)) 查表操作就是从头到尾搜索,如果左边的 key 是要找的变量,就返回整个 pair。简单吧?

ext-env 扩展一个环境。比如,如果原来的环境是 ((y . 2) (z . 5)) 那

么 (ext-env x 1 ((y . 2) (z . 5))),就会得到 ((x . 1) (y . 2) (z .

5))。也就是把 (x . 1) 放到最前面去。值得注意的一点是,环境被扩

展以后其实是形成了一个新的环境,原来的环境并没有被“改变”。比如上面红色的部分就是原来的数据结构,只不过它被放到另一个更大的结构里面了。这叫做“函数式数据结构”。这个性质在我们的解释器里是至关重要的,因为当我们扩展了一个环境之后,其它部分的代码仍然可以原封不动的访问扩展前的那个旧的环境。当我们讲到调用的时候也许你就会发现这个性质的用处。

你也可以用另外的,更高效的数据结构(比如 splay tree)来表示环境。你甚至可以用函数来表示环境。唯一的要求就是,它是变量到值的“映射”(map)。你把 x 映射到 1,待会儿查询 x 的值,它应该仍然是 1,而不会消失掉或者别的值。也就是说,这几个函数要满足这样的一种“界面约定”:如果 e 是 (ext-env 'x 1 env) 返回的环境,那么 (lookup 'x

e) 应该返回 1。只要满足这样的界面约定的函数都可以被叫

做 ext-env 和 lookup,以至于可以它们用来完全替代这里的函数而不会导致其它代码的修改。这叫做“抽象”,也就是“面向对象语言”的精髓所在。

对变量的解释

了解了变量,函数和环境,让我们来看看解释器对变量的操作,也就是 interp1 的 match 的第一种情况。它非常简单,就是在环境中查找变量的值。这里的 (? symbol? x) 是一个特殊的模式,它使用Scheme 函

数 symbol? 来判断输入是否匹配,如果是的就把它绑定到 x,查找它的值,然后返回这个值。

[(? symbol? x) (lookup x env)] 注意由于我们的解释器是递归的,所以这个值也许会被返回到更高层的表达式,比如 (* x 2)。

对数字的解释

对数字的解释也很简单。由于在 Scheme 里面名字 '2 就是数字 2(我认为这是 Scheme 设计上的一个小错误),所以我们不需要对数字的名字做特殊的处理,把它们原封不动的返回。

[(? number? x) x] 对函数的解释

对函数的解释是一个比较难说清楚的问题。由于函数体内也许会含有外层函数的参数,比如 (lambda (y) (lambda (x) (* y 2))) 里面的 y 是外层函数的参数,却出现在内层函数定义中。如果内层函数被作为值返回,那么 (* y 2) 就会跑到y 的作用域以外。所以我们必须把函数做成“闭包”(closure)。闭包是一种特殊的数据结构,它由两个元素组成:函数的定义和当前的环境。所以我们对 (lambda (x) e) 这样一个函数的解释就是这样:


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

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

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

马上注册会员

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