变体

到目前为止,ReScript 的大多数数据结构对你来说可能很熟悉。本节介绍了一个极其重要的、也许是你不熟悉的数据结构:变体。

大部分的编程语言中的数据结构都是“这个那个”,变体允许我们表达“这个那个”。

ReScriptJS Output
type myResponse =
  | Yes
  | No
  | PrettyMuch

let areYouCrushingIt = Yes

myResponse 是一个具有 YesNoPrettyMutch 三种情况的变体类型,它们被称为“变体构造器”(或“变体标签”)。各构造器通过 | 分隔。

注意:变体构造器名首字母必须大写。

变体需要显式定义

如果你使用的变体类型在一个不同的文件中,需要像你在记录类型做的那样将把它引入作用域:

ReScriptJS Output
// Zoo.res
type animal = Dog | Cat | Bird
ReScriptJS Output
// Example.res
let pet: Zoo.animal = Dog // preferred
// or
let pet2 = Zoo.Dog

构造器参数

变体构造器可以持有用逗号分隔的额外数据。

ReScriptJS Output
type account =
  | None
  | Instagram(string)
  | Facebook(string, int)

这里 Instagram 持有一个 stringFacebook 持有 stringint。用法:

ReScriptJS Output
let myAccount = Facebook("Josh", 26)
let friendAccount = Instagram("Jenny")

带标签的变体 payload (内联记录)

如果一个变体 payload (payload,指变体构造器持有的额外数据)有多个字段,你可以使用类似记录的语法来标记它们,以提高可读性:

ReScriptJS Output
type user =
  | Number(int)
  | Id({name: string, password: string})

let me = Id({name: "Joe", password: "123"})

这在技术上被称为“内联记录”,并且只允许在变体构造器中使用,你不能在 ReScript 的其他地方内联一个记录类型声明。

当然,你也可以直接把一个普通的记录类型放到一个变体中:

ReScriptJS Output
type u = {name: string, password: string}
type user =
  | Number(int)
  | Id(u)

let me = Id({name: "Joe", password: "123"})

输出结果比之前要稍微难看一些,并且性能也较差。

变体与模式匹配

请阅读后面的模式匹配/解构章节。

JavaScript 输出

一个变体值根据其类型声明,可以编译成 3 种可能的 JavaScript 输出:

  • 若变体值是没有 payload 的构造器,它编译成一个数字。

  • 若变体值是带 payload 的构造器,它会被编译成一个带有 TAG 字段的对象,对象的字段 _0 为第一个 payload ,字段 _1 为第二个 payload ,依此类推。

  • 上述情况的一个例外是类型声明只包含单个带 payload 变体构造器的变体。在这种情况下,构造器会编译成一个没有 TAG 字段的对象。

  • 带标签的变体 payload (使用前面的的内联记录技巧)会编译为带有标签名称的对象,而不是 _0_1 等。和之前的规则一样,该对象可能有也可能没有 TAG 字段。

检查这些例子中的输出:

ReScriptJS Output
type greeting = Hello | Goodbye
let g1 = Hello
let g2 = Goodbye

type outcome = Good | Error(string)
let o1 = Good
let o2 = Error("oops!")

type family = Child | Mom(int, string) | Dad (int)
let f1 = Child
let f2 = Mom(30, "Jane")
let f3 = Dad(32)

type person = Teacher | Student({gpa: float})
let p1 = Teacher
let p2 = Student({gpa: 99.5})

type s = {score: float}
type adventurer = Warrior(s) | Wizard(string)
let a1 = Warrior({score: 10.5})
let a2 = Wizard("Joe")

技巧和诀窍

请小心不要把有两个参数和有一个元组参数的构造器搞混了:

ReScriptJS Output
type account =
  | Facebook(string, int) // 2 arguments
type account2 =
  | Instagram((string, int)) // 1 argument - happens to be a 2-tuple

变体必须有构造器

如果你来自无类型语言,你可能会这样尝试:type myType = int | string。这在 ReScript 中是不可能的;你必须给每个分支一个构造器:type myType = Int(int) | String(string)。前者看起来不错,但会给后续工作带来很多麻烦。

与 JavaScript 互操作

本节假设你对我们的 JavaScript 互操作有所了解。如果如果你还没有使用变体来包装 JS 函数的欲望,请跳过这部分。

相当多的 JS 库使用可以接受多种类型参数的函数。在这种情况下,将它们建模为变体是非常诱人的。例如,假设有一个 myLibrary.draw JS 函数,它可以接受 numberstring,你可能很想这样绑定它:

ReScriptJS Output
// reserved for internal usage
@module("myLibrary") external draw : 'a => unit = "draw"

type animal =
  | MyFloat(float)
  | MyString(string)

let betterDraw = (animal) =>
  switch animal {
  | MyFloat(f) => draw(f)
  | MyString(s) => draw(s)
  }

betterDraw(MyFloat(1.5))

尽量不要这样做,因为这样输出的 JS 会产生额外的噪音。你可以定义两个编译为相同 JS 调用的 external

ReScriptJS Output
@module("myLibrary") external drawFloat: float => unit = "draw"
@module("myLibrary") external drawString: string => unit = "draw"

ReScript 还提供了一些其他方式来做这件事。

变体类型是通过字段名找到的

请阅读记录中的这一节。变体也是一样的:一个函数不能接受两个不同变体共有的任意构造器。同样,这样的特性是存在的,它被称为多态变体(polymorphic variant)。我们会在未来介绍这个特性。

设计决策

变体,以其多种形式(多态变体,开放变体,广义代数数据类型等),可能是像 ReScript 这样的类型系统的 关键 特性。例如,前面提到的 option 变体完全消除了对可空类型的需求,这在其他语言中是 bug 的主要来源。从哲学上讲,一个问题是由许多可能的分支/条件组成的。对这些分支/条件的错误的处理就是我们所说的 bug 的主要组成。类型系统并不能神奇地消除 bug;它指出了未处理的条件并要求你覆盖它们。正确建模“这个或那个”的能力是至关重要的。

比如说,有些人想知道类型系统如何安全地消除格式不正确的 JSON 数据,避免传播到他们的程序中。类型系统自己没法做到!但是如果 parser 返回的 option 类型是 None | Some(actualData),那么你就必须在后续的调用点明确处理 None 的情况。这就是类型系统能做到的。

从性能上讲,变体可以潜在地极大地加快你的程序逻辑。这里有一段 JavaScript 代码:

JS
let data = 'dog' if (data === 'dog') { ... } else if (data === 'cat') { ... } else if (data === 'bird') { ... }

这里有一个线性的分支检查(O(n)),将它与使用 ReScript 变体的例子比较:

ReScriptJS Output
type animal = Dog | Cat | Bird
let data = Dog
switch data {
| Dog => Js.log("Wof")
| Cat => Js.log("Meow")
| Bird => Js.log("Kashiiin")
}

编译器看到了这个变体,然后:

  1. 在概念上把它们变成 type animal = 0 | 1 | 2

  2. 编译 switch 到一个常数时间的跳表(O(1)