Coursera Programming Languages, Part B 华盛顿大学 Week 2
Datatype-programming in Racket without structs
在 ML 语言中,我们使用 datatype binding
来实现对 标签联合类型的构建:传送门
这是因为 ML 语言中的 list
中的元素类型是统一的
这里是一份经典的 ML 实现标签联合类型 exp
以及提取 exp
类型实际值的函数 eval
的代码
datatype exp = Constant of int
| Negate of exp
| Add of exp*exp
| Multiply of exp*exp
fun eval x =
case x of
Constant e => e
| Negate e => ~ (eval e)
| Add (e1, e2) => eval e1 + eval e2
| Multiply (e1, e2) => (eval e1) * (eval e2)
而得益于 Racket 的 dynamic typing 特性,我们可以直接通过 list
实现标签联合类型的构建
先介绍 Racket 中的一个 feature:Symbols
创建一个 symbol 的方法是 '+symbol名
,用 eq?
来比较两个 symbol 是否相同
定义 constructor:
(define (Const i) (list 'Const i))
(define (Negate e) (list 'Negate e))
(define (Add e1 e2) (list 'Add e1 e2))
(define (Multiply e1 e2) (list 'Multiply e1 e2))
定义类型分析函数:
(define (Const? x) (eq? (car x) 'Const))
(define (Negate? x) (eq? (car x) 'Negate))
(define (Add? x) (eq? (car x) 'Add))
(define (Multiply? x) (eq? (car x) 'Multiply))
定义提取值函数:
(define (Const-int e) (car (cdr e)))
... ; 剩余的相似
定义 eval
函数:注意,这里的 eval
函数返回的是一个 Const
(define (eval-exp e)
(cond [(Const? e) e]
[(Negate? e) (Const (- Const-int (eval-exp (Negate-e e)))))]
[(Add? e) (let ([v1 (Const-int (eval-exp (Add-e1 e)))]
[v2 (Const-int (eval-exp (Add-e2 e)))])
(Const (+ v1 v2)))]
[(Multiply? e) (let ([v1 (Const-int (eval-exp (Multiply-e1 e)))]
[v2 (Const-int (eval-exp (Multiply-e2 e)))])
(Const (* v1 v2)))]
[#t (error "eval-exp expect an exp")]))
Datatype-programming in Racket with structs
在 racket 中,除了利用 list
来实现 each-of type ,还有一个 feature 可供选用:那就是 struct
我们这样定义一个 struct
: (struct foo (bar baz quuz) #:transparent)
其中 foo
是这个 struct
的名称,bar
,baz
,quuz
分别是各个 field
的名称
这里就同 ML 中的 record
很相似了,不同的是 record
中的每一个 field
需要声明类型,而 racket 完全不需要,可以容纳任意类型的数据
定义了一个 struct
,便会生成对应的三个函数:
struct "constructor": 返回一个 foo
,其 bar
,baz
,quuz
领域的值分别是 e1
,e2
,e3
(foo e1 e2 e3)
类型判断函数:如果 e
是一个 foo
,返回 true
(foo? e)
提取值函数:structName-fieldName
返回该 struct 中对应 field 中存储的值
(foo-bar e)
(foo-baz e)
(foo-quuz e)
这三类函数即是我们在用 list 实现时定义的三类函数
那么接下来我们用 struct
实现自己的 eval-exp
函数
(struct const (int) #:transparent)
(struct negate (e) #:transparent)
(struct add (e1 e2) #:transparent)
(struct multiply (e1 e2) #:transparent)
(define (eval-exp e)
(cond [(const? e) e]
[(negate? e) (const (- const-int (eval-exp (negate-e e)))))]
[(add? e) (let ([v1 (const-int (eval-exp (add-e1 e)))]
[v2 (const-int (eval-exp (add-e2 e)))])
(const (+ v1 v2)))]
[(multiply? e) (let ([v1 (const-int (eval-exp (multiply-e1 e)))]
[v2 (const-int (eval-exp (multiply-e2 e)))])
(const (* v1 v2)))]
[#t (error "eval-exp expect an exp")]))
其实是和上面用 list
实现的代码一致的,只不过这些函数无需自己定义,直接被 struct
生成了
另外,标识符 #:transparent
代表在 REPL 中打印出完整的 struct
#:mutable
代表声明的 struct
可以被函数 set-structName-fieldName!
进行修改
Why the struct approach is BETTER
首先需要强调的是,struct 实现 不是 list 实现的语法糖。与 list 不同,struct 实际上创造了 new type of value
对于该 struct : (struct add (e1 e2) #:transparent)
除了 add?
之外的类型判定函数 (如 pair?
, number?
等等) 都不会返回 true
而 list 实现由于本质上是个 list,用 pair?
进行判定仍会返回 true
另外,struct 实现的抽象性也明显更优
在 struct 实现中,访问 field 中的值只能够通过自带的 accessor
而 list 实现中则可以使用 cdr
,car
等等 list
函数来访问,这大大降低了抽象性
Implementing a programming language in general
介绍一下一个语言的一般实现过程 (Language implementation),我们称这个语言为 \(B\) 语言
首先,代码文本经过 parser (解析器,语法分析器) 的检查,排除有语法错误 (括号不匹配,关键字错误) 的代码
生成 Abstract Syntax Tree (抽象语法树)。这是源代码语言结构的一种树形抽象表示,其中树上的每个结点代表源代码中的一种结构
接下来,如果这种语言有类型检查机制的话,type-checker 会对这颗 AST 进行检查
剩下的语言实现 (the rest of the implementation) 一般采取两种方式,有时也会采取这两种方式的混合:
- interpreter (解释器):我们用 \(A\) 语言编写一个称为解释器的程序,它读入 \(B\) 语言程序的 AST 并生成相应的结果
在这个意义上,将这个程序被称为 \(evaluator\) 或者 \(executor\) 可能更为合适。但 \(interpreter\) 这个称呼已经成为了学术共识。 - compiler (编译器):我们用 \(A\) 语言编写一个称为编译器的程序,它读入 \(B\) 语言程序的 AST 并生成一段与其完全等价 (equivalent) 的 \(C\) 语言程序。再利用某些已有的 \(C\) 语言实现 (pre-existing implementation for C) 生成相应结果。在这里,我们称 \(B\) 为 source language (源语言),\(C\) 为 target language (目标语言)
在这两种实现里,用来编写解释器与编译器来实现 \(B\) 语言实现的 \(A\) 语言被称为 metalanguage (元语言)
A one-sentence sermon: Interpreter versus compiler is a feature of a particular programming-language implementation, not a feature of the programming language.
也就是说,编译器或者解释器是语言实现的两种方式,与语言本身无关
我们称 \(C/C++\) 为解释型语言,函数式语言 (functional language) 为编译型语言只是这些语言大多数情况下的实现,但其实为 \(C/C++\) 语言编写一个解释器也是毫无问题的
Implementing a programming language using Rackets
上面的例子就是一个很简单的 interpreter 模型
需要被解释的语言是由 const
, negate
, add
, mutiply
四个关键字组成的算术表达式 (arithmetic expression)
eval-exp
函数则是 interpreter
而编写该 interpreter 的元语言 (metalanguage) 则是 racket
下面我们将利用 Racket 为一个虚拟的编程语言编写 interpreter
假定给定的 AST 是合法的 (这意味着忽略 parser),且不考虑类型错误 (不编写 type-checker,即使出现了实现语言报错 (implementation-depentent error) 的 crash 都没问题)
Interpreter for language with variables need environments
在上面的例子中,我们的算术表达式语言一个最大的缺陷就是没有变量 (variable)
由于没有变量的存在,我们的 interpreter 也十分简单,一个简单的函数就可以搞定了
那么对于含有变量的语言,我们的 interpreter 应该如何编写?
在 week 1 中我们学过,对于某个含有变量的表达式 (expression with variables),我们需要在对应的 环境 (environment that maps variables to values) 中进行 evaluate
所以我们的 interpreter 中必须定义一个辅助函数,读入某个 expression 与某个 environment 并输出对应的结果
在我们自己的 interpreter 中,我们选择牺牲效率,以最形象的方式来表达一个 environment:由若干个 string (变量名) 与 value 对组成的 association list
对于某个给定的环境,interpreter 在以下三种情况处理方式不同:
- To evaluate a variable expression, it looks up the variable’s name (i.e., the string) in the environment.
- To evaluate most subexpressions, such as the subexpressions of an addition operation, the interpreter
passes to the recursive calls the same environment that was passed for evaluating the outer expression. - To evaluate things like the body of a let-expression, the interpreter passes to the recursive call a slightly
different environment, such as the environment it was passed with one more binding (i.e., pair of string
and value) in it.
简单概括,就是 含 variable 的 expression, subexpression 以及 let-expression 的 body 部分三种情况
Implementing closures
除了变量之外,一个编程语言中必不可少的部分还有函数
回忆之前关于 function closure 与 lexical scope 的知识:调用 (call) 函数时函数体的环境并不是调用时的环境 (current environment),而是函数被定义时的环境 (the environment when function is difined)。因此,我们的 interpreter 需要 “记忆” 某个函数被定义时所处的环境。
因此我们可以我们创造一个新的数据结构 closure 来储存 interpret 某个函数后的结果,它包含函数本身和定义时所处的环境。
(struct closure (env fun) #:transparent)
接下来我们处理调用函数的情况。对于一个函数调用 (call e1 e2)
- 在当前环境 (current environment) 下 evaluate e1,结果应为一个 closure (否则出现 run-time error)
- 在当前环境 (current environment) 下 evaluate e2,结果将作为上述 closure 的参数 (argument)
- closure 包含两个部分:代码部分与环境部分,我们用环境部分加上调用方传入的参数形成的环境 evaluate 代码的主体部分。
(原文:We evaluate the body of the code part of the closure using the environment part of the closure extended with the argument of the code part mapping to the argument at the call-site.)