文档 / 语言手册 / 模式匹配/解构
Edit

模式匹配/解构

ReScript 最棒的特性之一就是模式匹配了。模式匹配将 3 个极佳的特性融为一体:

  • 解构。

  • 基于数据形状的 switch

  • 穷尽性检查。

我们将在下面深入探讨各个特性。

解构

甚至 JavaScript 也有解构功能,也就是“打开”一个数据结构,提取我们想要的部分,并为其分配变量名:

ReScriptJS Output
let coordinates = (10, 20, 30)
let (x, _, _) = coordinates
Js.log(x) // 10

大部分内置数据结构都是可以解构的:

ReScriptJS Output
// 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`

你也可以在通常放置绑定的地方使用解构:

ReScriptJS Output
type result =
  | Success(string)
let displayMessage = (Success(m)) => {
  // we've directly extracted the success message
  // string by destructuring the parameter
  Js.log(m)
}
displayMessage(Success("You did it!"))

对于记录,你可以在解构时重命名字段:

ReScriptJS Output
let {name: n} = student1 // "John" assigned to `n`

理论上,你也 可以 在顶层解构数组和列表:

RES
let 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

虽然模式匹配的解构挺不错,但它并没有真正改变你对代码结构的思考方式。一种改变你对于代码的思维模式的方法是,根据数据形状来执行一些代码。

考虑如下变体:

ReScriptJS Output
type payload =
  | BadResult(int)
  | GoodResult(string)
  | NoResult

我们想要以不同的方式处理这三种情况。例如,如果值是 GoodResult(...),那就打印一条成功消息;如果值是 NoResult 那就做其他事情,等等。

在其他语言中,你最终会得到一系列难以阅读和容易出错的 if-else。在 ReScript 中,你可以使用强大的 switch 模式匹配来对值进行解构,然后根据解构的结果调用正确的代码:

ReScriptJS Output
let data = GoodResult("Product shipped!")
switch data {
| GoodResult(theMessage) =>
  Js.log("Success! " ++ theMessage)
| BadResult(errorCode) =>
  Js.log("Something's wrong. The error code is: " ++ Js.Int.toString(errorCode))
| NoResult =>
  Js.log("Bah.")
}

这种情况 message 的值会是 "Success! Product shipped!"

转眼之间,乱七八糟的 if-else 变成了一个干净的、经过编译器验证的、线性的代码列表。它可以准确地根据值的形状来执行。

复杂的示例

下面是一段真实场景的代码,在别的语言中表达相同的功能是很头疼的。考虑这个数据结构:

ReScriptJS Output
type status = Vacations(int) | Sabbatical(int) | Sick | Present
type reportCard = {passing: bool, gpa: float}
type person =
  | Teacher({
    name: string,
    age: int,
  })
  | Student({
    name: string,
    status: status,
    reportCard: reportCard,
  })

假如有以下需求:

  • 如果是老师而且名字是“Marry”或“Joe”的话,和他/她非正式的打声招呼。

  • 正式地和别的老师打招呼。

  • 如果是学生并且通过了期末考试,那就祝贺他/她获得的分数。

  • 如果学生的 gpa 为 0,并且正在休假或公休,那就显示不同的信息。

  • 对其他学生显示一条通用的信息。

ReScript 可以轻松完成任务!

ReScriptJS Output
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

下面是另一个模式匹配的例子,这次用在内联元组上。

ReScriptJS Output
type animal = Dog | Cat | Bird
let categoryId = switch (isBig, myAnimal) {
| (true, Dog) => 1
| (true, Cat) => 2
| (true, Bird) => 3
| (false, Dog | Cat) => 4
| (false, Bird) => 5
}

注意元组上的模式匹配是如何等价于二维查找表的:

isBig \ myAnimalDogCatBird
true123
false445

Fall-Through 模式

在前面的 person 的例子中展示的嵌套模式检查,也可以用在 switch 的顶层:

ReScriptJS Output
let myStatus = Vacations(10)

switch myStatus {
| Vacations(days)
| Sabbatical(days) => Js.log(`Come back in ${Js.Int.toString(days)} days!`)
| Sick
| Present => Js.log("Hey! How are you?")
}

让多种情况落入同一处理可以整理某些类型的逻辑。

忽略值的一部分

如果你有个像 Teacher(payload) 这样的值,你可能只想匹配 Teacher 部分而完全忽略 payload,你可以像这样使用 _ 通配符:

ReScriptJS Output
switch person1 {
| Teacher(_) => Js.log("Hi teacher")
| Student(_) => Js.log("Hey student")
}

_ 也可用在 switch 的顶层,它被当作默认情况:

ReScriptJS Output
switch myStatus {
| Vacations(_) => Js.log("Have fun!")
| _ => Js.log("Ok.")
}

不要滥用顶层 _,相反,最好写出所有的情况:

ReScriptJS Output
switch myStatus {
| Vacations(_) => Js.log("Have fun!")
| Sabbatical(_) | Sick | Present => Js.log("Ok.")
}

这稍微有些啰嗦,但只是一次性的编写工作。当你为 status 类型添加了一个新的变体(例如 Quarantined)并需要更新模式匹配的位置时,这将有所帮助。顶层通配符会意外地默默继续工作,可能会造成 bug。

If 子句

有时,你想检查的不仅仅是一个值的形状,你还想对它进行一些任意的检查。你可能很想这样写:

ReScriptJS Output
switch person1 {
| Teacher(_) => () // do nothing
| Student({reportCard: {gpa}}) =>
  if gpa < 0.5 {
    Js.log("What's happening")
  } else {
    Js.log("Heyo")
  }
}

switch 模式支持简写的任意 if 检查,以保持模式匹配的线性外观:

ReScriptJS Output
switch person1 {
| Teacher(_) => () // do nothing
| Student({reportCard: {gpa}}) if gpa < 0.5 =>
  Js.log("What's happening")
| Student(_) =>
  // fall-through, catch-all case
  Js.log("Heyo")
}

注意:ReScript 9.0 版本之前使用的是 when 子句而不是 if 子句。ReScript 在 9.0 版本将 when 改为 if。(when 可能仍然可用,但已被废弃)

对异常进行匹配

如果函数抛出了一个异常(稍后会介绍),除了匹配函数的正常返回值外,你还可以对 异常 进行匹配。

ReScriptJS Output
switch List.find(i => i === theItem, myItems) {
| item => Js.log(item)
| exception Not_found => Js.log("No such item found!")
}

匹配数组

ReScriptJS Output
let students = ["Jane", "Harvey", "Patrick"]
switch students {
| [] => Js.log("There are no students")
| [student1] =>
  Js.log("There's a single student here: " ++ student1)
| manyStudents =>
  // display the array of names
  Js.log2("The students are: ", manyStudents)
}

匹配列表

列表的模式匹配与数组类似,但有个提取列表尾部的额外特性(除第一个元素外的所有元素):

ReScriptJS Output
let rec printStudents = (students) => {
  switch students {
  | list{} => () // done
  | list{student} => Js.log("Last student: " ++ student)
  | list{student1, ...otherStudents} =>
    Js.log(student1)
    printStudents(otherStudents)
  }
}
printStudents(list{"Jane", "Harvey", "Patrick"})

一些小陷阱

注意: 你只能传递字面值(即具体的值)作为模式,而不能传递 let 绑定的名称或其他东西,下面的做法不能像预期的那样工作:

ReScriptJS Output
let coordinates = (10, 20, 30)
let centerY = 20
switch coordinates {
| (x, centerY, _) => Js.log(x)
}

第一次使用 ReScript 的人可能会不小心写下这段代码,认为 coordinates 的第二个值与 centerY 相同的时候会匹配 coordinates。但实际上这会被解释为在 coordinates 上进行匹配,并将元组的第二个值赋给 centerY,这不是我们想要的结果。

穷尽性检查

如果上述特性还不够,ReScript 还提供了可以说是最重要的模式匹配特性:缺失模式的编译时检查

让我们重新回到上面的例子:

ReScriptJS Output
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}!`
}

你看到我们删除了什么吗?这一次,我们省略了对 person1Teacher({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。事实上,这就是大多数可空值的处理方式:

ReScriptJS Output
let myNullableValue = Some(5)

switch myNullableValue {
| Some(v) => Js.log("value is present")
| None => Js.log("value is absent")
}

如果你不处理 None 的情况,编译器会发出警告。你的代码中不会再有 undefined 之类的 bug 了!

结论,技巧与诀窍

希望你能看到模式匹配是如何改变编写正确代码的游戏规则的,通过简明的解构语法,switch 中恰当的条件处理,以及静态的穷尽性检查。

下面是一些建议:

避免不必要地使用通配符 _,使用 _ 将绕过编译器的穷尽性检查。因此,当你在变体中增加一个新的 case 时,编译器将无法提示可能出现的错误。试着只用 _ 应对无限的可能性,例如字符串,整数,等等。

谨慎地使用 if 子句。

尽可能扁平化你的模式匹配。这样做能最大程度地消除 bug,下面是一系列代码实例,从坏到好:

ReScriptJS Output
let optionBoolToBool = opt => {
  if opt == None {
    false
  } else if opt === Some(true) {
    true
  } else {
    false
  }
}

这样做有点蠢,让我们把它变成模式匹配:

ReScriptJS Output
let optionBoolToBool = opt => {
  switch opt {
  | None => false
  | Some(a) => a ? true : false
  }
}

稍微好一点了,但仍然存在嵌套,模式匹配允许你这样做:

ReScriptJS Output
let optionBoolToBool = opt => {
  switch opt {
  | None => false
  | Some(true) => true
  | Some(false) => false
  }
}

看起来更线性了!现在,你可能很想这样做:

ReScriptJS Output
let optionBoolToBool = opt => {
  switch opt {
  | Some(true) => true
  | _ => false
  }
}

这确实更简洁了,但会破坏上面提到的穷尽性检查;尽量不要这样做。下面是最好的写法:

ReScriptJS Output
let optionBoolToBool = opt => {
  switch opt {
  | Some(trueOrFalse) => trueOrFalse
  | None => false
  }
}

现在要想在这段代码中犯错是非常困难的!当你想使用一个有许多分支的 if-else 时,最好用模式匹配来代替。它更简明,而且性能也更好