17. 自己的效果:effectwith handle

!io 是编译器内建的那一种效果。你也可以声明自己的:一组操作,谁来实现由调用方 在使用点决定。

effect Ask {
  fn ask() -> Int
}

fn sum_three() -> Int !Ask = ask() + ask() + ask()

pub fn main() -> Unit !io = {
  with handle Ask { ask() => 42 }
  println(to_string(sum_three()))
}
在 Playground 打开
126

三件事在这段里:

多个操作,和有参数的操作

一个效果可以有多个操作,handler 必须每个都答,一个不多一个不少:

effect Log {
  fn note(msg: String) -> Unit
  fn level() -> Int
}

fn work(n: Int) -> Int !Log = {
  note("working on ${n}")
  n * level()
}

pub fn main() -> Unit !io = {
  with handle Log {
    note(m) => println("[log] ${m}")
    level() => 3
  }
  println(to_string(work(7)))
}
在 Playground 打开
[log] working on 7
21

臂的身体可以做任何事——包括 io。它算在装 handler 的那个块头上(上面的 main 因此 是 !io),不算在发出操作的 work 头上:work 只欠 !Log

谁来应答:词法上最近的那个

handler 是按写在哪里找的,不是按运行时的栈找的。内层遮蔽外层:

effect Ask {
  fn ask() -> Int
}

fn twice() -> Int !Ask = ask() + ask()

pub fn main() -> Unit !io = {
  with handle Ask { ask() => 1 }
  println(to_string(twice()))
  with handle Ask { ask() => 10 }
  println(to_string(twice()))
}
在 Playground 打开
2
20

臂里再发本效果,找的是外层的 handler(handler 不答自己)——所以 with handle Ask { ask() => ask() * 10 } 是「把外面那个答案乘十」,不是死循环。

闭包不会留住它写在谁下面的那个 handler。带它跑出块外,标签也跟着跑:它的行里仍写着 !Ask,应答的是最终调用它的地方在场的那个 handler。类型说的是谁得供 handler, 不是某个臂会做什么,所以纯臂的 handler 交给你的同样是一个 !Ask 闭包,io 臂的也是。

于是函数类型可以写具名效果,而且字面读法就是它的意思。fn(f: fn() -> Int !Ask) 说的是 「谁调用 f,谁供 Ask 的 handler」,调用真就是这么走的。fn(f: fn() -> Int !e) 是另一种写法,不是迁移写法:效果变量收任何行的闭包,并把那条行转发进你自己的行。

高阶函数不用改一行

效果变量(!e)会连同具名效果一起转发,所以 mapfoldfor 循环都照旧:

effect Ask {
  fn ask() -> Int
}

fn shifted(xs: List[Int]) -> List[Int] !Ask = map(xs, x => x + ask())

pub fn main() -> Unit !io = {
  with handle Ask { ask() => 100 }
  println(to_string(shifted([1, 2, 3])))
}
在 Playground 打开
[101, 102, 103]

v1 的边界

完整规则见 spec.md §6.5,设计取舍见 effects-design.md


至此你已见过 Dawn 的全部核心特性。更深的规范见 spec.md,设计取舍见 design.md。这两篇先写中文再翻译: 它们是活文档,每次改语言都在中文里改,所以中文是正本、英文按它登记。docs/ 其余文档 仍只有中文:它们的读者是作者本人,一段要先翻译才能写出来的话,就是一段写不出来的话。