Coursera Programming Languages, Part B 华盛顿大学 Homework 5
这次 Week 2 的作业比较难,任务目标是使用 \(racket\) 给一个虚拟语言 \(MUPL\) (made-up programming language) 写一个解释器
所以单独开个贴来好好分析一下
首先是 MUPL 语言的几个 semantic,已经通过 \(racket\) struct 的形式提供
(struct var (string) #:transparent) ;; a variable, e.g., (var "foo")
(struct int (num) #:transparent) ;; a constant number, e.g., (int 17)
(struct add (e1 e2) #:transparent) ;; add two expressions
(struct ifgreater (e1 e2 e3 e4) #:transparent) ;; if e1 > e2 then e3 else e4
(struct fun (nameopt formal body) #:transparent) ;; a recursive(?) 1-argument function
(struct call (funexp actual) #:transparent) ;; function call
(struct mlet (var e body) #:transparent) ;; a local binding (let var = e in body)
(struct apair (e1 e2) #:transparent) ;; make a new pair
(struct fst (e) #:transparent) ;; get first part of a pair
(struct snd (e) #:transparent) ;; get second part of a pair
(struct aunit () #:transparent) ;; unit value -- good for ending a list
(struct isaunit (e) #:transparent) ;; evaluate to 1 if e is unit else 0
;; a closure is not in "source" programs but /is/ a MUPL value; it is what functions evaluate to
(struct closure (env fun) #:transparent)
我们用 eval-exp
函数对上面的 semantic 进行解释
(define (eval-exp e)
(eval-under-env e null))
而 eval-under-env
函数则是在提供的 \(env\) 环境下对表达 \(e\) 进行解释,也是我们需要实现的主要函数
这里,环境 我们用 \(racket\) pair 组成的 list 表示,每个 (string, val)
组成的 pair 中,string
为变量名,而 val
为变量值
另外作业中还提供了 envlookup
函数,提供变量名 string
,返回环境中对应的变量值
(define (envlookup env str)
(cond [(null? env) (error "unbound variable during evaluation" str)]
[(equal? (car (car env)) str) (cdr (car env))]
[#t (envlookup (cdr env) str)]))
在开始分析之前,需要强调一下 \(MUPL\) 语言是一门函数式语言,一个表达 (expression) 是由若干个表达 (subexpression) 嵌套而成
在所有提供的 semantic 中,可以产生 binding 的只有 mlet
(let 表达定义的本地变量 binding),call
(call 中传入的值 call-actual
与对应函数的参数 fun-formal
进行 binding) 以及 fun
(函数名 fun-nameopt
与函数)
接下来我们一个一个分析对应的 case:
- \(var\)
var
中存储的是一个变量的名字,对其解释的结果应该是这个变量的值,所以
[(var? e) (envlookup env (var-string e))]
- \(int\), \(aunit\) 与 \(closure\)
对于 int
,其储存的是一个 int 类型的数字,可以算作是所谓最基本的表达,无需再进行解释
aunit
与 closure
类似。如果说 int 是 int 类型变量的值,那么 closure 可以说是 function 的 值。所以
[(int? e) e]
[(aunit? e) e]
[(closure? e) e]
- \(fun\) 与 \(call\)
这里真的想了很久,关于 \(fun\) 与 \(call\) 两个表达的解释方式
由于受到线性语言的影响,自然而然会有这样的想法:在函数被定义的时候将其与对应的 closure 加入环境 (形成一个 fun-closure binding),在被调用的时候像变量一样进行解释
这样的想法是可行的,但在 \(MUPL\) 语言中,由于只提供了有限的 semantic,我们可以发现类似 函数定义形成 binding (如 ML 的 fun
, Racket 的 define
) 是没有对应的 semantic 的,唯一能使函数形成 binding 的地方 在 \(call\) 语句中 (在解释函数体时将 fun-closure binding 加入环境以实现函数的递归调用)
本质的来讲,就是 \(MUPL\) 语言中,函数的 第一次被定义与第一次(层)被调用是同步进行的。所以,只有第二层及以上的调用可以使用函数的名字
而在线性语言中,函数的定义和调用之间可以间隔很远,在定义时解释器就将 fun-closure binding 加入环境,之后的调用都可以直接采用名字来调用
[(fun? e) (closure env e)] ; closure is function's value
[(call? e)
(let ([cl (eval-under-env (call-funexp e) env)]
[arg (eval-under-env (call-actual e) env)])
(if (closure? cl)
(let* ([fn (closure-fun cl)]
[bodyenv (cons (cons (fun-formal fn) arg) (closure-env cl))] ; 将 参数名-参数值对 传入解释函数体的环境中
[bodyenv (if (fun-nameopt fn) (cons (cons (fun-nameopt fn) cl) bodyenv) bodyenv]) ; 若函数具名,则将 函数-闭包对 传入解释函数体的环境,以保证递归可实现
(eval-under-env (fun-body fn) bodyenv)) ; 解释函数体的环境 = 定义函数的环境 (存储在闭包中) + 参数相关绑定 + 函数-闭包绑定
(error "MUPL funciton call with nonfunction")))]
- \(add\) 与 \(ifgreater\)
这个就比较简单了,比较标准的树形结构,先 interpret subexpressions 再最终 interpret expression
贴一个 add
,ifgreater
差不多
[(add? e)
(let ([v1 (eval-under-env (add-e1 e) env)]
[v2 (eval-under-env (add-e2 e) env)])
(if (and (int? v1)
(int? v2))
(int (+ (int-num v1)
(int-num v2)))
(error "MUPL addition applied to non-number")))]
- \(apair\),\(fsd\) 与 \(snd\)
三个 \(pair\) 相关的 semantic,同样遵循 subexpression-expression 的规则
[(apair? e)
(apair (eval-under-env (apair-e1 e) env) (eval-under-env (apair-e2 e) env))]
[(fst? e)
(let ([pr (eval-under-env (fst-e e) env)])
(if (apair? pr)
(apair-e1 pr)
(error "MUPL fst applied to non-apair")))]
[(snd? e)
(let ([pr (eval-under-env (snd-e e) env)])
(if (apair? pr)
(apair-e1 pr)
(error "MUPL snd applied to non-apair")))]
- \(isaunit\)
aunit
这个 semantic 很特殊,它没有 field,因此也储存不了任何其他值
它的存在和 ML 中的 NONE 很相似
[(isaunit? e)
(let ([v (eval-under-env (isaunit-e e) env)])
(if (aunit? v) (int 1) (int 0)))]
- \(mlet\)
call
与 mlet
是唯二可以向环境中添加新 binding 的 semantic
所以只要理解了 \(call\),\(mlet\) 的实现也可以说是很简单
[(mlet? e)
(let ([bodyenv (cons (cons (mlet-var e) (eval-under-env (mlet-e e) env)) env)])
(eval-under-env (mlet-body e) bodyenv))]
至此,我们已经成功编写了 \(MUPL\) 语言的解释器 eval-exp
接下来,我们要用 racket 继续 扩展 这门语言
扩展一门语言的方法就是:用元语言编写函数的方式来定义宏 (Defining "Macros" via functions in metalanguage)
记得一定要分清被实现语言 (language being implemented) 与元语言 (metalanguage) 的区别
也就是说,在用编写函数的方式来定义被实现语言的宏时,不能用到元语言本身的 semantic (包括 eval-exp
)
(ifaunit e1 e2 e3)
若 \(e1\) evaluate 为 aunit,则返回 \(e2\),否则返回 \(e3\)
; 错误例子1
(define (ifaunit e1 e2 e3)
(if (aunit? e1) e2 e3)) ; 这样定义的 macro 是错误的,因为用到了元语言的 semantic "if"
; 正确写法
(define (ifaunit e1 e2 e3)
(ifgreater (isaunit e1) (int 0) e2 e3)) ; 这里用到的所有 semantic (ifgreater, isaunit) 都是 MUPL 语言中的
(mlet* bs e2)
初始环境为空,按顺序 evaluate bs 中的每一个 (var-val) 对并添加入环境,最后用该环境 evaluate e2
(define (mlet* bs e2)
(ifaunit bs
e2
(mlet (fst (fst bs)) (snd (fst bs)) (mlet* (snd bs)))))
(ifeq e1 e2 e3 e4)
若 \(e1\) 与 \(e2\) evaluate 出的值一致,则返回 \(e3\),否则返回 \(e4\)
且保证 \(e1\) 与 \(e2\) 只被 evaluate 一次
(define (ifeq e1 e2 e3 e4)
(mlet "_x" e1
(mlet "_y" e2 ; 定义临时变量,保证 e1 与 e2 都只被 evaluate 一次
(ifgreater (var "_x") (var "_y") e4
(ifgreater (var "_y") (var "_x") e4 e3)))))
mupl-map
将名为 mupl-map
的 \(Racket\) 函数与一个 \(MUPL\) 函数绑定
这个函数 takes 一个函数 \(a\),返回一个函数 \(b\)
函数 \(b\) takes 一个 list,并且将 \(a\) 对 list 的每个元素应用,最后返回新 list (其实就是 map)
(define mupl-map
(fun #f "a"
(fun "b" "ls"
(ifaunit (var "ls")
(aunit)
(apair (call (var "a") (fst (var "ls")))
(call (var "b") (snd (var "ls"))))))))
注意,在调用函数时,第一个参数 funexp
是整个函数本身,所以我们用 (var "fun_name")
进行传入
这样 evaluate 出来的结果也是对应的 closure,符合 evaluate function 后得到的结果
mupl-mapAddN
同上,函数 takes 一个整数 \(i\) 返回一个函数 \(f\)
这个函数 \(f\) takes 一个 list,并将 list 中的每个元素都加上 \(i\)
(define mupl-map
(fun #f "i"
(call mupl-map (fun #f "x" (add (var "x") (var "i"))))))