函数
绑定 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 将确保编译器对返回值不做任何处理。这一般用的很少,但为了调试的目的在此介绍。