文档 / 语言手册 / 绑定到 JS 函数
Edit

函数

绑定 JS 函数就像绑定其他值一样:

ReScriptJS Output
// Import nodejs' path.dirname
@module("path") external dirname: string => string = "dirname"
let root = dirname("/User/github") // returns "User"

我们也提供了一些特别的语言特性,如下所述。

标签参数

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 函数,而不用去考虑参数位置:

ReScriptJS Output
@module("MyGame")
external draw: (~x: int, ~y: int, ~border: bool=?, unit) => unit = "draw"

draw(~x=10, ~y=20, ~border=true, ())
draw(~x=10, ~y=20, ())

我们编译得到相同的函数,但是 ReScript 这边的参数有标签,使得函数的用法更加清晰了!

注意:在这种特殊情况下,你需要在 border 后面加上 (),一个 unit,因为 border最后一个可选参数。如果没有 () 来标识你已经完成了传参,编译器将产生一个警告。

请注意,你可以随意重新排列 ReScript 的标签参数;它们会以声明的顺序正确地出现在 JavaScript 输出中:

ReScriptJS Output
@module("MyGame")
external draw: (~x: int, ~y: int, ~border: bool=?, unit) => unit = "draw"

draw(~x=10, ~y=20, ())
draw(~y=20, ~x=10, ())

对象方法

附加在 JS 对象(不是 JS 模块)上的函数需要用一种特殊的方式进行绑定,使用 send

ReScriptJS Output
type document // abstract type for a document object
@send external getElementById: (document, string) => Dom.element = "getElementById"
@val external doc: document = "document"

let el = getElementById(doc, "myId")

send 声明中,对象总是第一个参数,方法的实际参数紧随其后(这有点像现代 OOP 的对象)。

链式调用

在 JS OOP 中用过 foo().bar().baz() 这种链式调用(“流式 api”)吗?通过使用管道操作符,我们也可以在 Rescript 中这样做。

可变参数

你或许有接受任意数量参数的 JS 函数。ReScript 支持对这些函数进行建模,但是可变参数的类型需要是相同的。如果你确定,可以添加 variadicexternal

ReScriptJS Output
@module("path") @variadic
external join: array<string> => string = "join"

let v = join(["a", "b"])

module 将会在 从 JS 导入/导出到 JS 章节中说明。

对多态函数建模

除了上面的特殊情况,一般的 JS 函数在参数类型和数量上是可以任意重载的。该怎样绑定这样的函数呢?

技巧 1:使用多个 external

如果你可以穷举一个 JS 函数的重载形式,那就只需对每种不同形式进行绑定:

ReScriptJS Output
@module("MyGame") external drawCat: unit => unit = "draw"
@module("MyGame") external drawDog: (~giveName: string) => unit = "draw"
@module("MyGame") external draw: (string, ~useRandomAnimal: bool) => unit = "draw"

注意这三个 external 是如何绑定到同一个 JS 函数 draw 的。

技巧 2:多态变体 + unwrap

如果你在想“要是这个 JS 函数的参数是变体,而不是 stringint 就好了”,那么好消息是:我们确实提供这样的 external 特性,通过将参数标注为多态变体来实现!假设你想绑定以下 JS 函数:

JS
function 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 在概念上就是一个变体。让我们像这样建模它:

ReScriptJS Output
@val
external padLeft: (
  string,
  @unwrap [
    | #Str(string)
    | #Int(int)
  ])
  => string = "padLeft"
padLeft("Hello World", #Int(4))
padLeft("Hello World", #Str("Message from ReScript: "))

显然,JS 端不可能有多态变体参数!但这里只是借用了多态变体的类型检查和语法。类型的 @unwrap 标注会导致编译时去掉变体构造器,而仅留下 payload 值。请看输出。

更好的参数约束

让我们看看 Node.js 的 fs.readFileSync 函数的第二个参数,它可以接受一个字符串,但是只能从一个字符串集合中选取:"ascii""utf8" 等。你可以将它们绑定为 string 类型,但是也可以使用多态变体 + string 来确保正确地调用:

ReScriptJS Output
@module("fs")
external readFileSync: (
  ~name: string,
  @string [
    | #utf8
    | @as("ascii") #useAscii
  ],
) => string = "readFileSync"

readFileSync(~name="xx.txt", #useAscii)
  • @string 添加到整个多态变体类型,使其构造器编译为同名字符串

  • @as("bla") 添加到构造器可以让你自定义输出的字符串

现在,传递类似 "myOwnUnicode" 或其他变体构造器给 readFileSync 函数,编译器会正确地报错。

除了编译为字符串,你也可以把参数编译为整数,方法类似,把 string 替换成 int 即可:

ReScriptJS Output
@val
external testIntType: (
  @int [
    | #onClosed
    | @as(20) #onOpen
    | #inBinary
  ])
  => int = "testIntType"
testIntType(#inBinary)

onClosed 编译成 0onOpen 编译成 20inBinary 编译成 21

特殊情况:事件监听

多态变体的最后一个技巧:

ReScriptJS Output
type readline

@send
external on: (
    readline,
    @string [
      | #close(unit => unit)
      | #line(string => unit)
    ]
  )
  => readline = "on"

let register = rl =>
  rl
  ->on(#close(event => ()))
  ->on(#line(line => Js.log(line)));

固定参数

当给 JS 函数传递预先决定的固定值时,使用 external 绑定函数是很方便的:

ReScriptJS Output
@val
external processOnExit: (
  @as("exit") _,
  int => unit
) => unit = "process.on"

processOnExit(exitCode =>
  Js.log("error code: " ++ Js.Int.toString(exitCode))
);

同时使用 @as("exit") 和占位符 _ 参数,表示你想让第一个参数编译为字符串 "exit"。你也可以一起使用as 和 JSON 字面量,例如:@as(JSON`true`)@as(JSON`{"name":"John"}`)等。

忽略参数

你还可以在 JS 输出中显式“隐藏” external 函数的参数,如果你想在不影响 JS 端的情况下向其他参数添加类型约束,这个特性就很实用:

ReScriptJS Output
@val external doSomething: (@ignore 'a, 'a) => unit = "doSomething"

doSomething("this only shows up in ReScript code", "test")

注意:这是一个非常小众的特性,主要用于映射多态的 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 函数:

  1. 当函数的所有参数都被提供(没有柯里化)时,ReScript 会以最佳方式进行编译,例如,将有 3 个参数的函数调用编译成 3 个参数的普通 JS 调用。

  2. 如果很难检测函数是否完全被应用 *,ReScript 会使用运行时机制(“Curry” 模块),将参数尽可能的柯里化,并确认在最终结果中函数是否完全被应用。

  3. 一些 JS API(如 throttledebouncepromise)可能会搞乱上下文,也就是会使用函数 bind 机制、使用 this 等。这种实现方式与柯里化的逻辑有冲突。

* 如果调用点被声明为具有 3 个参数的函数,我们有时不知道它到底是一个被柯里化的函数,还是一个确实只有 3 个参数的原始函数。

ReScript 尽可能尝试 #1。即使放弃 #1 使用 #2 的柯里化机制时,通常也是无害的。

然而,如果你遇到了 #3,启发式的方法还不够好:你需要一种有保障的方法来完全应用函数,不进行中间的柯里化步骤。我们通过在函数声明和调用处使用“去柯里化”语法来提供这种保证。

解决方案:保证去柯里化

去柯里化标注同样可用于 external

ReScriptJS Output
type timerId
@val external setTimeout: ((. unit) => unit, int) => timerId = "setTimeout"

let id = setTimeout((.) => Js.log("hello"), 1000)

额外的解决方案

上面的解决方案是安全的、有保证的、性能良好的,但有些累赘。我们提供了一个替代方案,如果:

  • 你正在使用 external

  • external 函数接受另一个函数作为参数。

  • 你希望用户不需要. 标注调用点。

试试 @uncurry

ReScriptJS Output
@send external map: (array<'a>, @uncurry ('a => 'b)) => array<'b> = "map"
map([1, 2, 3], x => x + 1)

一般情况下,推荐使用 uncurry;编译器会在编译时做很多优化来将柯里化函数去柯里化。然而,在某些情况下,编译器无法对其进行优化。在这些情况下,它会被转换为运行时的检查。

对基于 this 的回调函数建模

许多 JS 库都有依赖 this 的回调函数,例如:

JS
x.onload = function(v) { console.log(this.response + v) }

这里的 this 是指向 x 的(实际上,这取决于 onload 方法是如何被调用的,但是讨论这个我们就偏题了)。将 x.onload 的类型声明为 (. unit) -> unit 是不对的。相反,我们引入了一个特殊的属性,@this,它允许我们像这样声明 x 的类型:

ReScriptJS Output
type x
@val external x: x = "x"
@set external setOnload: (x, @this ((x, int) => unit)) => unit = "onload"
@get external resp: x => int = "response"
setOnload(x, @this ((o, v) => Js.log(resp(o) + v)))

@this 将它的第一个参数为 JS 的 this 保留。对于无参函数,声明时不需要冗余的 unit 类型。

对返回可空值的函数进行包装

对于返回值可能是 undefinednull 的 JS 函数,我们提供了 @return(...) 来自动将该值转换为 option 类型(回忆一下,ReScript optionNone 值只会编译为 undefined 而不是 null)。

ReScriptJS Output
type element
type dom

@send @return(nullable)
external getElementById: (dom, string) => option<element> = "getElementById"

let test = dom => {
  let elem = dom->(getElementById("haha"))
  switch (elem) {
  | None => 1
  | Some(_ui) => 2
  }
}

return(nullable) 属性会自动将 nullundefined 转换为 option 类型。

目前支持 4 个指令:null_to_optundefined_to_optnullableidentity

identity 将确保编译器对返回值不做任何处理。这一般用的很少,但为了调试的目的在此介绍。