文档 / 语言手册 / 生成转换器与辅助函数
Edit

生成转换器与辅助函数

注意:以下装饰器:

  • 用于记录的 @deriving(jsConverter)

  • 用于记录的 @deriving({jsConverter: newType})

  • 用于多态变体的 @deriving(jsConverter)

在 9.0 及更新的版本中已经不再需要,请在侧边栏菜单中切换至旧版本查看文档。

在使用 ReScript 时,有时你可以会有以下需求:

  • 自动生成在 ReScript 内部值(如变体)和 JS 值之间转换的函数。

  • 将一个记录类型转换为一个抽象类型,它有自动生成的构造函数、访问函数和方法函数。

  • 生成其他辅助函数,例如从记录的属性名生成函数。

你可以在不同的代码生成场景中使用 @deriving 装饰器。所有选项和配置都将在本章讨论。

请注意:大量使用代码生成可能会让你的程序难以理解(因为生成的代码在源代码中不可见,而且你只需要知道装饰器生成了什么样的函数/值)。

为变体生成函数与值

在一个变体类型上使用 @deriving(accessors) 来为其构造器创建访问函数。

ReScriptJS Output
@deriving(accessors)
type action =
  | Click
  | Submit(string)
  | Cancel;

有 payload 的变体构造器会生成函数,而无 payload 构造器则会生成值(变体的内部表示)。

注意:

  • 生成的访问器名字是小写的

  • 你可以在 JavaScript 端使用这些生成的辅助函数,但请不要依赖它们的实际值!

用法

RES
let s = submit("hello"); /* gives Submit("hello") */

以下情况时很有用:

  • 当你将访问器函数作为高阶函数传递时(普通的变体构造器做不到)

  • 当你希望在 JS 端不透明地使用这些值或函数,并向你传回一个变体构造器时(因为 JS 没有这样的东西)

请注意,如果你只是想 传入 payload 到一个构造器,你不必生成函数。可以直接使用 -> 语法,例如 "test"->Submit

为记录生成字段访问器

在一个记录类型上使用 @deriving(accessors),为其记录字段名创建访问器。

ReScriptJS Output
@deriving(accessors)
type pet = {name: string}

let pets = [{name: "bob"}, {name: "bob2"}]

pets
 ->Belt.Array.map(name)
 ->Js.Array2.joinWith("&")
 ->Js.log

为 JS 的整数型枚举和 ReScript 的变体生成双向转换器

在变体类型上使用 @deriving(jsConverter) 来创建转换器函数,这些函数允许在 JS 整数型枚举和 ReScript 变量值之间来回转换。

RES
@deriving(jsConverter) type fruit = | Apple | Orange | Kiwi | Watermelon;

这个选项使 jsConverter 再次生成以下类型的函数:

RESI
let fruitToJs: fruit => int; let fruitFromJs: int => option(fruit);

fruitToJs 会将 fruit 的各个构造器映射到从 0 开始的整数,按照变体中的声明顺序递增。

fruitFromJs 的返回类型是 option,因为不是每个整数都能映射到一个构造器。

你也可以给每个构造器添加 @as(1234) 来自定义其输出。

用法

RES
@deriving(jsConverter) type fruit = | Apple | @as(10) Orange | @as(100) Kiwi | Watermelon let zero = fruitToJs(Apple) /* 0 */ switch fruitFromJs(100) { | Some(Kiwi) => Js.log("this is Kiwi") | _ => Js.log("received something wrong from the JS side") }

注意:因为这里使用了 @as,所有后续数字编号都会改变。Apple 仍然是 0,但 Orange10Kiwi100Watermelon101

提高安全性

与 JS 对象和记录类型间的转换类似,你可以通过对 @deriving(jsConverter) 使用相同的 newType 选项,来隐藏 JS 枚举是整数的事实。

RES
@deriving({jsConverter: newType}) type fruit = | Apple | @as(100) Kiwi | Watermelon;

这个选项使 @deriving(jsConverter) 生成以下类型的函数:

RESI
let fruitToJs: fruit => abs_fruit; let fruitFromJs: abs_fruit => fruit;

这次,fruitFromJs 与之前的非抽象类型的情况不同,返回值不含 option,因为不可能有不正确的值传入其中;只有 fruitToJs 可以创建 abs_fruit 类型的值!

用法

RES
@deriving({jsConverter: newType}) type fruit = | Apple | @as(100) Kiwi | Watermelon let opaqueValue = fruitToJs(Apple) @module("myJSFruits") external jsKiwi: abs_fruit = "iSwearThisIsAKiwi" let kiwi = fruitFromJs(jsKiwi) let error = fruitFromJs(100) /* nope, can't take a random int */

将记录转换为抽象记录

注意: 对于 >= v7 版本的 ReScript,推荐使用用于编译到 JS 对象的记录。 对于特定场景,这个功能仍然非常有用,但是使用体验比较糟糕。

在记录类型上使用 @deriving(abstract) 可以将该类型扩展为一个构造函数,并为其字段和方法提供一组 getter/setter 函数。

通常情况下,你只需使用 ReScript 记录来编译成相同形状的 JS 对象。但在某些特定场景中 @deriving(abstract) 还是很有用。比如你需要将一个带有 option 类型字段的记录编译到 JS 对象,但是当某个属性值为 undefined 时,这个属性不应该出现在 JS 对象中(例如,{name: "Carl", age: undefined} vs {name: "Carl"})。请查看可选标签(Optional Labels)小节来了解关于这种特殊情况的更多细节。

用法示例

RES
@deriving(abstract) type person = { name: string, age: int, job: string, }; @val external john : person = "john";

注意person 类型不是一个记录!它是一个看起来像记录的类型,使用记录的语法和类型检查。@deriving(abstract) 装饰器会把它转换为一个“抽象类型”(我们无法知道抽象类型的实际值的形状)。

创建

你不必绑定来自 JS 端的现存 person 对象,你可以从 ReScript 端创建这样的 person JS 对象。

由于 @deriving(abstract) 会将 person 记录转换为一个抽象类型,你不能按照平时的做法直接创建一个 person 记录。{name: "Joe", age: 20, job: "teacher"} 是不行的。

相反,你会使用与记录类型同名的创建函数,它由 @deriving(abstract) 隐式生成:

ReScriptJS Output
let joe = person(~name="Joe", ~age=20, ~job="teacher")

请注意,上面的例子是没有 JS 运行时开销的。

重命名字段

有时候你需要绑定的 JS 对象的某些字段名在 ReScript 中无效。例如 {type: "foo"} (type 在 ReScript 中是保留关键字)和 {"aria-checked": true}。选择一个有效的字段名,然后使用 @as 来规避这个问题:

ReScriptJS Output
@deriving(abstract)
type data = {
  @as("type") type_: string,
  @as("aria-label") ariaLabel: string,
};

let d = data(~type_="message", ~ariaLabel="hello");

可选标签

你可以在创建对象时忽略一些字段:

ReScriptJS Output
@deriving(abstract)
type person = {
  @optional name: string,
  age: int,
  job: string,
};

let joe = person(~age=20, ~job="teacher", ());

未定义的可选值不会出现在最终的 JS 对象的属性中。在上面的例子中,你可以看到 name 字段被忽略了。

注意@optional 标签将 name 字段变成了可选的。仅仅将 name 的类型标注为 option<string> 是行不通的。

注意:因为创建函数包含可选字段,所以需要在最后使用 () 表示已完成该函数的应用

访问器

同样,由于 @deriving(abstract) 隐藏了记录的实际形状,你不能使用类似 joe.age 的方式访问字段。我们通过生成 getter 和 setters 来补救这个问题。

读取

@deriving(abstract) 装饰的记录类型的每个字段都会生成一个 getter 函数。以上面的代码为例,你会得到 3 个函数:nameGetageGetjobGet。它们会接收一个 person 值,并分别返回 stringintstring

RES
let twenty = ageGet(joe)

另外,你也可以使用管道操作符(->)来获得更漂亮的访问语法:

RES
let twenty = joe->ageGet

如果喜欢更短的 getter 函数名字,我们也支持轻量级 light 设置:

RES
@deriving({abstract: light}) type person = { name: string, age: int, } let joe = person(~name="Joe", ~age=20) let joeName = name(joe)

现在 getter 函数的名字和对象字段名字相同。

写入

一个 @deriving(abstract) 值默认是不可变的。要修改这样的值,首先你需要将抽象记录的字段标记为 mutable,就像将普通记录标记为可变一样:

RES
@deriving(abstract) type person = { name: string, mutable age: int, job: string, }

随后会生成一个名为 ageSet 的 setter 函数。可以像这样使用它:

RES
let joe = person(~name="Joe", ~age=20, ~job="teacher"); ageSet(joe, 21);

也可以通过管道操作符来使用它:

RES
joe->ageSet(21)

方法

你可以在类型上附加任意的方法(事实上是 任何 类型,不仅仅是 @deriving(abstract) 记录类型)。查看“绑定到 JS 函数”一节中的对象方法来获取更多信息。

技巧和诀窍

可以利用 @deriving(abstract) 来实现更精细的访问控制。

可变性

可以在实现文件(.res)中把字段标记为可变,而在接口文件(.resi)中 隐藏 这种可变性:

RES
/* test.res */ @deriving(abstract) type cord = { @optional mutable x: int, y: int, };
RESI
/* test.resi */ @deriving(abstract) type cord = { @optional x: int, y: int, };

好了!现在,现在你可以在自己的文件中尽情使用可变性,并防止别人这样做!

隐藏创建函数

将记录标记为 private 可以禁用创建函数:

RES
@deriving(abstract) type cord = private { @optional x: int, y: int, }

访问器依然存在,只是你无法再创建该数据结构。因此,它非常适合创建 JS 对象绑定的同时防止其他人创建这个对象!

使用子模块来避免命名冲突和绑定覆盖

很多时候,你会有多个类似属性的抽象类型。由于 ReScript 会在类型被定义的作用域中展开全部的抽象 getter,setter 和创建函数,你最终会遇到值被覆盖的问题。

例如:

RES
@deriving(abstract) type person = {name: string} @deriving(abstract) type cat = { name: string, isLazy: bool, }; let person = person(~name="Alice") /* Error: This expression has type person but an expression was expected of type cat */ person->nameGet()

为了绕过这个问题,你可以使用模块将类型与其相关函数放在一起,然后通过使用局部 open 语句来使用它们:

RES
module Person = { @deriving(abstract) type t = {name: string} } module Cat = { @deriving(abstract) type t = { name: string, isLazy: bool, } } let person = Person.t(~name="Alice") let cat = Cat.t(~name="Snowball", ~isLazy=true) /* We can use each nameGet function separately now */ let shoutPersonName = { open Person person->nameGet->Js.String.toUpperCase } /* Note how we use a local `open Cat` expression to get access to Cat's nameGet function */ let whisperCatName = { open Cat cat->nameGet->Js.String.toLowerCase }

external 转换为 JS 对象创建函数

external 上使用 @obj 来创建一个函数,当调用该函数时,会得到一个 JS 对象,其字段对应于函数的参数标签。

这非常方便,因为你可以使其中一些标记的参数成为可选项,如果你不传入这些参数,输出的对象将不包括相应字段。因此,你可以用它在运行时动态创建只包含需要字段的对象。

例如,假设你需要一个这样的 JavaScript 对象:

JS
var homeRoute = { type: "GET", path: "/", action: () => console.log("Home"), // options: ... };

但是只有前三个字段是必须的。options 字段是可选的。你可以像这样声明绑定函数:

RES
@obj external route: ( ~\"type": string, ~path: string, ~action: list<string> => unit, ~options: {..}=?, unit, ) => _ = ""

注意: 由于语法限制,末尾的 = "" 只是一个虚拟占位符。当前它没有任何作用。

这个函数有 4 个带标签的参数(第 4 个是可选的),和末尾的一个无标签的参数(我们规定有可选参数的函数必须这样做)。由于在 ReScript 中 type 是一个保留的关键字,所以参数 \"type" 必须带有双引号以避免冲突

另外值得注意的是其返回类型:_,它告诉 ReScript 自动推导出 JS 对象的完整类型,省去了你手写类型的麻烦!

这个函数是这样调用的:

RES
let homeRoute = route( ~\"type"="GET", ~path="/", ~action=_ => Js.log("Home"), (), )