17. 自己的效果:effect 与 with 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
三件事在这段里:
effect Ask { ... }声明效果与它的操作。操作是没有体的函数签名——体由 handler 给。sum_three直接调ask(),签名里写下!Ask。不写会报错,并告诉你两条出路。with handle Ask { ask() => 42 }装上 handler:这一句之后的整个块在它的作用域内。 臂ask() => 42就是一个闭包,调用ask()就是调它,返回值就是ask()的值。- 这个「块的剩余部分」自己也是闭包,所以它按值捕获:
with handle之前声明的var, 在它之后既不能读也不能赋值;之后声明的var没问题。诊断会说明这一点,并给两条 出路:在with handle之前先let一个快照,或者把值作为参数传进来。
多个操作,和有参数的操作
一个效果可以有多个操作,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)会连同具名效果一起转发,所以 map、fold、for 循环都照旧:
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 的边界
- 一个臂就是一次普通调用、返回值即结果(尾恢复)。没有「把延续存起来以后再恢复」 「恢复两次」这类玩法——那需要延续捕获,不在这一档里。
- 「操作不返回调用点」的用法请走既有的失败机制:
Result+?、catch_fault/catch_panic/bracket。 - 效果不带类型参数(没有
effect Yield[T])。 - comptime / const 初始化不发具名效果,也装不了 handler。trait / impl 方法可以带标签,
任何写出来的函数类型(
alias目标、record 字段、参数位)也可以;impl 仍欠的是一条 标签与 trait 恰相等的行,因为每个标签都是这个方法的一格隐藏参数。 - 带标签的函数可以当函数值传:标签进到值的类型里,由调用点供 handler。只有操作本身
不行,它背后没有可取的函数符号;包一层 lambda(
() => ask())即可。 - 臂是闭包,所以臂里不能写外层的
var,也不能return/break跳出去。
完整规则见 spec.md §6.5,设计取舍见 effects-design.md。
至此你已见过 Dawn 的全部核心特性。更深的规范见
spec.md,设计取舍见 design.md。这两篇先写中文再翻译:
它们是活文档,每次改语言都在中文里改,所以中文是正本、英文按它登记。docs/ 其余文档
仍只有中文:它们的读者是作者本人,一段要先翻译才能写出来的话,就是一段写不出来的话。