函数

完整的函数语法清单在最后。

ReScript 函数用箭头声明,并返回一个表达式,就像 JS 函数一样。它们也可以编译成干净的 JS 函数。

ReScriptJS Output
let greet = (name) => "Hello " ++ name

这声明了一个函数并给它指定了 greet 这个名字,你可以这样调用它:

ReScriptJS Output
greet("world!") // "Hello world!"

多参数函数的参数用逗号分隔:

ReScriptJS Output
let add = (x, y, z) => x + y + z
add(1, 2, 3) // 6

对于较长的函数,你得用 {} 将函数体围起来:

ReScriptJS Output
let greetMore = (name) => {
  let part1 = "Hello"
  part1 ++ " " ++ name
}

如果你的函数没有参数,只需要写 let greetMore = () => {...}

标签参数

多参数函数,特别是那些参数类型相同的函数,在调用时可能会很混乱。

ReScriptJS Output
let addCoordinates = (x, y) => {
  // use x and y here
}
// ...
addCoordinates(5, 6) // which is x, which is y?

你可以为参数附加标签,在参数名前面加上 ~ 符号:

ReScriptJS Output
let addCoordinates = (~x, ~y) => {
  // use x and y here
}
// ...
addCoordinates(~x=5, ~y=6)

你可以按任意顺序传入这些参数:

ReScriptJS Output
addCoordinates(~y=6, ~x=5)

声明中的 ~x 部分意味着该函数接受一个标签为 x 的参数,并且可以在函数体中用相同的名称来指代它。你也可以在函数体中用不同的名字指代参数,这样更简洁:

ReScriptJS Output
let drawCircle = (~radius as r, ~color as c) => {
  setColor(c)
  startAt(r, r)
  // ...
}

drawCircle(~radius=10, ~color="red")

事实上,(~radius) 只是 (~radius as radius) 的一个缩写。

下面是给参数标记类型的语法:

ReScriptJS Output
let drawCircle = (~radius as r: int, ~color as c: string) => {
  // code here
}

可选的标签参数

带标签的函数参数可以在声明时设置成可选的。你可以在调用函数时省略它们。

ReScriptJS Output
// radius can be omitted
let drawCircle = (~color, ~radius=?, ()) => {
  setColor(color)
  switch radius {
  | None => startAt(1, 1)
  | Some(r_) => startAt(r_, r_)
  }
}

当使用这种语法的时候,radius包装在标准库的 option 类型中,默认值为 None。如果函数调用提供了这个参数,那么它会被包装到 Some 中。所以这里 radius 的类型是 None| Some(int)

这里有更多关于 option 类型的信息。

注意:因为类型系统的限制,只要函数有一个可选参数,你就需要确保在它后面至少有一个位置参数(又称非标记的,非可选的参数)。如果没有,可以提供一个虚拟的 unit(又表示为 ())参数。

类型签名和类型标注

当涉及到类型签名和类型标注时,带有可选标记参数的函数可能会令人困惑。事实上,一个可选标记参数的类型在不同地方看起来是不同的,这取决于你是正在调用函数,还是在函数体内部。在函数外部,一个原生值要么被传入(比如 int),要么不传入。在函数内部,参数总是存在的,但其值是一个 option 值(option<int>)。这意味着类型签名是不同的,这取决于你标注的是函数类型还是参数类型,前者是一个原生值,而后者是一个 option

如果我们回到之前的例子,同时给它的参数添加一个签名和类型标注,我们会得到这样的结果:

ReScriptJS Output
let drawCircle: (~color: color, ~radius: int=?, unit) => unit =
  (~color: color, ~radius: option<int>=?, ()) => {
    setColor(color)
    switch radius {
    | None => startAt(1, 1)
    | Some(r_) => startAt(r_, r_)
    }
  }

第一行是函数的签名,我们会在一个接口文件中这样定义它(见签名章节)。函数的签名描述了外部世界与函数交互的类型,因此 radius 的类型是 int,因为它在调用时确实期望得到一个 int

在第二行中,我们对参数进行标注,以帮助我们在函数内部使用参数时记住参数的类型,这里 radius 在函数内部将是一个 option<int>

如果你在编写带有可选标记参数的函数的类型签名时遇到了困难,请记住参数类型内外有别!

显式传入的 option

有时,你想将值转发给函数,但不确定值是 None 还是 Some(a)。你可能会这样做:

ReScriptJS Output
let result =
  switch payloadRadius {
  | None => drawCircle(~color, ())
  | Some(r) => drawCircle(~color, ~radius=r, ())
  }

这会变得很啰嗦。我们提供了一个简写:

ReScriptJS Output
let result = drawCircle(~color, ~radius=?payloadRadius, ())

这样做的意思是“我知道 radius 是可选值,当我给它传值时,它需要是一个 int,但我不确定传的值是 None 还是 Some(val),所以我传整个 option 包装给你”。

有默认值的可选参数

可选的标签参数也可以提供一个默认值,这种情况下,它们不会被包装在 option 类型中。

ReScriptJS Output
let drawCircle = (~radius=1, ~color, ()) => {
  setColor(color)
  startAt(radius, radius)
}

递归函数

ReScript 默认不允许函数在它内部调用自身。要创建一个递归函数,需要在 let 后面加上 rec 关键字:

ReScriptJS Output
let rec neverTerminate = () => neverTerminate()

一个简单的递归函数可能看起来像这样:

ReScriptJS Output
// Recursively check every item on the list until one equals the `item`
// argument. If a match is found, return `true`, otherwise return `false`
let rec listHas = (list, item) =>
  switch list {
  | list{} => false
  | list{a, ...rest} => a === item || listHas(rest, item)
  }

递归调用一个函数对性能和调用栈不利。不过 ReScript 会智能地将尾递归编译为快速的 JS 循环。试着检查一下上面代码的 JS 输出!

互递归函数

互递归函数像递归函数一样使用 rec 关键字开头,然后用 and 关键字串在一起:

ReScriptJS Output
let rec callSecond = () => callFirst()
and callFirst = () => callSecond()

去柯里化的函数

ReScript 的函数是默认柯里化的,这是少数的编译到 JS 之后付出的性能代价之一。编译器会尽最大可能去除这些柯里化。然而,在一些边缘情况下,你可能希望保证函数没有柯里化。这时可以在函数的参数列表前放一个 .

ReScriptJS Output
let add = (. x, y) => x + y

add(. 1, 2)

如果你为去柯里化函数标注了类型,你也要在那加一个 .

注意:声明和调用位置都要加上去柯里化的标记。

这个特性看起来似乎微不足道,但实际上是 ReScript 作为一门函数式语言最重要的特性之一。如果你想要在 JS 输出中移除所有运行时柯里化,我们鼓励你使用它。

Async/Await(自 v10.1 起)

就像在 JS 中一样,可以通过在定义前添加 async 来声明一个异步函数,可以在这类函数中使用 await。输出结果看上去和 JS 没多少区别:

ReScriptJS Output
let getUserName = async (userId) => userId

let greetUser = async (userId) => {
  let name = await getUserName(userId)
  "Hello " ++ name ++ "!"
}

getUser 的返回类型被推断为 promise<string>。类似地,await getUserName(userID) 在函数返回 promise<string> 时返回一个 string。在 async 函数之外使用 await(包括在异步函数的非异步回调中)是一个错误。

人性化的错误处理

错误处理是通过简单地使用 try/catch 或者匹配 exception 的 switch 来完成的,就像在非异步的函数中一样。JS 异常和 ReScript 中定义的异常都可被捕获。编译器负责将 JS 异常打包成内置的 JsError 异常:

ReScriptJS Output
exception SomeReScriptException

let somethingThatMightThrow = async () => raise(SomeReScriptException)

let someAsyncFn = async () => {
  switch await somethingThatMightThrow() {
  | data => Some(data)
  | exception JsError(_) => None
  | exception SomeReScriptException => None
  }
}

ignore() 函数

偶尔你可能想忽略一个函数的返回值,ReScript 提供了一个 ignore() 函数,它抛弃传入的值并返回 ()

ReScriptJS Output
mySideEffect()->Promise.catch(handleError)->ignore

Js.Global.setTimeout(myFunc, 1000)->ignore

技巧和诀窍

有关函数的语法清单:

函数声明

RES
// anonymous function (x, y) => 1 // bind to a name let add = (x, y) => 1 // labeled let add = (~first as x, ~second as y) => x + y // with punning sugar let add = (~first, ~second) => first + second // labeled with default value let add = (~first as x=1, ~second as y=2) => x + y // with punning let add = (~first=1, ~second=2) => first + second // optional let add = (~first as x=?, ~second as y=?) => switch x {...} // with punning let add = (~first=?, ~second=?) => switch first {...}

带类型标注的函数声明

RES
// anonymous function (x: int, y: int): int => 1 // bind to a name let add = (x: int, y: int): int => 1 // labeled let add = (~first as x: int, ~second as y: int) : int => x + y // with punning sugar let add = (~first: int, ~second: int) : int => first + second // labeled with default value let add = (~first as x: int=1, ~second as y: int=2) : int => x + y // with punning sugar let add = (~first: int=1, ~second: int=2) : int => first + second // optional let add = (~first as x: option<int>=?, ~second as y: option<int>=?) : int => switch x {...} // with punning sugar // note that the caller would pass an `int`, not `option<int>` // Inside the function, `first` and `second` are `option<int>`. let add = (~first: option<int>=?, ~second: option<int>=?) : int => switch first {...}

应用函数

RES
add(x, y) // labeled add(~first=1, ~second=2) // with punning sugar add(~first, ~second) // application with default value. Same as normal application add(~first=1, ~second=2) // explicit optional application add(~first=?Some(1), ~second=?Some(2)) // with punning add(~first?, ~second?)

在应用时加上类型标注

RES
// labeled add(~first=1: int, ~second=2: int) // with punning sugar add(~first: int, ~second: int) // application with default value. Same as normal application add(~first=1: int, ~second=2: int) // explicit optional application add(~first=?Some(1): option<int>, ~second=?Some(2): option<int>) // no punning sugar when you want to type annotate

独立的类型签名

RES
// first arg type, second arg type, return type type add = (int, int) => int // labeled type add = (~first: int, ~second: int) => int // labeled type add = (~first: int=?, ~second: int=?, unit) => int

在接口文件中加上类型签名

在你的接口文件(.resi)中为一个来自实现文件(.res)的函数加上类型注解:

let add: (int, int) => int

类型标注部分和上面的是一样的。

不要混淆 let add: myTypetype add = myType。在 .resi 接口文件中使用时,前者导出绑定 add,并将其类型标注为 myType;后者导出类型 add,其值为 myType 类型。