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的更多相关文章

  1. Coursera课程 Programming Languages, Part A 总结

    Coursera CSE341: Programming Languages 感谢华盛顿大学 Dan Grossman 老师 以及 Coursera . 碎言碎语 这只是 Programming La ...

  2. Coursera课程 Programming Languages 总结

    课程 Programming Languages, Part A Programming Languages, Part B Programming Languages, Part C CSE341: ...

  3. Coursera课程 Programming Languages, Part B 总结

    Programming Languages, Part A Programming Languages, Part B Part A 笔记 碎言碎语 很多没有写过 Lisp 程序的人都会对 Lisp ...

  4. The history of programming languages.(transshipment) + Personal understanding and prediction

    To finish this week's homework that introduce the history of programming languages , I surf the inte ...

  5. Natural language style method declaration and usages in programming languages

    More descriptive way to declare and use a method in programming languages At present, in most progra ...

  6. The future of programming languages

    In this video from JAOO Aarhus 2008 Anders Hejlsberg takes a look at the future of programming langu ...

  7. Hex Dump In Many Programming Languages

    Hex Dump In Many Programming Languages See also: ArraySumInManyProgrammingLanguages, CounterInManyPr ...

  8. 在西雅图华盛顿大学 (University of Washington) 就读是怎样一番体验?

    http://www.zhihu.com/question/20811431   先说学校.优点: 如果你是个文青/装逼犯,你来对地方了.连绵不断的雨水会一下子让写诗的感觉将你充满. 美丽的校园.尤其 ...

  9. ESSENTIALS OF PROGRAMMING LANGUAGES (THIRD EDITION) :编程语言的本质 —— (一)

    # Foreword> # 序 This book brings you face-to-face with the most fundamental idea in computer prog ...

  10. Comparison of programming languages

    The following table compares general and technical information for a selection of commonly used prog ...

随机推荐

  1. #pragma 小节

    (1)#pragma startup function-name <priority> (2)#pragma exit function-name <priority>   含 ...

  2. KiCad,一款开源的PCB设计软件

    Kicad,一款开源的PCB设计软件 1.偷偷摸摸久矣 还记得是大二的实践课,用Protel99se设计一款PCB,把电路图转印到铜板上并腐蚀出来,然后钻孔.焊接,那时候的软件是真难用,后面自学了Al ...

  3. Elasticsearch 实战

    需求 假设现在有这么一个需求,系统接了很多的报文,需要提供全文检索,为了简化,报文目前只有类型,流水号,内容这三个字段. 索引设计 建立msg索引,映射规则如下 PUT /msg { "ma ...

  4. C#中socket的简单使用

    一.Socket的概念Socket其实并不是一个协议,而是为了方便使用TCP或UDP而抽象出来的一层,是位于应用层和传输控制层之间的一组接口. 当两台主机通信是,必须通过Socket连接,Socket ...

  5. (一)从路由器和IP地址开始折腾

    我们应当知道的一点是,由于IP地址只有32bit, 所以很快就面临着不够用的情况,现在之所以大家还在正常使用IPv4, 就是因为采用了公有地址和私有地址的概念:所谓的私有地址是从当时公有地址中还没有分 ...

  6. jabc连接数据库

    Java数据库连接,(Java Database Connectivity,简称JDBC)是Java语言中用来规范客户端程序如何来访问数据库的应用程序接口,提供了诸如查询和更新数据库中数据的方法.JD ...

  7. jmeter-时间处理

    ${__time(,)} 1486091280955 //无格式化参数,返回当前毫秒时间,默认13位.一般用来做时间戳 ${__time(/1000,)} //为取10位的时间戳的函数表达式(时间精确 ...

  8. gin web 2

    routers/router.go package routers import ( "github.com/gin-gonic/gin" "gin-blog/pkg/s ...

  9. Java设计模式之策略模式(13)

    策略模式定义了一系列算法,每个算法封装起来,他们可以相互替换,且算法的变化不会影响到使用算法的客户.可以设计一个抽象类提供辅助. package WHP; public interface ICalc ...

  10. C++ NRV优化

    NRV,named return value 某个函数返回一个对象时,在外部接收 A a = Fun(); 会调用复制构造函数 会损失性能 为何不能将函数Fun定义成传入引用a,然后直接在a上进行操作 ...