在 EYG 中编写有状态的效应处理器
摘要
一份关于在 EYG 编程语言中编写有状态效应处理器的指南,涵盖单效应计数器、通用 Update/State 效应、用于测试的效应转发,以及从效应中返回函数时因递归类型错误而遇到的陷阱。
<p><a href="https://lobste.rs/s/shs3xv/writing_stateful_effect_handlers_eyg">评论</a></p>
查看缓存全文
缓存时间: 2026/10/16 10:42
# 高级效应
来源:https://eyg.run/guides/advanced-effects
## 有状态处理器
### 入门
处理效应时,一个常见的需求是依赖于引发效应的计算之外的某个状态。例如,`Increment`效应需要跟踪当前计数。又如,在测试中,`StandardOut`的处理器可能会构建一个由所有已写入消息组成的状态。
下面是单个有状态`Increment`效应的实现。
``
let with_counter = (initial, computation) -> {
handle Increment(
(_unit, resume) -> {
(state) -> {
resume(state)(!int_add(state, 1))
}
},
(_) -> {
let value = computation({})
let finish = (count) -> { {count, value} }
finish
}
)(initial)
}
with_counter(0, (_) -> {
let _ = perform Increment({})
let _ = perform Increment({})
let _ = perform Increment({})
"完成"
})
``
每个 handle 表达式只作用于一个效应标签。可以嵌套处理器以处理多个效应,但需要谨慎管理多个处理器之间共享的同一个状态。
这是为了保持类型系统简单而做出的设计,不过将来可能会放宽这一限制。如果需要多个带效应的操作,你有两种选择。
### 通用的有状态效应
``
let with_counter = (initial, computation) -> {
handle Update(
(transform, resume) -> {
(state) -> {
let state = transform(state)
resume(state)(state)
}
},
(_) -> {
let value = computation({})
let finish = (count) -> { {count, value} }
finish
}
)(initial)
}
let increase = (amount) -> {
let _ = perform Update((state) -> { !int_add(state, amount) })
{}
}
let get = (_) -> {
perform Update((state) -> { state })
}
with_counter(0, (_) -> {
let _ = increase(3)
let value = get({})
let _ = perform StandardOut(!int_to_string(value))
let _ = increase(2)
!int_to_string(value)
})
``
这个计数器的值可以增加,也可以读取。函数`get`和`increase`是这一有状态操作的公共接口。通用的`Update`效应接收一个将当前状态转换为新状态的变换函数。
只有在你能控制效应的设计时,才能使用这种通用方法,而这并非总是可行。例如,我们可能想测试一个使用`Get`和`Increase`效应的库。
在这种情况下,可以将这些效应转发给一个你能够控制的新效应。
``
let with_state = (initial, f) -> {
// 通用的 State 效应持有共享状态。
let run = handle State((update, resume, state) -> {
let state = update(state)
resume(state)(state)
}, (_) -> {
// 这些处理器转换操作;它们不持有独立的状态。
let value = handle Increase((amount, resume) -> {
let _ = perform State((state) -> { !int_add(state, amount) })
resume({})
}, (_) -> {
handle Get((_, resume) -> {
let state = perform State((state) -> { state })
resume(state)
}, f)
})
let finish = (state) -> { {state, value} }
finish
})
run(initial)
}
with_state(0, (_) -> {
let before = perform Get({})
let _ = perform Increase(2)
let middle = perform Get({})
let _ = perform Increase(-1)
let after = perform Get({})
[before, middle, after]
})
// {state: 1, value: [0, 2, 1]}
``
## 返回函数
对以下代码进行类型检查时,会失败并报错`此表达式具有递归类型`。
``
let main = (_) -> {
let f = perform Get({})
f("")
}
main({})
``
失败的原因并非一目了然。`f`的类型出现在`main`函数的`Get`效应行的返回类型中。调用`f`时,它的效应会与当前函数块(即`main`块)中的效应进行统一。这会导致系统尝试构造一个递归类型。
因此,建议不要从效应中返回函数。
相似文章
在 Erlang 程序中嵌入 EYG
一份关于将 EYG 脚本语言嵌入 Erlang 应用程序的指南,涵盖依赖项配置、运行纯程序、错误处理以及添加代数效应支持。
EYG 迎来一次异常实用的更新
EYG 编程语言迎来了实用性更新,包括已文档化的文本语法、CLI 中新增的文件系统操作,以及改进的解析器错误提示,便于 Shell 脚本编写。
EYG:大量CLI改进、新指南和新效果
EYG CLI 新增了命令(eval、check、script、shell)、标志、通过 glam 库实现的漂亮打印、改进的错误输出、更简便的安装,以及新的效果,如 DeleteFile、Env、Hash、Now、Random、Sleep。同时发布了新的指南。
Effectful 递归方案
《Effekt》编程语言博客文章演示了如何利用效应和处理器实现递归方案(特别是 catamorphisms),以此取代传统的基于函子的方法,从而避免了对无限递归类型的依赖。
代数效应:给普通人的解释
这是一篇教育性博客文章,通过类比 try/catch 和 async/await 来解释编程中的代数效应概念,并讨论了它们与 React 及未来编程范式的潜在关联。