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 的准确定义:

  1. 给出一个 模式 (pattern) 和 一些 值 (value)
  2. 判断模式与值是否匹配 (match)
  3. 如果匹配,将 模式中的变量与右侧的值绑定 (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)

这是一个尾递归

posted @ 2022-03-19 23:30  四季夏目天下第一  阅读(99)  评论(0编辑  收藏  举报