这是我参与11月更文挑战的第10天,活动详情查看:2021最后一次更文挑战」
可修改的列表
列表是不能被修改的。一般没有办法去修改列表中的元素的值。但是,在OCaml中你可以从旧列表中创建新的列表。比如,如果我们想写一个函数可以返回和输入的列表相同的列表,除了列表的第一个元素加一。我们可以这么做:
utop # let inc_first lst =
match lst with
| [] -> []
| h :: t -> h + 1 :: t;;
val inc_first : int list -> int list = <fun>
现在,你可能考虑到这样做会浪费空间。毕竟,至少有两种方式编译器可以实现上面的代码:
- 当创建一个新列表是可以把列表
t的尾部复制,这样内存会增加t的长度 - 两个列表共享
t,这样内存的容量不会增加,但是需要一个额外的空间存放h + 1
实际上,编译器会选择后面。编译器这样选择的原因是因为列表中的元素是不可变的。如果列表变得可修改,那么我们会担心共享的列表在做出修改时是否可见。所以不可见性会使得代码变得容易并且编译器也很容易优化。
列表的模式匹配
之前我们使用模式匹配来访问列表,下面我们详细的说明模式匹配的语法
Syntax
match e with
| p1 -> e1
| p2 -> e2
| ...
| pn -> en
其中, 分句p1 -> e1被称为一个分支或者模式匹配的一种情况。前面的竖线代表整个匹配中是可选的。p为一个新的语法结构: “模式”。一个模式可以是:
- 一个变量名 : 如
x - 下划线,
_, 为通配符 - 空列表 :
[] p1 :: p2[p1; ...; pn]
没有变量名字可以在“模式”中出现超过一次。例如,x :: x就是非法的。
Dynamic semantics
模式匹配设计两个相关内涵的任务: 决定一个模式是否匹配一个值;并且确定哪些部分应与模式中哪些变量相关。前一个任务直观的确定模式和值是否有相同的shape;后一个任务是确定模式引入的variable bindings。例如, 考虑下面的代码:
utop # match 1 :: [] with
| [] -> false
| h :: t -> h >= 1 && List.length t = 0;;
- : bool = true
我们先看第二个分支,h与1绑定,并且t与[]绑定。这样我们可以写成h->1表示h有值1;类似的记号, 我们可以得到,h-> 1,t -> []。
使用这种极好,下面就会有当模式匹配到一个值的时候发生的事情:
- 模式
x匹配任意的值v会产生variable binding:x -> v - 模式
_可以与任意值匹配,但不产生绑定 - 模式
[]与值[]匹配但不产生任何绑定 - 如果
p1与v1匹配并且产生系列的绑定b_1,然后p2与v2匹配并且产生系列的绑定b_2。这样p1::p2与v1::v2匹配产生的匹配为b_1 U b_2。 - If for all
iin1..n, it holds thatpimatchesviand produces the set 𝑏𝑖bi of bindings, then[p1; ...; pn]matches[v1; ...; vn]and produces the set⋃_𝑖 𝑏𝑖⋃ibiof bindings.
其evaluate过程可以看: lists more
深度模式匹配
_ :: []matches all lists with exactly one element_ :: _matches all lists with at least one element_ :: _ :: []matches all lists with exactly two elements_ :: _ :: _ :: _matches all lists with at least three elements
立即匹配(Immediate Matches)
let rec sum lst =
match lst with
| [] -> 0
| h :: t -> h + sum t
你可以写成
let rec sum = function
| [] -> 0
| h :: t -> h + sum t
尾部递归
考虑下面的代码:
let rec sum (l : int list) : int =
match l with
| [] -> 0
| x :: xs -> x + (sum xs)
let rec sum_plus_acc (acc : int) (l : int list) : int =
match l with
| [] -> acc
| x :: xs -> sum_plus_acc (acc + x) xs
let sum_tr : int list -> int =
sum_plus_acc 0
观察上面的代码,我们可以发先sum和sum_tr的区别: 在sum函数中,当递归返回值是我们将x加上去,这是非尾递归;而sum_tr或sum_plus_acc,当所有的递归返回时,我们立即的返回值而不需要额外的计算。
如果你要对长列表写递归函数,那么尾递归就显得很重要。但不是说,尾递归就会一直很重要。例如,尾递归会使得代码变得很难读。下面有一个用尾递归函数来产生一个长队列:
utop # let rec from i j l = if i> j then l else from i (j - 1) (j :: l);;
val from : int -> int -> int list -> int list = <fun>
utop # let ( -- ) i j = from i j [];;
val ( -- ) : int -> int -> int list = <fun>
utop # let long_list = 0 -- 1_000_000;;
val long_list : int list =
[0; 1; 2; 3; 4; 5; 6; 7; 8; 9; 10; 11; 12; 13; 14; 15; 16; 17; 18; 19; 20;
21; 22; 23; 24; 25; 26; 27; 28; 29; 30; 31; 32; 33; 34; 35; 36; 37; 38; 39;...]