看到王垠的这篇博文《怎样写一个解释器》,里面简单介绍了lambda calculus语言及解释器,即简单又抓住了解释器的实质。
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;; 以下三个定义 env0, ext-env, lookup 是对环境(environment)的基本操作: ;; 空环境 (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)])))) ;; 闭包的数据结构定义,包含一个函数定义 f 和它定义时所在的环境 (struct Closure (f env)) ;; 解释器的递归定义(接受两个参数,表达式 exp 和环境 env) ;; 共 5 种情况(变量,函数,调用,数字,算术表达式) (define interp1 (lambda (exp env) (match exp ; 模式匹配 exp 的以下情况(分支) [(? symbol? x) (lookup x env)] ; 变量 [(? number? x) x] ; 数字 [`(lambda (,x) ,e) ; 函数 (Closure exp env)] [`(,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))]))] [`(,op ,e1 ,e2) ; 算术表达式 (let ([v1 (interp1 e1 env)] [v2 (interp1 e2 env)]) (match op ['+ (+ v1 v2)] ['- (- v1 v2)] ['* (* v1 v2)] ['/ (/ v1 v2)]))]))) ;; 解释器的“用户界面”函数。它把 interp1 包装起来,掩盖第二个参数,初始值为 env0 (define interp (lambda (exp) (interp1 exp env0))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
这个短小的程序完成了任何解释器的核心功能,实现了语言的三大基本元素:变量、函数和调用。
解释器接收的输入是“程序”,输出的是对程序的解释执行结果。实际上各种程序语言在经过语法分析之后,得到的是“抽象语法树”。
如果我们直接绕过语法分析,或者简化语法,直指抽象语法树,那么程序处理将变得非常容易。就像在自然语音领域,我们的最终目的不是语言本身,而是语义,它代表我们真正要传达的信息。现在演讲时经常使用幻灯片,通过图片,声音,逻辑图等等方式来传达信息,就是因为自然语言的语法及其表达能力有限,需要改进。改进后的方式,可能一幅设计良好的示意图,要比洋洋洒洒好几千字的文章还要清晰明了。而scheme的S表达式其实是一种语法树,当初设计时是想让它作为中间语言,最后觉得用起来非常方便而且直接,就作为scheme的表达式,由于它接近语法树,所以自身的语法规则就非常简单,因为需要被处理的步骤少。
首先定义了三个关于环境的过程,env0为空环境,ext-env扩展环境,lookup查找环境中的变量值。环境是用来保存变量的值的地方。变量呢,在这里就是函数的输入,它的值是可变的,被绑定不同数字就表示不同的值,而变量—值作为一对(pair)就保存在该函数对应的环境中。
还定义了闭包的概念,利用struct Closure保存函数表达式及其环境,这对于函数嵌套定义时有相同变量名的处理很有用,解决了名称决议(name resolution)的问题。避免了dynamic scope带来的变量名引用困扰。
(interpl '((lambda (x) (* 2 x)) 3))
这个代码是调用刚才定义的解释器,输入的表达式为'((lambda (x) (* 2 x)) 3)。根据解释器代码分析模式匹配一个调用(,e1 ,e2),然后let语句表示递归调用自身求值e1,e2并分别绑定到v1,v2。e1为(lambda (x) (* 2 x)),解释器通过闭包将这个lambda表达式与它的环境env一起记录下来,然后match v1处理这个闭包,将lambda的参数x及其值v2(等于数字3)扩展到环境中。扩展后的环境将作为参数,连同lambda函数体(* 2 3)一起传递给解释器求值。这里x变量,其值就保存在刚才传递给解释器的环境中(通过lookup接口查询env)。最后调用计算机器的match pattern得到6。如果遇到lambda嵌套定义函数时,尤其是参数名还相同的情况下,为使程序行为可预测,闭包和环境的概念就起了很大作用了。
(interp '(((lambda (x) (lambda (y) (* x y))) 2) 3)) ;; => 6 (interp '((lambda (x) (* 2 x)) 3)) ;; => 6 (interp '((lambda (y) (((lambda (y) (lambda (x) (* y 2))) 3) 0)) 4))