OCaml Lists Part Two

454 阅读4分钟

这是我参与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

我们先看第二个分支,h1绑定,并且t[]绑定。这样我们可以写成h->1表示h有值1;类似的记号, 我们可以得到,h-> 1t -> []

使用这种极好,下面就会有当模式匹配到一个值的时候发生的事情:

  • 模式x匹配任意的值v会产生variable bindingx -> v
  • 模式_可以与任意值匹配,但不产生绑定
  • 模式[]与值[]匹配但不产生任何绑定
  • 如果p1v1匹配并且产生系列的绑定b_1,然后p2v2匹配并且产生系列的绑定b_2。这样p1::p2v1::v2匹配产生的匹配为b_1 U b_2
  • If for all i in 1..n, it holds that pi matches vi and produces the set 𝑏𝑖bi of bindings, then [p1; ...; pn] matches [v1; ...; vn] and produces the set ⋃_𝑖 𝑏𝑖⋃ibi of 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

观察上面的代码,我们可以发先sumsum_tr的区别: 在sum函数中,当递归返回值是我们将x加上去,这是非尾递归;而sum_trsum_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;...]