函数
绑定 JS 函数就像绑定其他值一样:
我们也提供了一些特别的语言特性,如下所述。
标签参数
Rescript 拥有标签参数(也可以是可选参数)。标签参数在 external
中也可以使用!你可以使用它来 修复 函数不明确的用法。假设我们在建模这个函数:
JS// MyGame.js
function draw(x, y, border) {
// suppose `border` is optional and defaults to false
}
draw(10, 20)
draw(20, 20, true)
在 Rescript 这边,我们加上标签,就可以轻松地绑定和调用 draw
函数,而不用去考虑参数位置:
我们编译得到相同的函数,但是 ReScript 这边的参数有标签,使得函数的用法更加清晰了!
注意:在这种特殊情况下,你需要在 border
后面加上 ()
,一个 unit,因为 border
是最后一个可选参数。如果没有 ()
来标识你已经完成了传参,编译器将产生一个警告。
请注意,你可以随意重新排列 ReScript 的标签参数;它们会以声明的顺序正确地出现在 JavaScript 输出中:
对象方法
附加在 JS 对象(不是 JS 模块)上的函数需要用一种特殊的方式进行绑定,使用 send
:
在 send
声明中,对象总是第一个参数,方法的实际参数紧随其后(这有点像现代 OOP 的对象)。
链式调用
在 JS OOP 中用过 foo().bar().baz()
这种链式调用(“流式 api”)吗?通过使用管道操作符,我们也可以在 Rescript 中这样做。
可变参数
你或许有接受任意数量参数的 JS 函数。ReScript 支持对这些函数进行建模,但是可变参数的类型需要是相同的。如果你确定,可以添加 variadic
到 external
。
module
将会在 从 JS 导入/导出到 JS 章节中说明。
对多态函数建模
除了上面的特殊情况,一般的 JS 函数在参数类型和数量上是可以任意重载的。该怎样绑定这样的函数呢?
技巧 1:使用多个 external
如果你可以穷举一个 JS 函数的重载形式,那就只需对每种不同形式进行绑定:
注意这三个 external
是如何绑定到同一个 JS 函数 draw
的。
技巧 2:多态变体 + unwrap
如果你在想“要是这个 JS 函数的参数是变体,而不是 string
或 int
就好了”,那么好消息是:我们确实提供这样的 external
特性,通过将参数标注为多态变体来实现!假设你想绑定以下 JS 函数:
JSfunction padLeft(value, padding) {
if (typeof padding === "number") {
return Array(padding + 1).join(" ") + value;
}
if (typeof padding === "string") {
return padding + value;
}
throw new Error(`Expected string or number, got '${padding}'.`);
}
这里的 padding
在概念上就是一个变体。让我们像这样建模它:
显然,JS 端不可能有多态变体参数!但这里只是借用了多态变体的类型检查和语法。类型的 @unwrap
标注会导致编译时去掉变体构造器,而仅留下 payload 值。请看输出。
更好的参数约束
让我们看看 Node.js 的 fs.readFileSync
函数的第二个参数,它可以接受一个字符串,但是只能从一个字符串集合中选取:"ascii"
,"utf8"
等。你可以将它们绑定为 string 类型,但是也可以使用多态变体 + string
来确保正确地调用:
将
@string
添加到整个多态变体类型,使其构造器编译为同名字符串将
@as("bla")
添加到构造器可以让你自定义输出的字符串
现在,传递类似 "myOwnUnicode"
或其他变体构造器给 readFileSync
函数,编译器会正确地报错。
除了编译为字符串,你也可以把参数编译为整数,方法类似,把 string
替换成 int
即可:
onClosed
编译成 0
,onOpen
编译成 20
,inBinary
编译成 21
。
特殊情况:事件监听
多态变体的最后一个技巧:
固定参数
当给 JS 函数传递预先决定的固定值时,使用 external
绑定函数是很方便的:
同时使用 @as("exit")
和占位符 _
参数,表示你想让第一个参数编译为字符串 "exit"
。你也可以一起使用as
和 JSON 字面量,例如:@as(JSON`true`)
,@as(JSON`{"name":"John"}`)
等。
忽略参数
你还可以在 JS 输出中显式“隐藏” external
函数的参数,如果你想在不影响 JS 端的情况下向其他参数添加类型约束,这个特性就很实用:
注意:这是一个非常小众的特性,主要用于映射多态的 JS API。
柯里化和去柯里化
咖喱是一道美味的印度菜。更重要的是,在 ReScript(以及一般的函数式编程)的上下文中,柯里化意味着多参数函数可以每次应用几个参数,直到所有参数都被应用。
看到 addFive
这个中间函数了吗?add
有 3 个参数,但只收到了 1 个。它被解释为将参数 5
柯里化了,并等待后面 2 个参数被应用。函数类型签名如下:
let add: (int, int, int) => int
let addFive: (int, int) => int
let twelve: int
(在 JS 这样的动态语言中,柯里化操作是有风险的,因为如果忘了传递参数,在编译时并不会报错)。
缺点
不幸的是,由于上述原因,JS 没有柯里化,ReScript 多参数函数很难 100% 干净地映射到 JS 函数:
当函数的所有参数都被提供(没有柯里化)时,ReScript 会以最佳方式进行编译,例如,将有 3 个参数的函数调用编译成 3 个参数的普通 JS 调用。
如果很难检测函数是否完全被应用 *,ReScript 会使用运行时机制(“Curry” 模块),将参数尽可能的柯里化,并确认在最终结果中函数是否完全被应用。
一些 JS API(如
throttle
、debounce
和promise
)可能会搞乱上下文,也就是会使用函数bind
机制、使用this
等。这种实现方式与柯里化的逻辑有冲突。
* 如果调用点被声明为具有 3 个参数的函数,我们有时不知道它到底是一个被柯里化的函数,还是一个确实只有 3 个参数的原始函数。
ReScript 尽可能尝试 #1。即使放弃 #1 使用 #2 的柯里化机制时,通常也是无害的。
然而,如果你遇到了 #3,启发式的方法还不够好:你需要一种有保障的方法来完全应用函数,不进行中间的柯里化步骤。我们通过在函数声明和调用处使用“去柯里化”语法来提供这种保证。
解决方案:保证去柯里化
去柯里化标注同样可用于 external
:
额外的解决方案
上面的解决方案是安全的、有保证的、性能良好的,但有些累赘。我们提供了一个替代方案,如果:
你正在使用
external
。external
函数接受另一个函数作为参数。你希望用户不需要用
.
标注调用点。
试试 @uncurry
:
一般情况下,推荐使用 uncurry
;编译器会在编译时做很多优化来将柯里化函数去柯里化。然而,在某些情况下,编译器无法对其进行优化。在这些情况下,它会被转换为运行时的检查。
对基于 this 的回调函数建模
许多 JS 库都有依赖 this 的回调函数,例如:
JSx.onload = function(v) {
console.log(this.response + v)
}
这里的 this
是指向 x
的(实际上,这取决于 onload
方法是如何被调用的,但是讨论这个我们就偏题了)。将 x.onload
的类型声明为 (. unit) -> unit
是不对的。相反,我们引入了一个特殊的属性,@this
,它允许我们像这样声明 x
的类型:
@this
将它的第一个参数为 JS 的 this
保留。对于无参函数,声明时不需要冗余的 unit
类型。
对返回可空值的函数进行包装
对于返回值可能是 undefined
或 null
的 JS 函数,我们提供了 @return(...)
来自动将该值转换为 option
类型(回忆一下,ReScript option
的 None
值只会编译为 undefined
而不是 null
)。
return(nullable)
属性会自动将 null
和 undefined
转换为 option
类型。
目前支持 4 个指令:null_to_opt
、undefined_to_opt
、nullable
和 identity
。
identity
将确保编译器对返回值不做任何处理。这一般用的很少,但为了调试的目的在此介绍。