文档 / 语言手册 / 多态变体
Edit

多态变体

多态变体是变体的表亲。它们有这些不同之处:

  • 多态变体以 # 开头,构造器的名字无需首字母大写。

  • 多态变体不需要显式的类型定义,类型是由使用情况推断出来的。

  • 不同多态变体可以共享它们共同的构造器(也就是说,多态变体是“结构式”的类型,而不是“名义式”的类型)。

它们是普通变体的更便利的替代,但不应该被滥用,见本页末尾的缺点。

创建多态变体

我们为多态变体构造器提供了 3 种语法:

ReScriptJS Output
let myColor = #red
let myLabel = #"aria-hidden"
let myNumber = #7

看一下输出。多态变体对 JavaScript 互操作是很有用的。例如,你可以用它来为 JavaScript 的字符串和数字枚举建模,就像 TypeScript 一样,但不会把这种用法与普通的字符串和数字搞混。

myColor 使用的是一般语法。第二和第三种语法是为了更方便地表达字符串和数字。我们允许第二种语法,因为不这样做的话它将是无效的的语法,因为像 - 和其他符号通常是保留的。

类型声明

虽然类型声明是可选的,但你仍然可以预先声明一个多态变体类型:

RES
// Note the surrounding square brackets, and # for constructors type color = [#red | #green | #blue]

与普通变体不同,这些类型可以被内联:

ReScriptJS Output
let render = (myColor: [#red | #green | #blue]) => {
  switch myColor {
  | #blue => Js.log("Hello blue!")
  | #red
  | #green => Js.log("Hello other colors")
  }
}

注意:因为多态变体值的类型定义是推断出来的,而不是在作用域内搜索得到的,所以下面的代码片段不会报错:

ReScriptJS Output
type color = [#red | #green | #blue]

let render = myColor => {
  switch myColor {
  | #blue => Js.log("Hello blue!")
  | #green => Js.log("Hello green!")
  // works!
  | #yellow => Js.log("Hello yellow!")
  }
}

myColor 参数的类型被推断为 #red#green#yellow,而且与 color 类型无关。如果你想让 myColorcolor 类型,请在任何地方都将它标注为 myColor: color

构造器参数

它和普通变体的构造器参数类似:

ReScriptJS Output
type account = [
  | #Anonymous
  | #Instagram(string)
  | #Facebook(string, int)
]

let me: account = #Instagram("Jenny")
let him: account = #Facebook("Josh", 26)

类型合并与模式匹配

你可以在多态变体类型中使用其他多态变体类型,来创建包含所有构造体的和类型:

ReScriptJS Output
type red = [#Ruby | #Redwood | #Rust]
type blue = [#Sapphire | #Neon | #Navy]

// Contains all constructors of red and blue.
// Also adds #Papayawhip
type color = [red | blue | #Papayawhip]

let myColor: color = #Ruby

还有一些特殊的模式匹配语法,用于匹配在特定多态变体类型中定义的构造器:

ReScriptJS Output
// 续前一个例子...

switch myColor {
| #...blue => Js.log("This blue-ish")
| #...red => Js.log("This red-ish")
| other => Js.log2("Other color than red and blue: ", other)
}

这是下面代码的一个较短的版本:

RES
switch 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) }

结构共享

因为多态变体值的类型没有一个真正的来源,所以你可以写这样的代码:

ReScriptJS Output
type preferredColors = [#white | #blue]

let myColor: preferredColors = #blue

let displayColor = v => {
  switch v {
  | #red => "Hello red"
  | #green => "Hello green"
  | #white => "Hey white!"
  | #blue => "Hey blue!"
  }
}

Js.log(displayColor(myColor))

如果是普通的变体,在 displayColor(myColor) 这一行就会报错,类型系统会抱怨 myColor 的类型和 v 的类型不匹配。用多态变体就没有问题。

JavaScript 输出

多态变体十分适合用于 JavaScript 互操作!你可以将它们的值共享给 JS 代码,或者将传入的 JS 值建模为多态变体。

  • #red#"I am red 😃" 编译为 JavaScript 的 "red""I am red 😃"

  • #1 编译为 JavaScript 1

  • Instagram("Jenny") 的单参数多态变体构造器直接编译为 {NAME: "Instagram", VAL: "Jenny"}。像 #Facebook("Josh", 26) 拥有 2 个或者更多参数的构造器会编译成一个类似的对象,但是 VAL 是一个由参数组成的数组

绑定到函数

例如,假设我们想绑定到 Intl.NumberFormat,并想确保我们的用户只传递有效的语言环境(locale),我们可以这样定义一个外部绑定:

ReScriptJS Output
type t

@scope("Intl") @val
external makeNumberFormat: ([#"de-DE" | #"en-GB" | #"en-US"]) => t = "NumberFormat"

let intl = makeNumberFormat(#"de-DE")

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 值的类型:

ReScriptJS Output
type direction = [ #UP | #DOWN | #LEFT | #RIGHT ]
@module("./direction.js") external myDirection: direction = "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 变得相当难读和难用。

封闭的 [

这是最简单的多态变体定义,也是最实用的定义。像普通的变体类型一样,它精确定义了构造器集合。

RES
type rgb = [ #Red | #Green | #Blue ] let color: rgb = #Green

在上面的例子中,color 只允许是 rgb 类型中定义的三个构造器中的一个。这通常是多态变体应该被定义的方式。

如果你想要定义可扩展的类型,你需要使用下界/上界语法。

下界 [>

下界定义了多态变体类型所知道的最小构造器集合。它也被认为是一个“开放的多态变体类型”,因为它不限制任何额外的值。

下面是一个例子,展示了如何使一组最小的 basicBlueTones 可扩展为一个新的 color 类型:

RES
type 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)”。

上界 [<

上界的工作方式和下界相反:扩展的类型只能使用上界约束中声明的构造器。

这是另一个例子,只是使用了各种红色:

RES
type 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]>

强制类型转换

你可以将多态变体类型零成本地转换到 stringint

ReScriptJS Output
type company = [#Apple | #Facebook]
let theCompany: company = #Apple

let message = "Hello " ++ (theCompany :> string)

注意:为了让强制转换奏效,多态变体类型需要是封闭的;你需要对其进行标注,否则 theCompany 会被推断为 [> #Apple]

技巧和诀窍

变体 vs 多态变体

有人可能会觉得多态变体比普通变体更强大。但凡事都有取舍:

  • 由于多态变体的“结构式”天性,多态变体的类型错误可能更令人困惑。如果你不小心写了 #blur 而不是 #blue,ReScript 仍然会出错,但无法轻易指出正确的来源。普通变体有类型定义这个真相源头,所以报错不会跑偏。

  • 重构多态变体也更难。考虑一下这个:

    RES
    let myFruit = #Apple let mySecondFruit = #Apple let myCompany = #Apple
    把第一个重构成 #Orange 并不意味着我们应该重构第三个。同样的道理,编辑器插件也不能触及第二个。普通变体没有这样的问题,因为这两个值可能来自不同的变体类型定义。

  • 你可能会失去一些来自编译器的很棒的模式匹配检查:

    RES
    let myColor = #red switch myColor { | #red => Js.log("Hello red!") | #blue => Js.log("Hello blue!") }

    因为没有多态变体的定义,所以很难知道是否可以安全地删除 #blue 的 case。

在大多数使用场景下,我们更推荐使用普通变体而不是多态变体,尤其时当你在编写普通 ReScript 代码时。如果你想编写零成本的互操作绑定,或生成干净的 JS 输出,多态变体往往是更好的选择。