记录

记录和 JavaScript 对象很像,但:

  • 默认是不可变的

  • 有固定的字段(不可扩展)

类型声明

记录需要强制性的类型声明:

ReScriptJS Output
type person = {
  age: int,
  name: string,
}

创建记录

要创建一个 person 记录(上面已经声明):

ReScriptJS Output
let me = {
  age: 5,
  name: "Big ReScript"
}

当你创建新的记录值时,ReScript 试图找到符合值的形状的记录类型声明。因此,这里的 me 值被推断为 person 类型。

通过查找 me 值的上方可以找到该类型。如果类型存在于另一个文件或模块中,你需要明确指出它是哪个文件或模块:

ReScriptJS Output
// School.res
type person = {age: int, name: string}
ReScriptJS Output
// Example.res

let me: School.person = {age: 20, name: "Big ReScript"}
/* or */
let me2 = {School.age: 20, name: "Big ReScript"}

meme2 中都可以找到 School 的记录定义。建议使用带有常规类型标注的 me

字段访问

使用熟悉的点号:

ReScriptJS Output
let name = me.name

不可变更新

可以用 ... 展开操作符从旧记录中创建新记录。原始记录不会被修改。

ReScriptJS Output
let meNextYear = {...me, age: me.age + 1}

注意: 展开不能向记录值添加新字段,记录的形状是由其类型固定的。

可变更新

记录的字段可以是可变的,这允许你用 = 操作符高效地原地更新这些字段。

ReScriptJS Output
type person = {
  name: string,
  mutable age: int
}

let baby = {name: "Baby ReScript", age: 5}
baby.age = baby.age + 1 // `baby.age` is now 6. Happy birthday!

在类型声明中没有标记为 mutable 的字段不能被修改。

JavaScript 输出

ReScript 记录编译为直接的 JavaScript 对象;查看上面的各种 JS 输出标签。

可选记录字段

ReScript 在 v10 引入了可选记录字段,这意味着你可以定义在创建记录时可以省略的字段。它看起来像这样:

ReScriptJS Output
type person = {
  age: int,
  name?: string
}

注意 name 有一个 ? 后缀,这意味着字段本身是 可选的

创建记录

在创建一个记录时,你可以省略任何可选字段,未设置的可选字段值默认为 None

ReScriptJS Output
type person = {
  age: int,
  name?: string
}

let me = {
  age: 5,
  name: "Big ReScript"
}

let friend = {
  age: 7
}

这对模式匹配有影响,我们将很快展开讨论。

不可变更新

通过不可变更新来更新一个可选字段,可以让你设置该字段的值,而不用关心它是否可选。

ReScriptJS Output
type person = {
  age: int,
  name?: string
}

let me = {
  age: 123,
  name: "Hello"
}

let withoutName = {
  ...me,
  name: "New Name"
}

然而,如果你想将可选字段设置为 option 值,你需要在值前加上 ?

ReScriptJS Output
type person = {
  age: int,
  name?: string
}

let me = {
  age: 123,
  name: "Hello"
}

let maybeName = Some("My Name")

let withoutName = {
  ...me,
  name: ?maybeName
}

你可以使用同样的机制将一个可选字段的值设置为 ?None,从而消去字段的值。

可选字段的模式匹配

模式匹配是 ReScript 最重要的特性之一,当你处理可选字段时,有两点需要注意。

当直接对值进行匹配时,它是一个 option。例如:

ReScriptJS Output
type person = {
  age: int,
  name?: string,
}

let me = {
  age: 123,
  name: "Hello",
}

let isRescript = switch me.name {
| Some("ReScript") => true
| Some(_) | None => false
}

但是,当把字段作为一般记录结构的一部分进行匹配时,它被视为基本的、非可选的值:

ReScriptJS Output
type person = {
  age: int,
  name?: string,
}

let me = {
  age: 123,
  name: "Hello",
}

let isRescript = switch me {
| {name: "ReScript"} => true
| _ => false
}

有时你确实想知道这个字段是否被设置了。你可以通过在你的匹配选项前加上 ? 来告知模式匹配引擎这一点,就像这样:

ReScriptJS Output
type person = {
  age: int,
  name?: string,
}

let me = {
  age: 123,
  name: "Hello",
}

let nameWasSet = switch me {
| {name: ?None} => false
| {name: ?Some(_)} => true
}

技巧和诀窍

记录类型是通过字段名找到的

对于记录,你不能说“我想让这个函数接受任何记录类型,只要它们有 age 这个字段”。下面的方法不会按照预期工作

ReScriptJS Output
type person = {age: int, name: string}
type monster = {age: int, hasTentacles: bool}

let getAge = (entity) => entity.age

相反,getAge 将推断出参数 entity 必须是 monster 类型,这是与字段 age 最接近的记录类型。下面代码的最后一行会报错:

RES
let kraken = {age: 9999, hasTentacles: true} let me = {age: 5, name: "Baby ReScript"} getAge(kraken) getAge(me) // type error!

类型系统会抱怨 me 是一个 person,而 getAge 只对 monster 有效。如果你需要这样的能力,请使用 ReScript 对象,参考这里

记录的可选字段可用于绑定

很多 JavaScript API 往往有庞大的配置对象,如果用记录来建模这些对象可能有点令人讨厌,因为你总是需要在创建记录时指定所有记录字段。

v10 引入可选记录字段就是为了帮助解决这个问题。可选字段让你避免指定所有字段,而让你只指定你关心的字段。对于绑定,和其他具有大型配置对象的 API 来说,这是人体工程学上的重大改进。

设计决策

在阅读了前面几节的约束条件后,如果你是动态语言背景的人,你可能会想,为什么首先要用记录而不是直接用对象?因为记录需要明确的类型化,而且不允许将具有相同字段名的不同记录传递给同一个函数,等等。

  1. 事实是,在你的应用程序中,大多数时候你的数据的形状实际上是固定的,如果不是,它表示为变体(接下来介绍) + 记录的组合可能会更好。

  2. 由于记录类型是通过找到那个单一的显式类型声明来解决的(我们称之为“名义类型(nominal typing)”),类型错误信息最终会比对等的类型(“结构式类型(structural typing)”,如元组)更好。这使得重构更容易;改变一个记录类型的字段天然允许编译器知道它仍然是同一个记录,只是在某些地方被误用。否则,在结构化类型下,可能会很难分辨是定义的地方还是使用的地方出了问题。