变体
到目前为止,ReScript 的大多数数据结构对你来说可能很熟悉。本节介绍了一个极其重要的、也许是你不熟悉的数据结构:变体。
大部分的编程语言中的数据结构都是“这个和那个”,变体允许我们表达“这个或那个”。
myResponse
是一个具有 Yes
,No
和 PrettyMutch
三种情况的变体类型,它们被称为“变体构造器”(或“变体标签”)。各构造器通过 |
分隔。
注意:变体构造器名首字母必须大写。
变体需要显式定义
如果你使用的变体类型在一个不同的文件中,需要像你在记录类型做的那样将把它引入作用域:
构造器参数
变体构造器可以持有用逗号分隔的额外数据。
这里 Instagram
持有一个 string
,Facebook
持有 string
和 int
。用法:
带标签的变体 payload (内联记录)
如果一个变体 payload (payload,指变体构造器持有的额外数据)有多个字段,你可以使用类似记录的语法来标记它们,以提高可读性:
这在技术上被称为“内联记录”,并且只允许在变体构造器中使用,你不能在 ReScript 的其他地方内联一个记录类型声明。
当然,你也可以直接把一个普通的记录类型放到一个变体中:
输出结果比之前要稍微难看一些,并且性能也较差。
变体与模式匹配
请阅读后面的模式匹配/解构章节。
JavaScript 输出
一个变体值根据其类型声明,可以编译成 3 种可能的 JavaScript 输出:
若变体值是没有 payload 的构造器,它编译成一个数字。
若变体值是带 payload 的构造器,它会被编译成一个带有
TAG
字段的对象,对象的字段_0
为第一个 payload ,字段_1
为第二个 payload ,依此类推。上述情况的一个例外是类型声明只包含单个带 payload 变体构造器的变体。在这种情况下,构造器会编译成一个没有
TAG
字段的对象。带标签的变体 payload (使用前面的的内联记录技巧)会编译为带有标签名称的对象,而不是
_0
,_1
等。和之前的规则一样,该对象可能有也可能没有TAG
字段。
检查这些例子中的输出:
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")
技巧和诀窍
请小心不要把有两个参数和有一个元组参数的构造器搞混了:
变体必须有构造器
如果你来自无类型语言,你可能会这样尝试:type myType = int | string
。这在 ReScript 中是不可能的;你必须给每个分支一个构造器:type myType = Int(int) | String(string)
。前者看起来不错,但会给后续工作带来很多麻烦。
与 JavaScript 互操作
本节假设你对我们的 JavaScript 互操作有所了解。如果如果你还没有使用变体来包装 JS 函数的欲望,请跳过这部分。
相当多的 JS 库使用可以接受多种类型参数的函数。在这种情况下,将它们建模为变体是非常诱人的。例如,假设有一个 myLibrary.draw
JS 函数,它可以接受 number
或 string
,你可能很想这样绑定它:
尽量不要这样做,因为这样输出的 JS 会产生额外的噪音。你可以定义两个编译为相同 JS 调用的 external
:
ReScript 还提供了一些其他方式来做这件事。
变体类型是通过字段名找到的
请阅读记录中的这一节。变体也是一样的:一个函数不能接受两个不同变体共有的任意构造器。同样,这样的特性是存在的,它被称为多态变体(polymorphic variant)。我们会在未来介绍这个特性。
设计决策
变体,以其多种形式(多态变体,开放变体,广义代数数据类型等),可能是像 ReScript 这样的类型系统的 关键 特性。例如,前面提到的 option
变体完全消除了对可空类型的需求,这在其他语言中是 bug 的主要来源。从哲学上讲,一个问题是由许多可能的分支/条件组成的。对这些分支/条件的错误的处理就是我们所说的 bug 的主要组成。类型系统并不能神奇地消除 bug;它指出了未处理的条件并要求你覆盖它们。正确建模“这个或那个”的能力是至关重要的。
比如说,有些人想知道类型系统如何安全地消除格式不正确的 JSON 数据,避免传播到他们的程序中。类型系统自己没法做到!但是如果 parser 返回的 option
类型是 None | Some(actualData)
,那么你就必须在后续的调用点明确处理 None
的情况。这就是类型系统能做到的。
从性能上讲,变体可以潜在地极大地加快你的程序逻辑。这里有一段 JavaScript 代码:
JSlet data = 'dog'
if (data === 'dog') {
...
} else if (data === 'cat') {
...
} else if (data === 'bird') {
...
}
这里有一个线性的分支检查(O(n)
),将它与使用 ReScript 变体的例子比较:
编译器看到了这个变体,然后:
在概念上把它们变成
type animal = 0 | 1 | 2
编译
switch
到一个常数时间的跳表(O(1)
)