Coursera Programming Languages, Part A 华盛顿大学 Week 2
第一周介绍了 ML 语言的一些表达与基本的 Language pieces
第二周主要关注 ML 语言中的各种类型 (type)
Conceptual ways to build new types
任何一门编程语言都包含有两种类型,基础类型 (base type) 与复合类型 (compound type)。
其中,基础类型包括 int
, bool
, string
这种单一的类型,而复合类型的定义里可以包括不止一种基础类型。
我们可以简单的将复合类型分为三类:
- Each-of type (Product type) : 积类型。积复合类型 t 可以包括 t1, t2, ..., tn 基本类型中的 每一种 (Each of)
一个典型的例子是 Tuple 类型。a = (3, true) : int * bool
- One-of type (Sum type | tagged union) : 和类型 / 标签联合类型。和复合类型 t 可以包括 t1, t2, ..., tn 基本类型中的 任一种 (One of)
可以看作是把不同的类型,通过携带标签 (tag field) 的方式放在了一起。在 ML 语言中,值构造器 (value constructor) 就可以看作是标签。
一个典型的例子是 Option 类型。(int option -> int / NONE)
- Recursive type : 递归类型。递归类型的定义中通常包含自我引用 (self-reference) 。其定义也经常结合和类型与积类型,这使得定义递归数据结构成为了可能。
一个典型的例子是 list 类型。(hd a int list -> int; tl a int list -> int list / [])
不同的类型之间可以互相嵌套。 (Nested)
Records : Another approach to build each-of type
ML 语言中的 Record type 是一种每一个成员是一个命名域 (named field) 的 each-of type。我们使用 Record expression 来创建一个 Record value。
e = {foo = (3, true), bar = ("hi", 4)} : {bar : string*int, foo : int*bool}
#foo e = (3, true), #bar e = ("hi", 4) (* 通过 # 取出值 *)
foo, bar 都是这个 Record 的命名域。在 REPL 中命名域会按照字典序排序。
{f1 = e1, f2 = e2, ..., fn = en}
是一个 Record expression 的标准形式,其中每个 f 都是命名域的名称,e 是表达。我们无需声明 e 的类型,type-checker会自动帮我们检测类型。
Syntactic sugar : the truth about tuples
我们发现 tuples 和 records 有很多相似之处,都是 each-of 类型,允许任意多不同类型的复合。
不同的是,tuple 是通过组分的位置 (By position) 来调用各种组分的,而 records 则是通过组分对应的命名域 (By name) 来调用的。
不难发现,tuple 实际上是一种特殊的 record : 其 field name 均为 1 到 n 中的任意整数,即
(e1, e2, ..., en) = {1 = e1, 2 = e2, ..., n = en}
且
t1*t2*...*tn = {1 : t1, 2 : t2, ..., n : tn}
ML 语言使用 record 定义了 tuple。问题来了,tuple 的功能用 record 完全可以实现,为什么 ML 语言还要提供这么一种写法 (semantics) 呢?
这就要引出 语法糖 (syntactic sugar) 的定义了:
计算机语言中添加的某种语法,这种语法对语言的功能并没有影响,但是更方便程序员使用。通常来说使用语法糖能够增加程序的可读性,从而减少程序代码出错的机会。
因此,tuple 就是一种语法糖:即使去掉这个 feature,我们仍然可以利用 record 来实现;然而有了这个feature,我们的代码更简洁,更利于理解。
Datatype bindings: Build our own "one-of" types
接下来我们介绍 datatype binding,这是目前我们学到的第三种 binding 类型。它能够帮我们构建标签联合类型。
下面是一个例子:
datatype mytype = TwoInts of int*int
| Str of string
| Pizza
以上的例子定义了一个类型 mytype
,其值 (value) 可以是 int*int
或者 string
或者 nothing。
任何值都会被标记 (tagged) 来让我们知道它的种类:而这些“标签” (tags) 就是所谓的构造器 (constructors),在我们的例子里体现为 TwoInts
, Str
, 与 Pizza
。这些构造器的名字是自定义的,并且不同的构造器可以标记相同的数据类型。
例子中的 datatype binding 向环境中添加了:
- 一个新的类型
mytype
- 三个构造器
TwoInts
,Str
,Pizza
构造器实际上是一种将指定的类型转换为自定义类型的函数。在我们的例子中,TwoInts : int*int -> mytype, Str : string -> mytype, Pizza : mytype。注意到 Pizza 本身就是mytype
类型的值。
通过 constructor 我们可以构造 mytype 类型的值。如 TwoInts(1, 2), Str"hi", Pizza。
对于同样是 one-of type 的 option 来说,通过isSome : option -> bool
函数可以判断 variant 类型 (NONE or SOME),valOf : t option -> t
可以取出 SOME 中的值。而如果是自己定义的 datatype ML 语言并不提供这些函数,只能自己手写。我们接下来会学到一种更好的方式: Case expressions
How ML provides access to datatype values : Case Expressions
fun f x = (* f has the type mytype -> int*)
case x of
Pizza => 3
| TwoInts(i1, i2) => i1 + i2
| Str s => String.size s
在 case expression 中,每一个 branch 都是 p => e
的形式,其中 p 成为 pattern。
注意每一个 branch 的 e 必须是相同的类型,在上述例子中为 int。
pattern 看起来像是 expression,但实际上它并不会被 evaluate。真正被 evaluate 的是其后的 expression 。pattern 存在的意义是为了与之后的 expression 进行匹配 (match)。因此对一个 case expression 进行 evaluate 的过程被称为 pattern-matching 。
通过 case expression 与函数递归性质的结合,我们可以实现一下操作:
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)
eval(Add(Constant 3, Negate (Constant 3))) (* 这个表达式的结果是 0,这实际上是一个树形结构*)
Datatype bindings and Case expressions so far, precisely
Datatype bindings:
datatype t = C1 of t1 | C2 of t2 | ... | Cn of tn
向环境中添加 t 与 C1, C2, ..., Cn
(of tn) 可以省略如果 Cn carries nothing ,Cn 本身的类型即为 t
Case expressions:
case e of p1 => e1 | p2 => e2 | ... | pn => en
Case expressions 将 e evaluate 为 v,然后找到第一个与 v 匹配 (matches) 的 pi,然后 evaluate 对应的 ei 作为整个 case expression 的结果
形如 Ci(x1, x2, ..., xn) 的 pattern, Ci 为类型为 (t1t2...tn -> t) 的构造器(或者类型为 t, Ci carries nothing)。这样的 pattern 匹配 (match) 一个形如 Ci(v1, v2, ..., vn) 的值并且 将每一个 xi 与 vi 做 binding 来 evaluate 对应的 ei。
Type synonyms
和 C++ 中的 typedef
关键词很像
datatype suit = Club | Diamond | Heart | Spade
datatype rank = Ace | Jack | Queen | King | Num of int
type card = suit*rank
card 与 suit*rank 类型完全等价
Lists and options are datatypes
我们当然可以使用 datatype 来实现 list, option 这些内置数据 type。( 注意利用 datatype definition 中的 recursive 特性 )
datatype my_list = Empty | Cons of int*my_list
val one_two_three = Cons(3, Cons(2, Cons(1, Empty)))
fun append(xs, ys) =
case xs of
Empty => ys | Cons(x, xs') => Cons(x, Cons(xs', ys))
ML 语言内置的 list 也是类似如此实现的,只不过 Empty -> []
, Cons 换成了二元运算符 ::
以 pattern matching 的方式访问 list 与 option 比用 null 与 hd 这类内置函数访问的方式要更好,之所以 ML 语言提供这类内置函数的原因以后再说。
list 与 option 都是多态polymorphic datatypes,即可以储存多种数据类型。同样可以利用 datatype 进行定义
datatype t list = Empty | Cons of t*int
datatype t option = NONE | SOME of t
The truth about binding and pattern matching
我们可以发现,pattern matching 几乎可以用在所有需要引入新变量传递值的场合。
这里是 pattern matching 的准确定义:
- 给出一个 模式 (pattern) 和 一些 值 (value)
- 判断模式与值是否匹配 (match)
- 如果匹配,将 模式中的变量与右侧的值绑定 (binding)
这就是所谓的模式匹配。
特殊的,_
符号可与匹配上任何值且不会引入新的绑定
可以注意到,当我们在使用函数时,有时不需要加括号,如
fun f x = x + 1
而有时候在“传入多个参数时”,括号时不能省去的
fun f (x, y) = x + y
本来我以为这是一种很常见的语法糖,但实际上 ML 语言的逻辑是:任何函数只传入一个参数。我们理解的 f (x, y) 作为两个参数实际上是一个 pair
而 f() -> t
其实也是一个单参数函数,()
是一个类型为 unit 的参数
这个特性,使得 ML 语言中任意一个函数都可以很方便的作为其他函数的参数。
关于 type inference
ML 语言的 类型推断 (type inference) 功能保证了我们在定义函数的时候无需再对参数的类型进行说明。
Exception
ML 语言中异常可以被定义
exception IllegalMove
当出现异常时,使用 raise
关键字来抛出一个异常
raise IllegalMove
使用 handle
关键字来处理一个异常,格式如下
e1 handle p => e2
当 e1 抛出的异常与 p 模式匹配上时,则整个表达式为 e2 的值,若不匹配,仍然表现为异常
Tail recursion and accumulators
尾递归可以使递归栈能够被重复利用,能够大大提高运行效率。
具体方式是在参数中增加一个 accumulator 作为递归终点的返回值,并且每一层在得到下一层的返回值后不再有多余的操作。
fun fact n =
if n == 0
then 1
else n * fact(n - 1)
fact(10)
这是一个普通递归,在得到 f(n - 1) 后仍有相乘的操作
fun fact(n, acc) =
if n == 0
then acc
else fact(n - 1, acc * n)
fact(10, 1)
这是一个尾递归