原文地址:http://www.ocaml-tutorial.org/data_types_and_matching 翻译:ShiningRay
和Perl一样,OCaml也将对列表的支持直接内建在语言中了。OCaml中一个列表的所有元素的类型必须一致。使用以下格式来写列表:
[1; 2; 3]
(注意是分号,不是逗号)。
[] 表示空列表。
一个列表有一个“头”(第一个元素)和一个“尾”(剩下的元素)。头是一个元素,而尾则是一个列表,所以前面的例子中,表头是整数1,而表尾是list[2; 3]。
另一种做法是使用cons操作符——头 :: 尾。所以下面的列表写法是完全一样的:
[1; 2; 3]
1 :: [2; 3]
1 :: 2 :: [3]
1 :: 2 :: 3 :: []
为什么我要提到cons操作符呢?其实,当我们对列表使用模式匹配的时候,它是相当有用的,下面我说详细说明。
链表的类型
整数链表的类型是int list,一般来说,foo类型的链表的类型就是foo list。
由此可以看出,链表的所有元素都必须是同一种类型的。但是类型却可以是多态的(即,'a list),当你要写操作"lists of anything"(任意类型的列表)的泛型函数时,这就相当有用了(注意,'a list 并不代表每个单独的元素可以有不同的类型——所以你还是不能使用它来构造包含混合类型的列表,也就是说,元素的类型可以任意,但所有元素的类型必须相同)。
length函数是定义在OCaml的List模块中的,它是一个很好的例子。它不论列表包含的整数还是字符串或者对象、 还是什么小怪物,List.length函数都可以对其进行处理。因此,List.length的类型是:
List.length : 'a list -> int
C 和 C++都有一个简单的struct的概念,它是结构的缩写。Java中可以使用类来达到类似的效果,虽然费事得多。
考虑以下简单的C结构
struct pair_of_ints {
int a, b;
};
在OCaml中,能等同于它的最简单的东西是tuple(元组),诸如(3, 4),其类型为int * int。与列表不同,元组可以包含不同类型的元素,所以例如(3, "hello", 'x')的类型为int * string * char。
在OCaml中还有一种稍微复杂一点的写C结构的方法,是使用一个record(记录)。记录,和C结构一样,可以让你对其中的元素进行命名,而无需记住它们出现的顺序。下面是与上面的C结构等同的记录:
type pair_of_ints = { a : int; b : int };;
这就可以定义一个类型,那么如何实际创建这种类型的对象呢?
{ a=3; b=5 }
注意我们在类型定义中使用了":",而在创建这种类型的对象时使用了"="。
下面是一些例子:
# type pair_of_ints = { a : int; b : int };;
type pair_of_ints = { a : int; b : int; }
# {a=3; b=5};;
- : pair_of_ints = {a = 3; b = 5}
# {a=3};;
Some record field labels are undefined: b
所以OCaml不会允许在结构中有未定义的字段。
C语言中并不直接存在“条件联合”("qualified union"),虽然gcc编译器中对它有一些支持。 下面是C中使用条件联合的一种模式:
struct foo {
int type;
#define TYPE_INT 1
#define TYPE_PAIR_OF_INTS 2
#define TYPE_STRING 3
union {
int i; // If type == TYPE_INT.
int pair[2]; // If type == TYPE_PAIR_OF_INTS.
char *str; // If type == TYPE_STRING.
} u;
};
大家应该可以明白,而且它看上去并不太好看。首先,它不安全:程序员可能会犯一个错误,并意外地(假设)当结构中实际存储着字符串的时候调用了u.i字段。同时编译器也不能很容易检查是否所有可能的类型都在switch语句中检验过了(你可以使用一个enum类型来解决这个特殊的问题)。程序员也可能忘记设置type字段,这就可能导致这种奇怪的事情发生。此外,这种模式也很麻烦。
下面是在OCaml中等同于上面的一种优雅并简练的解决方法:
type foo = Nothing | Int of int | Pair of int * int | String of string;;
这就是我们所需的类型定义。每个由| 分开的部分的第一个部分称做构造器(constructor)。你可以将其换作任何东西,只要其首字母大写。如果构造器可以用于定义一个值,那么在其后跟上of type部分,这里类型type总是以小写字母开头。在前面的例子中, Nothing被用作一个常量,其他的构造器则用于创建值。
要实际创建这种类型的东西,你可以输入:
Nothing
Int 3
Pair (4, 5)
String "hello"
&c.
其中每个表达式的类型都为foo。
注意当定义类型的时候,使用of,当写该类型的元素的时候则没有。
扩展一下,一个简单的Cenum的定义为:
enum sign { positive, zero, negative };
在OCaml中可以写作:
type sign = Positive | Zero | Negative;;
递归变体(用于树)
变体也可以是递归的,这也常用于定义树状结构。这就是函数式语言强大表达力之所在:
type binary_tree = Leaf of int | Tree of binary_tree * binary_tree;;
以下是一些二叉树。你可以在纸上画一下它们的结构练习一下。
Leaf 3
Tree (Leaf 3, Leaf 4)
Tree (Tree (Leaf 3, Leaf 4), Leaf 5)
Tree (Tree (Leaf 3, Leaf 4), Tree (Tree (Leaf 3, Leaf 4), Leaf 5))
参数化变体
上一节中的二叉树每个叶上都是整数,但如果我们只是要描述一个二叉树的形状,而留着以后再确切决定存储何种叶结点,该如何做?这时我们可以使用一个参数化(多态)变体,如下:
type 'a binary_tree = Leaf of 'a | Tree of 'a binary_tree * 'a binary_tree;;
这是一个通用类型。在每个叶上都存储了整数的类型称之为int binary_tree。类似的,存储字符串的特定类型称之为string binary_tree。在下面的例子中我们会在OCaml中输入一些实例并让类型推断系统将类型告诉我们:
# Leaf "hello";;
- : string binary_tree = Leaf "hello"
# Leaf 3.0;;
- : float binary_tree = Leaf 3.
注意类型的名称是反向的。将它类型名称和列表的类型名称进行一下类比,比如int list等等。
事实上'a list也这样“逆向”写并不是偶然。列表只不过是有着以下一些特殊定义的参数化变体类型:
type 'a list = [] | :: of 'a * 'a list (* OCaml伪代码 *)
事实上,上面的定义是不能通过编译的。下面才是等同的有效定义:
# type 'a list = Nil | :: of 'a * 'a list;;
type 'a list = Nil | :: of 'a * 'a list
# Nil;;
- : 'a list = Nil
# 1 :: Nil;;
- : int list = :: (1, Nil)
# 1 :: 2 :: Nil;;
- : int list = :: (1, :: (2, Nil))
记得前面我们说过列表可以有两种写法,使用[1; 2; 3]这样的语法糖或者较为正式的1 :: 2 :: 3 :: []。如果你看懂了上面的'a list的定义,那么你就会明白正规定义的理由和机制了。
OCaml名称 类型定义例子 使用例子
list int list [1; 2; 3]
tuple int * string (3, "hello")
record type pair = { a : int; b : string } { a = 3; b = "hello" }
variant type foo = Int of int Int 3
| Pair of int * string
variant type sign = Positive | Zero Positive
| Negative Zero
parameterised type 'a my_list = Empty Cons (1, Cons (2, Empty))
variant | Cons of 'a * 'a my_list
接下来的函数式编程语言的一个神奇特点是能够解开数据结构并对数据进行模式匹配。这又不是一个真正的“函数式”的特点——你可能知道某些C的变种也可以这么做,不过肯定没有这么神奇。
我们现在一个实际的编程需求开始:我想要将一个简单的像n * (x + y)这样的数学表达式表示为将其中的乘号分开的形式,得到n * x + n * y。
让我们定义满足这些表达式的一个类型:
type expr = Plus of expr * expr (* 表示 a + b *)
| Minus of expr * expr (* 表示 a - b *)
| Times of expr * expr (* 表示 a * b *)
| Divide of expr * expr (* 表示 a / b *)
| Value of string (* "x", "y", "n", etc. *)
;;
表达式n * (x + y)可以写作:
Times (Value "n", Plus (Value "x", Value "y"))
现在让我们写一个可以将Times (Value "n", Plus (Value "x", Value "y"))输出为类似n * (x + y)的东西的函数。实际上,我要写两个函数,一个用于将表达式转换成漂亮的字符串,另一个将其打印出来(这样做的原因是我可能也想将同样的字符串写入到一个文件,而我又不想为了这点小事就将整个函数再重写一遍——尽可能复用代码)。
let rec to_string e =
match e with
Plus (left, right) -> "(" ^ (to_string left) ^ " + " ^ (to_string right) ^ ")"
| Minus (left, right) -> "(" ^ (to_string left) ^ " - " ^ (to_string right) ^ ")"
| Times (left, right) -> "(" ^ (to_string left) ^ " * " ^ (to_string right) ^ ")"
| Divide (left, right) -> "(" ^ (to_string left) ^ " / " ^ (to_string right) ^ ")"
| Value v -> v
;;
let print_expr e =
print_endline (to_string e);;
(注意:^操作符用于联结字符串。)
立刻对其进行测试:
# print_expr (Times (Value "n", Plus (Value "x", Value "y")));;
(n * (x + y))
模式匹配的一般形式是:
match object with
pattern -> result
| pattern -> result
...
左边的模式可以是简单的,如上面的to_string中所示,也可以是复杂的、嵌套的。下面一个例子是分解形式为n * (x + y)或者(x + y) * n的表达式的函数,为此我们将用到一个嵌套模式:
let rec multiply_out e =
match e with
Times (e1, Plus (e2, e3)) ->
Plus (Times (multiply_out e1, multiply_out e2),
Times (multiply_out e1, multiply_out e3))
| Times (Plus (e1, e2), e3) ->
Plus (Times (multiply_out e1, multiply_out e3),
Times (multiply_out e2, multiply_out e3))
| Plus (left, right) -> Plus (multiply_out left, multiply_out right)
| Minus (left, right) -> Minus (multiply_out left, multiply_out right)
| Times (left, right) -> Times (multiply_out left, multiply_out right)
| Divide (left, right) -> Divide (multiply_out left, multiply_out right)
| Value v -> Value v
;;
下面是结果:
# print_expr (multiply_out (Times (Value "n", Plus (Value "x", Value "y"))));;
((n * x) + (n * y))
multiply_out函数是如何运作的呢?第一个模式是匹配形如e1 * (e2 + e3)的表达式的Times (e1, Plus (e2, e3))。现在看一下这个模式的右边的东西,其实它就等同于(e1 * e2) + (e1 * e3)。
第二个模式的工作基本类似,除了针对形如(e1 + e2) * e3的表达式。
剩下的模式并不会改变表达式的形式,但是它们也十分关键,它们递归对子表达式调用了multiply_out函数。这确保了表达式中的所有的子表达式都能得到处理(除非你只需要分解表达式的最顶层,如果这样,你只需要将这些余下的模式替换成简单的e -> e规则)。
我们是否能逆向进行(即提取公共的子表达式)?当然可以(不过要复杂一些)!下面的版本只针对顶层表达式。你一定可以对其进行扩展来完美地处理表达式的所有层次或者更加复杂的情况:
let factorize e =
match e with
Plus (Times (e1, e2), Times (e3, e4)) when e1 = e3 -> Times (e1, Plus (e2, e4))
| Plus (Times (e1, e2), Times (e3, e4)) when e2 = e4 -> Times (Plus (e1, e3), e4)
| e -> e
;;
# factorize (Plus (Times (Value "n", Value "x"), Times (Value "n", Value "y")));;
- : expr = Times (Value "n", Plus (Value "x", Value "y"))
上面的factorize函数引入了另外两个特点。你可以对每个模式匹配添加所谓的守卫(guard)。一个守卫(条件)是指跟在when的条件表达式,它表示该模式匹配只有在模式被匹配了并且满足了在when子句中的条件时,才会发生模式匹配。
match object with
pattern [ when condition ] -> result
pattern [ when condition ] -> result
...
第二个特点是用于测试两个表达式之间“结构相等”(structural equality)的=操作符。这表示会递归进入每个表达式检测它们是否在每一个级别上都完全一致。
OCaml可以在编译期检测出你是否已经在你的模式中将所有的可能性都涉及了。如果我给上面的type expr类型定义添加一个Product变体的话:
type expr = Plus of expr * expr (* 表示a + b *)
| Minus of expr * expr (* 表示a - b *)
| Times of expr * expr (* 表示 a * b *)
| Divide of expr * expr (* 表示 a / b *)
| Product of expr list (* 表示 a * b * c * ... *)
| Value of string (* "x", "y", "n", etc. *)
;;
然后我未对to_string函数加以修改就重新编译它,结果OCaml报告了以下警告:
Warning: this pattern-matching is not exhaustive.
Here is an example of a value that is not matched:
Product _