w3ctech

[ReasonML] - Pattern matching: destructuring, switch, if expressions - 模式匹配:解构,switch,if表达式


原文:http://2ality.com/2017/12/pattern-matching-reasonml.html

翻译:ppp

系列文章目录详见: “什么是ReasonML?


本文中,我们介绍和模式匹配相关的三个特性:解构, switch, if 表达式。(译者注:这里说的模式不是设计模式中的模式,而是数据结构里的一个概念,用来描述一个结构的组成,所以看下文的过程中,请把设计模式里的模式暂时屏蔽。)

1. 准备知识: 元组

为了说明白模式和模式匹配,我们需要用到元组。元组记录的是由位置而非名字决定的一组数据。这些组成元组的数据称之为元件。

让我们在创建一个元组:

# (true, "abc");
- : (bool, string) = (true, "abc")

这个元组的第一个元件是一个为true的布尔值,第二个是字符串"abc"。因此,该元组的类型就是 (bool, string)

更多例子:

# (1.8, 5, ('a', 'b'));
- : (float, int, (char, char)) = (1.8, 5, ('a', 'b'))

2. 模式匹配

在我们开始解释:解构,switch和if之前,我们需要了解他们的基础:模式匹配。

模式是一种帮助处理数据的编程原理。他们有两个目的:

  • 检查数据的结构。
  • 提取数据的内容。

这都是通过对数据做模式匹配来完成的。从语法上讲,模式是这样工作:

  • ReasonML有创建数据的语法。例如:元组是通过用逗号分隔数据并将数据放入括号来创建的。
  • ReasonML有处理数据的语法。模式的语法反映了创建数据的语法。

让我们从支持元组的简单模式开始。它们的语法如下:

  • 变量名是一个模式。
    • 例子:x,y,foo
  • 字面量是一种模式。
    • 例子:123, "abc", true
  • 元组也是一种模式。

    • 例子:(8,x), (3.2,"abc",true), (1, (9, foo))

    对于元组,不能在两个不同的位置使用相同的变量名。也就是说,下面的模式是非法的:

    • (x, x)

2.1 等于判断

最简单的模式没有任何变量。匹配这些模式基本上等同于对值做等于判断。让我们来看几个例子:

模式 数据 是否匹配?
3 3
1 3
(true, 12, 'p') (true, 12, 'p')
(false, 12, 'p') (true, 12, 'p')

到目前为止,我们已经用模式来确保数据具有固定的结构。下一步,我们引入变量名。这使得结构检查更加灵活,让我们可以提取数据。

2.2 (模式中的)变量名

一个变量名将匹配在其位置上的任何数据,同时将创建一个变量,并被赋予这个数据作为变量的值。

模式 数据 是否匹配? 变量绑定
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)

2.3 (模式中的)或

让我们看看另一个模式:由|分隔的两个或多个子模式形成一个"或"模式,如果其中的一个子模式匹配成功,则整个模式匹配成功。如果一个变量名存在于一个子模式中,那么它必须存在于所有子模式中。

例子:

模式 数据 是否匹配? 变量绑定
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

2.4 as 操作符: 绑定和匹配同时完成

到目前为止,你需要决定是要将一段数据绑定到一个变量还是通过子模式匹配它。通过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)

2.5 还有那个多方式可以创建模式

ReasonML支持比元组更复杂的数据类型。例如:列表和记录。许多数据类型也通过模式匹配得到支持。请关注后续文章。

3. 通过let的模式匹配(解构)

你可以通过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实现的单分支模式匹配称为"解构"。解构还可以用于函数参数。

4. switch

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" */

4.1 完整性覆盖警告

当你编译或在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。后面将会讲到。

4.2 变量模式

如果你在上面的例子中添加一个变量作为匹配模式,那么完整覆盖的警告就会自然消失。

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" */

4.3 元组模式

让我们通过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 */

4.4 as 操作符

as 操作符同样可以在 switch中使用:

let tuple = (8, (5, 9));
let result = switch tuple {
| (0, _) => (0, (0, 0))
| (_, (x, _) as t) => (x, t)
};
/* result == (5, (5, 9)) */

4.5 或

在子模式中使用或:

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" */

4.6 条件分支

条件分支是switch独有的特性:它们紧跟着模式后面,并用关键字when连接。看下面的例子:

let tuple = (3, 4);
let max = switch tuple {
| (x, y) when x > y => x
| (_, y) => y
};
/* max == 4 */

第一个分支只有在条件 x > ytrue时才会被执行。

5. if 表达式

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

5.1 省略else分支

你可以省略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

6. 三元运算符 (_?_:_)

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微信

扫码关注w3ctech微信公众号

共收到0条回复