8. lambda 与效果系统

  • 第八章,共二十章
  • 阅读约 2 分钟

匿名函数用 (参数) => 表达式——单个不带注解的参数可省括号,写成 x => 表达式; 类型可推导时参数注解可省。函数类型写作 fn(A) -> B !e,其中 !e 是效果。纯函数看签名即知没有副作用,测试无需 mock。

pub fn main() -> Unit !io = {
  let nums = [1, 2, 3, 4]
  let evens = filter(nums, n => n % 2 == 0)
  let doubled = map(evens, n => n * 2)
  println(to_string(doubled))
}
[4, 8]

高阶函数用效果变量转发参数的效果:map(f) 的效果等于 f 的效果。 两个函数参数的效果之并写作 !(e1 | e2)——纯 ∘ 纯还是纯,沾 io 便是 io。

fn compose[A, B, C](f: fn(A) -> B !e1, g: fn(B) -> C !e2) -> fn(A) -> C !(e1 | e2) =
  a => g(f(a))

fn inc(x: Int) -> Int = x + 1
fn dbl(x: Int) -> Int = x * 2

pub fn main() -> Unit !io = {
  let f = compose(inc, dbl)
  println(to_string(f(10)))
}
22

块作为最后一个实参

调用的最后一个实参是函数时,可以写成紧跟在调用后面、同一行上的一个裸块: f(a) { e } 就是 f(a, () => e)。带参数的块像 lambda 那样在 => 前写出参数, 一个裸名字,或者一个带括号的列表:

fn twice(body: fn() -> Unit !e) -> Unit !e = {
  body()
  body()
}

pub fn main() -> Unit !io = {
  twice {
    println("hi")
  }
  let total = fold([1, 2, 3], 0) { (acc, x) => acc + x * x }
  println("${total}")
  let loud = map(["a", "b"]) { s => s ++ "!" }
  println("${loud}")
}
hi
hi
14
["a!", "b!"]

不管前面传了什么,块填的都是最后一个形参,所以 fold(xs, 0) { ... } 读起来就是 「从 0 开始 fold,用这个」。块必须和调用在同一行开头;下一行的 { 是另一条块语句。 在 if、while、for 的头部以及 match 后面,花括号是语句体而不是实参,那里要给 调用加括号:if (f(x) { ... }) { ... }。配上具名实参与默认值,一个选项很多、又带一段 主体的函数在调用点就读作 column(gap: 12) { ... }。

另见:spec.md §4.3、§4.5、§6