生成转换器与辅助函数
注意:以下装饰器:
用于记录的
@deriving(jsConverter)
用于记录的
@deriving({jsConverter: newType})
用于多态变体的
@deriving(jsConverter)
在 9.0 及更新的版本中已经不再需要,请在侧边栏菜单中切换至旧版本查看文档。
在使用 ReScript 时,有时你可以会有以下需求:
自动生成在 ReScript 内部值(如变体)和 JS 值之间转换的函数。
将一个记录类型转换为一个抽象类型,它有自动生成的构造函数、访问函数和方法函数。
生成其他辅助函数,例如从记录的属性名生成函数。
你可以在不同的代码生成场景中使用 @deriving
装饰器。所有选项和配置都将在本章讨论。
请注意:大量使用代码生成可能会让你的程序难以理解(因为生成的代码在源代码中不可见,而且你只需要知道装饰器生成了什么样的函数/值)。
为变体生成函数与值
在一个变体类型上使用 @deriving(accessors)
来为其构造器创建访问函数。
有 payload 的变体构造器会生成函数,而无 payload 构造器则会生成值(变体的内部表示)。
注意:
生成的访问器名字是小写的
你可以在 JavaScript 端使用这些生成的辅助函数,但请不要依赖它们的实际值!
用法
RESlet s = submit("hello"); /* gives Submit("hello") */
以下情况时很有用:
当你将访问器函数作为高阶函数传递时(普通的变体构造器做不到)
当你希望在 JS 端不透明地使用这些值或函数,并向你传回一个变体构造器时(因为 JS 没有这样的东西)
请注意,如果你只是想 传入 payload 到一个构造器,你不必生成函数。可以直接使用 ->
语法,例如 "test"->Submit
。
为记录生成字段访问器
在一个记录类型上使用 @deriving(accessors)
,为其记录字段名创建访问器。
为 JS 的整数型枚举和 ReScript 的变体生成双向转换器
在变体类型上使用 @deriving(jsConverter)
来创建转换器函数,这些函数允许在 JS 整数型枚举和 ReScript 变量值之间来回转换。
RES@deriving(jsConverter)
type fruit =
| Apple
| Orange
| Kiwi
| Watermelon;
这个选项使 jsConverter
再次生成以下类型的函数:
RESIlet 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
,但 Orange
是 10
,Kiwi
是 100
,Watermelon
是 101
!
提高安全性
与 JS 对象和记录类型间的转换类似,你可以通过对 @deriving(jsConverter)
使用相同的 newType
选项,来隐藏 JS 枚举是整数的事实。
RES@deriving({jsConverter: newType})
type fruit =
| Apple
| @as(100) Kiwi
| Watermelon;
这个选项使 @deriving(jsConverter)
生成以下类型的函数:
RESIlet 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)
隐式生成:
请注意,上面的例子是没有 JS 运行时开销的。
重命名字段
有时候你需要绑定的 JS 对象的某些字段名在 ReScript 中无效。例如 {type: "foo"}
(type 在 ReScript 中是保留关键字)和 {"aria-checked": true}
。选择一个有效的字段名,然后使用 @as
来规避这个问题:
可选标签
你可以在创建对象时忽略一些字段:
未定义的可选值不会出现在最终的 JS 对象的属性中。在上面的例子中,你可以看到 name
字段被忽略了。
注意,@optional
标签将 name
字段变成了可选的。仅仅将 name
的类型标注为 option<string>
是行不通的。
注意:因为创建函数包含可选字段,所以需要在最后使用 ()
表示已完成该函数的应用。
访问器
同样,由于 @deriving(abstract)
隐藏了记录的实际形状,你不能使用类似 joe.age
的方式访问字段。我们通过生成 getter 和 setters 来补救这个问题。
读取
@deriving(abstract)
装饰的记录类型的每个字段都会生成一个 getter 函数。以上面的代码为例,你会得到 3 个函数:nameGet
、ageGet
和 jobGet
。它们会接收一个 person
值,并分别返回 string
、int
和 string
。
RESlet twenty = ageGet(joe)
另外,你也可以使用管道操作符(->
)来获得更漂亮的访问语法:
RESlet 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 函数。可以像这样使用它:
RESlet joe = person(~name="Joe", ~age=20, ~job="teacher");
ageSet(joe, 21);
也可以通过管道操作符来使用它:
RESjoe->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
语句来使用它们:
RESmodule 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 对象:
JSvar 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 对象的完整类型,省去了你手写类型的麻烦!
这个函数是这样调用的:
RESlet homeRoute = route(
~\"type"="GET",
~path="/",
~action=_ => Js.log("Home"),
(),
)