最近读了一篇论文:《A Programming Paradigm for Spatiotemporal Composability》。
它讲的不是一个单独的 API,也不只是“给插件加一个析构函数”。它真正想解决的是:一个大型系统里有很多可以动态加载、卸载、替换的组件,怎样让它们在变化过程中仍然保持正确。
先说结论
论文把这个问题拆成两个方向:
- 时间可组合性(Temporal composability):组件退出时,能够撤销自己留下的副作用。
- 空间可组合性(Spatial composability):组件之间的依赖发生变化时,系统能够自动协调谁启动、谁停止、谁先退出。
最后,这两个方向被统一成 时空可组合性(Spatiotemporal composability)。空间侧回答“谁应该运行、谁先停”,时间侧回答“停下来以后怎样清理干净”。两者合在一起,才是一个真正可热插拔的组件系统。
目录
- 1. 先看一个最小例子
- 2. 时间可组合性:退出时只撤销自己的贡献
- 3. 空间可组合性:依赖变化时自动协调
- 4. 为什么还要讨论 effect independence
- 5. 论文真正新增了什么
- 6. 收个尾
1. 先看一个最小例子
假设系统里有两个组件:
1 | A:数据库组件,提供 Database |
正常加载时:
1 | A 启动并提供 Database |
现在卸载 A。安全流程不是直接关闭数据库:
1 | A 宣布自己即将退出 |
如果 A 重新加载:
1 | A 再次提供 Database |
这个例子里其实同时发生了两件事:
| 问题 | 负责的方向 |
|---|---|
| B 依赖 A,A 退出时谁先停? | 空间可组合性 |
| B 停止时怎样删除路由、清理定时器? | 时间可组合性 |
2. 时间可组合性:退出时只撤销自己的贡献
论文里的 effect 可以理解为组件对共享环境做出的修改,也就是副作用:
1 | 注册路由 |
所谓 revertible effect,就是每次做修改时,同时提供一个撤销操作:
1 | ctx.effect(() => { |
组件卸载时,运行时执行已经积累的撤销操作:
1 | 注册 /users → 删除 /users |
关键不是“有没有写一个析构函数”,而是每个副作用和自己的逆操作在同一个地方配对。多个小操作的逆操作可以自动组合成整个组件的清理流程。
因此,卸载 B 时,B 只撤销:
1 | B 的路由、定时器、监听器 |
不会顺便删除 A 的数据库。卸载 A 时,A 也只撤销自己的:
1 | A 的数据库连接和服务注册 |
这就是时间可组合性最直观的含义:组件随时间离开系统后,它留下的贡献可以被正确收回。
3. 空间可组合性:依赖变化时自动协调
空间可组合性关注的不是“B 做了哪些修改”,而是:
1 | B 需要什么? |
论文把这部分叫 reactive coeffects。组件可以声明:
1 | 我需要 Database |
运行时持续检查依赖是否满足:
1 | Database 不存在 |
它还给出了一个很重要的 Ordering 保证:
1 | 依赖者先退出 |
原因很简单:B 的清理流程可能还要访问 A 提供的 Database。如果 A 先把数据库连接关掉,B 的退出就可能失败。
所以空间可组合性更像一张动态依赖图:
1 | A 提供 Database |
当 A 退出时,系统会沿着这张图协调:
1 | C 先停 |
4. 为什么还要讨论 effect independence
effect independence(效应独立性)是另一个层次的问题。它问的是:
两个组件对环境的修改,能不能交换执行顺序,并且在撤销时互不破坏?
例如:
1 | A:注册 /a |
通常无论先注册谁,结果都是:
1 | {/a, /b} |
卸载 A 后也只剩:
1 | {/b} |
这种操作可以认为比较独立。
但下面就不一样:
1 | A:余额加 5 |
如果初始余额是 10:
1 | (10 + 5) × 2 = 30 |
顺序改变了结果,因此两个 effect 不独立。
论文并不会把“不独立”的操作强行变成独立,而是:
- 同一个组件内部,按积累的逆操作以 LIFO 顺序撤销;
- 不同组件之间,如果存在必要的先后关系,就用依赖关系表达;
- 只有能够交换的部分,才允许在多个组件交错运行时自由重排。
因此:
1 | 依赖关系:B 是否需要 A 存在? |
这是两个不同问题。
5. 论文真正新增了什么
如果只看工程表面,它确实很像很多已有技术的组合:
1 | RAII / cleanup |
论文的主要工作不是凭空发明这些零件,而是把它们统一到一个运行时模型里,并尝试证明几个全局性质:
5.1 组件可以完整撤销自己的贡献
组件卸载后,自己的路由、监听器、服务注册和子组件不会残留。
5.2 依赖者不会在提供者之前被迫失去依赖
提供者退出时,依赖者先停,依赖者清理完成后提供者再退出。
5.3 异步启动期间不会悄悄混用两个依赖版本
如果 B 正在基于 Database v1 启动,而 v1 被 v2 替换,B 不会继续半边使用 v1、半边使用 v2,而是结束当前转换并重新开始。
5.4 最终状态尽量与中间历史无关
系统可能经历:
1 | 加载 A |
只要最后配置相同,系统稳定后的状态应当等价于直接从头加载最终配置。这是论文里比较强的 Confluence(合流性)结论。
当然,这些保证有前提:
- 每个副作用都要经过 Context,并提供正确的逆操作;
- 依赖图不能有环;
- 不可交换的操作必须保留必要顺序;
- 已经发出去的网络消息、邮件和扣款,不属于可以物理回滚的内部状态。
6. 收个尾
这篇论文最容易混淆的地方,是“时间”和“空间”听起来像两个互相独立的系统。其实它们是同一个问题的两个方向:
1 | 空间可组合性: |
所以它最终想做的事情可以用一句话说完:
让大型系统里的组件能够随时进出,而系统既知道该让谁先停,也能把离开的组件留下的东西收干净。
这也是我读完这篇论文后的直观理解:它的工程材料并不全新,但它试图把“动态插件系统为什么能够安全运行”这件事,从经验和约定提升为一套可以描述、实现和证明的模型。
