原文:http://2ality.com/2017/12/pattern-matching-reasonml.html
翻译:ppp
系列文章目录详见: “什么是ReasonML?”
本文中,我们介绍和模式匹配相关的三个特性:解构, switch, if 表达式。(译者注:这里说的模式不是设计模式中的模式,而是数据结构里的一个概念,用来描述一个结构的组成,所以看下文的过程中,请把设计模式里的模式暂时屏蔽。)
为了说明白模式和模式匹配,我们需要用到元组。元组记录的是由位置而非名字决定的一组数据。这些组成元组的数据称之为元件。
让我们在创建一个元组:
# (true, "abc");
- : (bool, string) = (true, "abc")
这个元组的第一个元件是一个为true的布尔值,第二个是字符串"abc"。因此,该元组的类型就是 (bool, string)
。
更多例子:
# (1.8, 5, ('a', 'b'));
- : (float, int, (char, char)) = (1.8, 5, ('a', 'b'))
在我们开始解释:解构,switch和if之前,我们需要了解他们的基础:模式匹配。
模式是一种帮助处理数据的编程原理。他们有两个目的:
这都是通过对数据做模式匹配来完成的。从语法上讲,模式是这样工作:
让我们从支持元组的简单模式开始。它们的语法如下:
元组也是一种模式。
对于元组,不能在两个不同的位置使用相同的变量名。也就是说,下面的模式是非法的:
最简单的模式没有任何变量。匹配这些模式基本上等同于对值做等于判断。让我们来看几个例子:
模式 | 数据 | 是否匹配? |
---|---|---|
3 | 3 | 是 |
1 | 3 | 否 |
(true, 12, 'p') | (true, 12, 'p') | 是 |
(false, 12, 'p') | (true, 12, 'p') | 否 |
到目前为止,我们已经用模式来确保数据具有固定的结构。下一步,我们引入变量名。这使得结构检查更加灵活,让我们可以提取数据。
一个变量名将匹配在其位置上的任何数据,同时将创建一个变量,并被赋予这个数据作为变量的值。
模式 | 数据 | 是否匹配? | 变量绑定 |
---|---|---|---|
x | 3 | yes | x = 3 |
(x, y) | (1, 4) | 是 | x = 1, y = 4 |
(1, y) | (1, 4) | 是 | y = 4 |
(2, y) | (1, 4) | 否 |
特殊变量名称_
,将不会创建变量绑定,且可以多次使用:
模式 | 数据 | 是否匹配? | 变量绑定 |
---|---|---|---|
(x, _) | (1, 4) | 是 | x = 1 |
(1, _) | (1, 4) | 是 | |
(_, _) | (1, 4) | 是 |
让我们看看另一个模式:由|分隔的两个或多个子模式形成一个"或"模式,如果其中的一个子模式匹配成功,则整个模式匹配成功。如果一个变量名存在于一个子模式中,那么它必须存在于所有子模式中。
例子:
模式 | 数据 | 是否匹配? | 变量绑定 |
---|---|---|---|
1❘2❘3 | 1 | 是 | |
1❘2❘3 | 2 | 是 | |
1❘2❘3 | 3 | 是 | |
1❘2❘3 | 4 | 否 | |
(1❘2❘3, 4) | (1, 4) | 是 | |
(1❘2❘3, 4) | (2, 4) | 是 | |
(1❘2❘3, 4) | (3, 4) | 是 | |
(x, 0) ❘ (0, x) | (1, 0) | 是 | x = 1 |
到目前为止,你需要决定是要将一段数据绑定到一个变量还是通过子模式匹配它。通过as操作符,你可以同时执行这两个操作:它的左边是一个匹配的子模式,右边是当前数据绑定到的变量的名称。
模式 | 数据 | 是否匹配? | 变量绑定 |
---|---|---|---|
7 as x | 7 | 是 | x = 7 |
(8, x) as y | (8, 5) | 是 | x = 5, y = (8, 5) |
((1,x) as y, 3) | ((1,2), 3)) | 是 | x = 2, y = (1, 2) |
ReasonML支持比元组更复杂的数据类型。例如:列表和记录。许多数据类型也通过模式匹配得到支持。请关注后续文章。
你可以通过let做模式匹配。让我们以一个元组为例:
# let tuple = (7, 4);
let tuple: (int, int) = (7, 4);
我们可以通过模式匹配的方式创建x,y两个变量,并可以分别为他们赋值7,4:
# let (x, y) = tuple;
let x: int = 7;
let y: int = 4;
用_作为变量名同样可以作为一个模式参与匹配,但不会创建变量:(译者注:可以理解为一个匿名变量)
# let (_, y) = tuple;
let y: int = 4;
# let (_, _) = tuple;
如果模式匹配失败,将会抛出一个异常:
# let (1, x) = (5, 5);
Warning: this pattern-matching is not exhaustive.
Exception: Match_failure.
我们可以从ReasonML得到两种结论:
(int, int)
元组的模式没有被匹配到。当我们了解了switch后,我们在来看看这是什么意思。通过let实现的单分支模式匹配称为"解构"。解构还可以用于函数参数。
let 匹配单一模式的数据。switch让我们可以匹配多种模式的数据。第一个匹配的结果就是整个表达式的结果。如下:
switch «value» {
| «pattern1» => «result1»
| «pattern2» => «result2»
···
}
switch按顺序进行遍历:第一个匹配成功的模式将作为switch表达式的结果。让我们看一个简单的匹配成功的例子:
let value = 1;
let result = switch value {
| 1 => "one"
| 2 => "two"
};
/* result == "one" */
如果 switch的值是一个复合值,需要用括号括起来:
let result = switch (1 + 1) {
| 1 => "one"
| 2 => "two"
};
/* result == "two" */
当你编译或在rtop中输入前面的例子时,将得到以下编译时的警告:
Warning: this pattern-matching is not exhaustive.
这意味着:switch的值为int类型,而分支中不包含该类型的所有元素。这个警告非常有用,因为它告诉我们有些情况我们可能已经漏掉了。也就是说,我们会得到潜在问题的警告。如果没有警告,switch将一定运行成功。
如果你不解决这个问题,当出现一个操作数匹配不到的分支时,就会抛出运行时异常:
let result = switch 3 {
| 1 => "one"
| 2 => "two"
};
/* Exception: Match_failure */
去掉这个警告的方法是处理这个类型的所有元素。我将简要介绍如何通过下列方式使用递归定义的类型:
例如,对于自然数,最基本的情况是0,递归的情况是1加上一个自然数。你可以通过两个分支来详尽地覆盖自然数,每个分支对应一个。在即将发布的文章中,我们将会详细描述这一过程。
现在,你只能竟可能的做到完整覆盖,预防抛出异常。当然,如果你漏了一个情况,编译器就会警告你。
如果做不到全面覆盖,可以用try catch。后面将会讲到。
如果你在上面的例子中添加一个变量作为匹配模式,那么完整覆盖的警告就会自然消失。
let result = switch 3 {
| 1 => "one"
| 2 => "two"
| x => "unknown: " ++ string_of_int(x)
};
/* result == "unknown: 3" */
我们已经创建了新的变量x
用来匹配switch
的值。这个新变量可以在分支的表达式中使用。
这种分支被称为“默认”:它放在最后,如果前面所有分支都匹配失败了,那就会匹配它。它总能成功匹配一切。在C语言中,默认分支被称为default。
如果你只想要默认分支,不管匹配的是什么,你可以使用下划线_:
let result = switch 3 {
| 1 => "one"
| 2 => "two"
| _ => "unknown"
};
/* result == "unknown" */
让我们通过switch表达式实现以下逻辑与(&&):
let tuple = (true, true);
let result = switch tuple {
| (false, false) => false
| (false, true) => false
| (true, false) => false
| (true, true) => true
};
/* result == true */
我们可以通过_和变量来简化这段代码:
let result = switch tuple {
| (false, _) => false
| (true, x) => x
};
/* result == true */
as 操作符同样可以在 switch中使用:
let tuple = (8, (5, 9));
let result = switch tuple {
| (0, _) => (0, (0, 0))
| (_, (x, _) as t) => (x, t)
};
/* result == (5, (5, 9)) */
在子模式中使用或:
switch someTuple {
| (0, 1 | 2 | 3) => "first branch"
| _ => "second branch"
};
同样可以在最上层使用:
switch "Monday" {
| "Monday"
| "Tuesday"
| "Wednesday"
| "Thursday"
| "Friday" => "weekday"
| "Saturday"
| "Sunday" => "weekend"
| day => "Illegal value: " ++ day
};
/* Result: "weekday" */
条件分支是switch
独有的特性:它们紧跟着模式后面,并用关键字when
连接。看下面的例子:
let tuple = (3, 4);
let max = switch tuple {
| (x, y) when x > y => x
| (_, y) => y
};
/* max == 4 */
第一个分支只有在条件 x > y
为true
时才会被执行。
ReasonML中if
表达式如下所示(else
可选):
if («bool») «thenExpr» else «elseExpr»;
例子:
# let bool = true;
let bool: bool = true;
# let boolStr = if (bool) "true" else "false";
let boolStr: string = "true";
加上作用域块也是可以的,下面两种表达式等价:
if (bool) "true" else "false"
if (bool) {"true"} else {"false"}
事实上,refm会把前者的表达式格式化为后面的形式。
if-else
后面的表达式必须是同一类型的:
Reason # if (true) 123 else "abc";
Error: This expression has type string
but an expression was expected of type int
你可以省略else分支,下面的两种表达式等价:
if (b) expr else ()
if (b) expr
上面代码中两个分支的类型必须相同,expr
必须是unit
类型(只能取值()
)。
例如 print_string()
返回值为 ()
那么这段代码正确:
# if (true) print_string("hello\n");
hello
- : unit = ()
相对应 ,下面的代码也会报错:
# if (true) "abc";
Error: This expression has type string
but an expression was expected of type unit
ReasonML也提供了三元运算符作为if
表达式的一个变种。下面的两种表达式等价。
if (b) expr1 else expr2
b ? expr1 : expr2
下面的两种表达式同样等价。refmt甚至会把上面的格式化成下面的表达式:
switch (b) {
| true => expr1
| false => expr2
};
b ? expr1 : expr2;
我认为三元运算符在ReasonML中并没有很大用处,三元运算符在C语言中的意义在与把if
语句转化为一个表达式,然而在ReasonML中,if
本来就是表达式。
扫码关注w3ctech微信公众号
共收到0条回复