Coursera Programming Languages, Part B 华盛顿大学 Week 2

2023-03-07,,

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 的 expressionsubexpression 以及 let-expression 的 body 部分三种情况

Implementing closures

除了变量之外,一个编程语言中必不可少的部分还有函数

回忆之前关于 function closurelexical 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.)

Coursera Programming Languages, Part B 华盛顿大学 Week 2的相关教程结束。

《Coursera Programming Languages, Part B 华盛顿大学 Week 2.doc》

下载本文的Word格式文档,以方便收藏与打印。