多态变体
多态变体是变体的表亲。它们有这些不同之处:
多态变体以
#
开头,构造器的名字无需首字母大写。多态变体不需要显式的类型定义,类型是由使用情况推断出来的。
不同多态变体可以共享它们共同的构造器(也就是说,多态变体是“结构式”的类型,而不是“名义式”的类型)。
它们是普通变体的更便利的替代,但不应该被滥用,见本页末尾的缺点。
创建多态变体
我们为多态变体构造器提供了 3 种语法:
看一下输出。多态变体对 JavaScript 互操作是很有用的。例如,你可以用它来为 JavaScript 的字符串和数字枚举建模,就像 TypeScript 一样,但不会把这种用法与普通的字符串和数字搞混。
myColor
使用的是一般语法。第二和第三种语法是为了更方便地表达字符串和数字。我们允许第二种语法,因为不这样做的话它将是无效的的语法,因为像 -
和其他符号通常是保留的。
类型声明
虽然类型声明是可选的,但你仍然可以预先声明一个多态变体类型:
RES// Note the surrounding square brackets, and # for constructors
type color = [#red | #green | #blue]
与普通变体不同,这些类型可以被内联:
注意:因为多态变体值的类型定义是推断出来的,而不是在作用域内搜索得到的,所以下面的代码片段不会报错:
myColor
参数的类型被推断为 #red
,#green
或 #yellow
,而且与 color
类型无关。如果你想让 myColor
是 color
类型,请在任何地方都将它标注为 myColor: color
。
构造器参数
它和普通变体的构造器参数类似:
类型合并与模式匹配
你可以在多态变体类型中使用其他多态变体类型,来创建包含所有构造体的和类型:
还有一些特殊的模式匹配语法,用于匹配在特定多态变体类型中定义的构造器:
这是下面代码的一个较短的版本:
RESswitch myColor {
| #Sapphire | #Neon | #Navy => Js.log("This is blue-ish")
| #Ruby | #Redwood | #Rust => Js.log("This is red-ish")
| other => Js.log2("Other color than red and blue: ", other)
}
结构共享
因为多态变体值的类型没有一个真正的来源,所以你可以写这样的代码:
如果是普通的变体,在 displayColor(myColor)
这一行就会报错,类型系统会抱怨 myColor
的类型和 v
的类型不匹配。用多态变体就没有问题。
JavaScript 输出
多态变体十分适合用于 JavaScript 互操作!你可以将它们的值共享给 JS 代码,或者将传入的 JS 值建模为多态变体。
#red
和#"I am red 😃"
编译为 JavaScript 的"red"
和"I am red 😃"
#1
编译为 JavaScript1
像
Instagram("Jenny")
的单参数多态变体构造器直接编译为{NAME: "Instagram", VAL: "Jenny"}
。像#Facebook("Josh", 26)
拥有 2 个或者更多参数的构造器会编译成一个类似的对象,但是VAL
是一个由参数组成的数组
绑定到函数
例如,假设我们想绑定到 Intl.NumberFormat
,并想确保我们的用户只传递有效的语言环境(locale),我们可以这样定义一个外部绑定:
JS 输出与手写 JS 相同,但如果我们不小心写了 makeNumberFormat(#"de-DR")
,我们也会遇到类型错误。
更多关于多态变体互操作的高级用例参见绑定到 JS 函数。
绑定到字符串枚举
假设我们有一个 TypeScript 模块,表达以下枚举导出:
JS// direction.js
enum Direction {
Up = "UP",
Down = "DOWN",
Left = "LEFT",
Right = "RIGHT",
}
export const myDirection = Direction.Up
对于这个特别的例子,我们也可以内联多态变体的类型定义来设计导入的 myDirection
值的类型:
再次提醒:因为我们使用的是多态变体,我们的 JS 输出是零开销的!没有增加任何额外的代码。
额外的类型约束
我们之前看到的多态变体类型标注是常规的“封闭”类型。然而,还有一种方式可以表达“我至少想要这些构造器”(下界)和“我最多想要这些构造器”(上界):
RES// Only #Red allowed. Closed.
let basic: [#Red] = #Red
// May contain #Red, or any other value. Open
// here, foreground will actually be inferred as [> #Red | #Green]
let foreground: [> #Red] = #Green
// The value must be, at most, one of #Red or #Blue
// Only #Red and #Blue are valid values
let background: [< #Red | #Blue] = #Red
注意:我们补充这些信息是出于教学目的。大多数情况下,你不会想使用这些东西,因为它会让你的 API 变得相当难读和难用。
封闭的 [
这是最简单的多态变体定义,也是最实用的定义。像普通的变体类型一样,它精确定义了构造器集合。
REStype rgb = [ #Red | #Green | #Blue ]
let color: rgb = #Green
在上面的例子中,color
只允许是 rgb
类型中定义的三个构造器中的一个。这通常是多态变体应该被定义的方式。
如果你想要定义可扩展的类型,你需要使用下界/上界语法。
下界 [>
下界定义了多态变体类型所知道的最小构造器集合。它也被认为是一个“开放的多态变体类型”,因为它不限制任何额外的值。
下面是一个例子,展示了如何使一组最小的 basicBlueTones
可扩展为一个新的 color
类型:
REStype basicBlueTone<'a> = [> #Blue | #DeepBlue | #LightBlue ] as 'a
type color = basicBlueTone<[#Blue | #DeepBlue | #LightBlue | #Purple]>
let color: color = #Purple
// This will fail due to missing minimum constructors:
type notWorking = basicBlueTone<[#Purple]>
这里编译器会强制用户在尝试扩展 basicBlueTone<'a>
时定义最小的构造器集合 #Blue | #DeepBlue | #LightBlue
。
注意:因为我们想要定义一个可扩展的多态变体,我们需要提供一个类型占位符 <'a>
,同时在类型声明后添加 as 'a
。这样做的意思是:“给定类型 'a
受制于 basicBlueTone
中定义的最小构造器集合(#Blue | #DeepBlue | #LightBlue
)”。
上界 [<
上界的工作方式和下界相反:扩展的类型只能使用上界约束中声明的构造器。
这是另一个例子,只是使用了各种红色:
REStype validRed<'a> = [< #Fire | #Crimson | #Ash] as 'a
type myReds = validRed<[#Ash]>
// This will fail due to unlisted constructor not defined by the lower bound
type notWorking = validRed<[#Purple]>
强制类型转换
你可以将多态变体类型零成本地转换到 string
或 int
:
注意:为了让强制转换奏效,多态变体类型需要是封闭的;你需要对其进行标注,否则 theCompany
会被推断为 [> #Apple]
。
技巧和诀窍
变体 vs 多态变体
有人可能会觉得多态变体比普通变体更强大。但凡事都有取舍:
由于多态变体的“结构式”天性,多态变体的类型错误可能更令人困惑。如果你不小心写了
#blur
而不是#blue
,ReScript 仍然会出错,但无法轻易指出正确的来源。普通变体有类型定义这个真相源头,所以报错不会跑偏。
重构多态变体也更难。考虑一下这个:
把第一个重构成RESlet myFruit = #Apple let mySecondFruit = #Apple let myCompany = #Apple
#Orange
并不意味着我们应该重构第三个。同样的道理,编辑器插件也不能触及第二个。普通变体没有这样的问题,因为这两个值可能来自不同的变体类型定义。
你可能会失去一些来自编译器的很棒的模式匹配检查:
RESlet myColor = #red switch myColor { | #red => Js.log("Hello red!") | #blue => Js.log("Hello blue!") }
因为没有多态变体的定义,所以很难知道是否可以安全地删除
#blue
的 case。
在大多数使用场景下,我们更推荐使用普通变体而不是多态变体,尤其时当你在编写普通 ReScript 代码时。如果你想编写零成本的互操作绑定,或生成干净的 JS 输出,多态变体往往是更好的选择。