编程语言设计--coursera课程笔记(第二周)

357 阅读10分钟

复合类型、模式匹配、抛错、类型推导以及尾调用

复合类型

复合类型要素

名字不是标准规定,这里的三种说法都是来自本课程

  1. “Each-of”
    表示这个类型的每个元素的类型,比如(3,5)的类型是int*int
  2. “One-of” 表示这个类型的元素的类型范围,类型是在这个范围的某一个
  3. “Self-Reference” 自我引用,这个类型里的元素也包含这个类型。 int list 包含了上面的三种类型,它可以是[] | int list也可以是int和int list的合并。毕竟list可以由一个元素和另一个list Cons而来。

复合元素取值

有两种类型,by position or by name.

(* by position *)
#1 (3,5) 
(* by name *)
fun x (a,b) => a + b

record 类型

record类型是”Each-of”类的,在创建绑定时会一次求每个field的值。

{f1 = e1, ..., fn = en} 

注意是等于号,类型那里才是冒号。

类型

{foo : int, bar : int*bool, baz : bool*int} 类型里顺序不重要,

求值

record本身就是值,会像tuples一样求值到所有表达式执行完毕。

 {bar = (1+2,true andalso true), foo = 3+4, baz = (false,9) } 
==>
 {bar = (3,true), foo = 7, baz = (false,9)}

在REPL中record的type或者value的filed的顺序按照字母来排序的,顺序不重要。

取值

通过#加上field name,和tuples很像

#f { f = 3, g = 12 }

和tuple的关系

tuple是record的语法糖( Syntactic sugar ) tuple表述的语义都能通过record的解释,并且其写法更简单

{1: true, 2: 5}  
(true,5)

当record的field name是从1开始的连续数字时可以转换成tuple. 语言设计的时候保证核心思想尽可能少可以使得语言更容易实现(implement), 比如这边的tuple就把它转换成record继而进行操作

自定义”one-of”类型

定义

datatype t = C1 of t1 | C2 of t2 | … | Cn of tn

datatype mytype = TwoInts of int * int
	                | Str of string
				    | Pizza 

“|” 理解成或,上面这个类型,存在三种值。TwoInts, Str都是构造函数,of后面理解成构造函数参数;所以这个类型可以由TwoInts (3,5), Str “xix”Pizza得到。Pizza是直接表示这个类型的某个值,它也是一个构造函数只是不携带数据。

val a = 3 : int
val a = TwoInts (3,4) : mytype

给a赋值为TwoInts (3,4), 在REPL会显示如上。TwoInts (3,4)整体是个值,里面的构造函数可以看成一个标签,表明这个值的来源。 自定义类型同样可以“self-reference”

datatype exp = Constant of int
             | Negate of exp
             | Add of exp * exp
             | Multiply of exp * exp

取值

需要判断类型的值是哪个变体variant,即怎么构造的,再进行分别取值。

取值方法一

listoption 两个”one-of” type 取值的时候都要进行判断是否是空的,再执行取值方法。list是null ,hd ; option是NONE, valOf;

取值方法二

自定义类型中ml是通过case表达式取值。其实上面的取值方法都是可以通过case表达式实现。又是一次语法糖呢~option和list也是都可以通过自定义类型实现~~

funfx= (* f has type mytype -> int *) 
		case x of 
        Pizza => 3
      | TwoInts(i1,i2) => i1 + i2
      | Str s => String.size s

会先对x 取值,进而一次匹配三条分支里的“模式”,进而取得某个值,三个分支 p => e p表示pattern, e是表达式,即是case表达式的取值。分支的值类型需要一致,就好像if else的分支表示式类型一致一样。其中i1,i2就好像本地绑定一样,匹配模式从值中绑定i1,i2的值,进而从variant取得了值。

case 表达式取值的好处
  1. 可以不必像hd取[]时抛错一样,不要依靠人为记忆,每次在取列表时先判断是否为空。减少运行时抛错总是好的~
  2. 如果写case表达式的时候漏了某个分支,编译器会告诉你,编译器”帮”你考虑了所有可能
case表达式的使用场景
  1. enumerations 定义某一类型的取值范围
  2. 识别实际生活中的人,物体。比如识别特别学生,可以采用学生号或者姓名~

和list, option之间的关系

list, option都可以用datatype来实现; 获取list和option的值最好使用case表达式(better style),这样不会忘记判断容器是空的情况。

fun inc_or_zero intoption =
    case intoption of
        NONE => 0
      | SOME i => i+1

fun append (xs,ys) =
    case xs of
        [] => ys
      | x::xs’ => x :: append(xs’,ys)

其中[]是构造函数 历史原因::这个符号其实也是构造函数,只是它和其他不同的是它的参数前后都有。

polymorphic datatypes

list,option可以携带任何类型的元素,int list ,string list, int option。自定义类型也可以携带任何类型。 比如option类型就是这么定义的,其中option可以理解成类型构造器,前面的需要加上符号~ 。这样 int option , string option整体才是类型 datatype ’a option = NONE | SOME of ’a.

函数参数的多态
  (* type is int list -> int *)
  fun sum_list xs =
      case xs of
          [] => 0
        | x::xs' => x + sum_list xs'
  (* type is 'a list * 'a list -> 'a list *)
  fun append (xs,ys) =
      case xs of
          [] => ys
        | x::xs' => x :: append(xs',ys)

如上type-checker会分析出第二个函数的参数类型是多态的,而第一个函数参数是int list,因为+运算符~

type synonym

type aname = t 创造t类型的另一种叫法,和alias比较像。

type card = suit * rank
(* 写了一个函数 *)
card -> bool  
suit * rank -> bool  (* 但REPL有时会这么显示 *)

模式匹配 pattern matching

模式不是表达式 模式匹配不只局限于上面的case expression里的”one-of” type, 还适用于”each-of”type

使用场景

  1. case expression 可以引入局部绑定,不想引入局部绑定使用_
  2. 绑定变量时 val p = e这里的p就是一个模式;想起了js中的 destructuring.
val (a,b) = (3,4);
(* a,b就会被分别赋值3和4 *)
  1. 函数其实只有一个参数,参数就是一个pattern. fun f p = e
fun full_name {first=x,middle=y,last=z} =
    x ^ " " ^ y ^ " " ^z

fun hello()= ... 这里的参数类型unit.

嵌套模式 (nested pattern)

模式可以嵌套任意多的层级,模式匹配就是去比较一个模式一个值(value),如果它们“长得一样”(same shape),就绑定变量到相应部分的值。 模式匹配的定义如下:

  1. 如果p是一个变量x, 那么匹配成功并且x绑定到v val x = v
  2. 如果p是_, 那么匹配成功,并且不会带入变量 case x of _=> 2
  3. 如果p是(p1,…pn)并且v是(v1,…vn),那么匹配从第一个 开始p1,v1依次匹配。并且递归的去匹配。
  4. 如果p是C p1,如果v是 C v1 那么匹配成功,C是构造函数。

idiom

  1. a::b::c::d 匹配list元素数量大于三的
  2. a::b::c::[]匹配list元素数量是三的
  3. ((a,b),(c,d))::e 匹配非空的pair嵌套pair的列表

案例

可以避免过多层级的判断

exception ListLengthMismatch
(* don't do this *)
fun shallow_zip3 (l1,l2,l3) =
    case l1 of
	[] => 
	(case l2 of 
	     [] => (case l3 of
			[] => []
		      | _ => raise ListLengthMismatch)
	   | _ => raise ListLengthMismatch)
      | hd1::tl1 => 
	(case l2 of
	     [] => raise ListLengthMismatch
	   | hd2::tl2 => (case l3 of
			      [] => raise ListLengthMismatch
			    | hd3::tl3 => 
			      (hd1,hd2,hd3)::shallow_zip3(tl1,tl2,tl3)))


(* do this *)
fun zip3 list_triple =
    case list_triple of 
	([],[],[]) => []
      | (hd1::tl1,hd2::tl2,hd3::tl3) => (hd1,hd2,hd3)::zip3(tl1,tl2,tl3)
      | _ => raise ListLengthMismatch


zip3 ([1,2,3],[4,5,6],[7,8,9])
[(1,4,7),(2,5,8),(3,6,9)]

函数模式

ml中引入了function pattern,和java中重载类似,只是把函数表达式内部的case表达式移到了函数定义部分。

fun f p1 = e1
    | f p2 = e2
    ...
    | f pn = en
  fun f x =
    case x of
       p1 => e1
     | p2 => e2
		...
		| pn => en 

类型推导 type inference

利用模式匹配,可以不用在函数定义时声明参数,ml的type-checker会推导出来。

(* int * int * 'a * int -> int *)
  fun partial_sum (x, y, z) =
		x+z 
fun sum_triple (triple : int * int * int) =
    #1 triple + #2 triple + #3 triple

对比上面两个函数,第一个函数里type-checker推测出y的类型为任意的;第二个函数因为没有使用模式匹配,type-checker不知道triple到底有几个元素,所以需要声明参数类型。

‘a list * 'a list -> 'a list
(* is more general than *)
string list * string list -> string list

equality types

''a list * ''a -> string

fun same_thing(x,y) = if x=y then "hs" else "no";

当参数使用了=做比较时会出现上面的”相等“类型。即两个参数可以做=运算,比如int,string,tuples(里面的也是相等类型).

exception

声明一个出错并且添加了一个构造函数,和datatype有点像,只不过这边的构造函数构造的类型都是exn

声明和抛出

exception MyFirstException
exception MySecondException of int * int
	raise MyFirstException
	raise MySecondException (7,9)

错误值用作参数

注意是值

exception MyUndesirableCondition
fun maxlist (xs,ex) = (* int list * exn -> int *)
    case xs of
        [] => raise ex
      | x::[] => x
      | x::xs' => Int.max(x,maxlist(xs',ex))

val w = maxlist ([3,4,5],MyUndesirableCondition) 

处理抛错

e1 handle exn => e2 e1表达式里有抛出的话就执行e2表达式,exn部分仍然是模式匹配,匹配可以像之前的case表达式那样有多个分支。

val x = maxlist ([3,4,5],MyUndesirableCondition) (* 5 *)
	handle MyUndesirableCondition => 42

函数定义在上面

尾递归(tail recursion)

调用栈( call stack )

函数调用就会忘调用栈里push一个元素,递归调用就不会接着往里push,每个栈里的元素会储存本地变量以及当前函数调用剩余没有执行的部分。 比如:

fun fact n = if n = 0 thenn 1 else n * fact (n-1)

每次递归调用时前一个函数调用要等递归调用结束完再去执行加法.调用栈是不断变多的。

尾调用 (tail call)

fun fact2 n =
    let fun aux(n,acc) = if n=0 then acc else aux(n-1,acc*n)
    in
        aux(n,1)
    end

如上代码每次调用都是直接返回另一个调用的结果,不存在当前函数和递归调用的函数的结果进行再次操作的情况。ml编译器会识别出这是尾调用,进而不会像上面的递归那样在调用栈里不停的创建调用元素,而是会重复利用当前的调用空间,节省了内存的使用。很多函数式编程语言中都会有这个优化手段。

将递归调用转化为尾调用的常规方法

创建一个帮助方法参数是一个累加器(accumulator) 递归函数返回累加器。

fun rev xs = 
	case xs of 
		[] => []
		| x::xs' => (rev xs') @ [x]

(* good *)
fun rev xs = 
	let fun aux(xs,acc) = 
		case xs of 
			[] => acc
			| x::xs' => aux(xs' ,x::acc)
		in 
			aux(xs,[])
		end

其中第一个递归调用的函数有个明显问题就是它的递归调用里因为使用了数组拼接,数组拼接会遍历前一个数组。所以是平方级的复杂度。

尾调用的问题

不是所有的函数调用都能被写成尾调用(比如遍历树),有时你写的尾调用甚至创造的额外数据占据的空间和常规递归调用创造的空间一样。另外“不要过早优化你的代码”~~

尾调用的定义

引入了尾位置(tail position), 如果函数调用是在尾位置就是尾调用

  1. fun f(x) = e e是尾位置
  2. 如果一个表达式不在尾位置,那么它内部的任意表达式也都不在尾位置
  3. if e1 then e2 else e3如果这个if表达式整体在尾位置,那么e2,e3是在尾位置
  4. let b1 … bn in e end如果这个let表达式整体在尾位置,那么e是在尾位置。
  5. 函数调用时的参数不在尾位置。因为我们任然需要去evaluate参数表达式,以及调用函数体。f(g x) 即使整个f()调用是尾调用,里面的g x不是。