模式匹配/解构
ReScript 最棒的特性之一就是模式匹配了。模式匹配将 3 个极佳的特性融为一体:
解构。
基于数据形状的
switch
。穷尽性检查。
我们将在下面深入探讨各个特性。
解构
甚至 JavaScript 也有解构功能,也就是“打开”一个数据结构,提取我们想要的部分,并为其分配变量名:
大部分内置数据结构都是可以解构的:
// Record
type student = {name: string, age: int}
let student1 = {name: "John", age: 10}
let {name} = student1 // "John" assigned to `name`
// Variant
type result =
| Success(string)
let myResult = Success("You did it!")
let Success(message) = myResult // "You did it!" assigned to `message`
你也可以在通常放置绑定的地方使用解构:
对于记录,你可以在解构时重命名字段:
理论上,你也 可以 在顶层解构数组和列表:
RESlet myArray = [1, 2, 3]
let [item1, item2, _] = myArray
// 1 assigned to `item1`, 2 assigned to `item2`, 3rd item ignored
let myList = list{1, 2, 3}
let list{head, ...tail} = myList
// 1 assigned to `head`, `list{2, 3}` assigned to tail
但是我们非常不推荐解构数组(这种情况最好用元组代替)。上面的例子会报错,它们只是为了做补充说明。正如你将在下面看到的,解构数组和列表的正确方法是使用 switch
。
基于数据形状的 switch
虽然模式匹配的解构挺不错,但它并没有真正改变你对代码结构的思考方式。一种改变你对于代码的思维模式的方法是,根据数据形状来执行一些代码。
考虑如下变体:
我们想要以不同的方式处理这三种情况。例如,如果值是 GoodResult(...)
,那就打印一条成功消息;如果值是 NoResult
那就做其他事情,等等。
在其他语言中,你最终会得到一系列难以阅读和容易出错的 if-else。在 ReScript 中,你可以使用强大的 switch
模式匹配来对值进行解构,然后根据解构的结果调用正确的代码:
这种情况 message
的值会是 "Success! Product shipped!"
。
转眼之间,乱七八糟的 if-else 变成了一个干净的、经过编译器验证的、线性的代码列表。它可以准确地根据值的形状来执行。
复杂的示例
下面是一段真实场景的代码,在别的语言中表达相同的功能是很头疼的。考虑这个数据结构:
假如有以下需求:
如果是老师而且名字是“Marry”或“Joe”的话,和他/她非正式的打声招呼。
正式地和别的老师打招呼。
如果是学生并且通过了期末考试,那就祝贺他/她获得的分数。
如果学生的 gpa 为 0,并且正在休假或公休,那就显示不同的信息。
对其他学生显示一条通用的信息。
ReScript 可以轻松完成任务!
let person1 = Teacher({name: "Jane", age: 35})
let message = switch person1 {
| Teacher({name: "Mary" | "Joe"}) =>
`Hey, still going to the party on Saturday?`
| Teacher({name}) =>
// this is matched only if `name` isn't "Mary" or "Joe"
`Hello ${name}.`
| Student({name, reportCard: {passing: true, gpa}}) =>
`Congrats ${name}, nice GPA of ${Js.Float.toString(gpa)} you got there!`
| Student({
reportCard: {gpa: 0.0},
status: Vacations(daysLeft) | Sabbatical(daysLeft)
}) =>
`Come back in ${Js.Int.toString(daysLeft)} days!`
| Student({status: Sick}) =>
`How are you feeling?`
| Student({name}) =>
`Good luck next semester ${name}!`
}
注意 我们是如何:
简洁的深入到内部值。
使用嵌套模式检查,即
"Mary" | "Joe"
和Vacations | Sabbatical
。从后一种情况中提取
dayLeft
数字。将问候语绑定给
message
。
下面是另一个模式匹配的例子,这次用在内联元组上。
注意元组上的模式匹配是如何等价于二维查找表的:
isBig \ myAnimal | Dog | Cat | Bird |
---|---|---|---|
true | 1 | 2 | 3 |
false | 4 | 4 | 5 |
Fall-Through 模式
在前面的 person
的例子中展示的嵌套模式检查,也可以用在 switch
的顶层:
让多种情况落入同一处理可以整理某些类型的逻辑。
忽略值的一部分
如果你有个像 Teacher(payload)
这样的值,你可能只想匹配 Teacher
部分而完全忽略 payload
,你可以像这样使用 _
通配符:
_
也可用在 switch
的顶层,它被当作默认情况:
不要滥用顶层 _
,相反,最好写出所有的情况:
这稍微有些啰嗦,但只是一次性的编写工作。当你为 status
类型添加了一个新的变体(例如 Quarantined
)并需要更新模式匹配的位置时,这将有所帮助。顶层通配符会意外地默默继续工作,可能会造成 bug。
If 子句
有时,你想检查的不仅仅是一个值的形状,你还想对它进行一些任意的检查。你可能很想这样写:
switch
模式支持简写的任意 if
检查,以保持模式匹配的线性外观:
注意:ReScript 9.0 版本之前使用的是 when
子句而不是 if
子句。ReScript 在 9.0 版本将 when
改为 if
。(when
可能仍然可用,但已被废弃)
对异常进行匹配
如果函数抛出了一个异常(稍后会介绍),除了匹配函数的正常返回值外,你还可以对 异常 进行匹配。
匹配数组
匹配列表
列表的模式匹配与数组类似,但有个提取列表尾部的额外特性(除第一个元素外的所有元素):
一些小陷阱
注意: 你只能传递字面值(即具体的值)作为模式,而不能传递 let 绑定的名称或其他东西,下面的做法不能像预期的那样工作:
第一次使用 ReScript 的人可能会不小心写下这段代码,认为 coordinates
的第二个值与 centerY
相同的时候会匹配 coordinates
。但实际上这会被解释为在 coordinates
上进行匹配,并将元组的第二个值赋给 centerY
,这不是我们想要的结果。
穷尽性检查
如果上述特性还不够,ReScript 还提供了可以说是最重要的模式匹配特性:缺失模式的编译时检查。
让我们重新回到上面的例子:
let message = switch person1 {
| Teacher({name: "Mary" | "Joe"}) =>
`Hey, still going to the party on Saturday?`
| Student({name, reportCard: {passing: true, gpa}}) =>
`Congrats ${name}, nice GPA of ${Js.Float.toString(gpa)} you got there!`
| Student({
reportCard: {gpa: 0.0},
status: Vacations(daysLeft) | Sabbatical(daysLeft)
}) =>
`Come back in ${Js.Int.toString(daysLeft)} days!`
| Student({status: Sick}) =>
`How are you feeling?`
| Student({name}) =>
`Good luck next semester ${name}!`
}
你看到我们删除了什么吗?这一次,我们省略了对 person1
是 Teacher({name})
而名字不是 Mary 或 Joe 的情况的处理。
未能处理一个值的每一种情况可能是 bug 主要来源。当你重构别人编写的代码时,这种事经常发生。幸运的是,在 ReScript 中遇到这种情况时,编译器会告诉你:
Warning 8: this pattern-matching is not exhaustive. Here is an example of a value that is not matched: Some({name: ""})
砰!在你运行代码之前,你就已经抹去了一整类的 bug。事实上,这就是大多数可空值的处理方式:
如果你不处理 None
的情况,编译器会发出警告。你的代码中不会再有 undefined
之类的 bug 了!
结论,技巧与诀窍
希望你能看到模式匹配是如何改变编写正确代码的游戏规则的,通过简明的解构语法,switch
中恰当的条件处理,以及静态的穷尽性检查。
下面是一些建议:
避免不必要地使用通配符 _
,使用 _
将绕过编译器的穷尽性检查。因此,当你在变体中增加一个新的 case 时,编译器将无法提示可能出现的错误。试着只用 _
应对无限的可能性,例如字符串,整数,等等。
谨慎地使用 if
子句。
尽可能扁平化你的模式匹配。这样做能最大程度地消除 bug,下面是一系列代码实例,从坏到好:
这样做有点蠢,让我们把它变成模式匹配:
稍微好一点了,但仍然存在嵌套,模式匹配允许你这样做:
看起来更线性了!现在,你可能很想这样做:
这确实更简洁了,但会破坏上面提到的穷尽性检查;尽量不要这样做。下面是最好的写法:
现在要想在这段代码中犯错是非常困难的!当你想使用一个有许多分支的 if-else 时,最好用模式匹配来代替。它更简明,而且性能也更好。