并行 Redux reducer 最怕一件事:两个 reducer 以为自己在改不同状态,实际写到了同一个 slice。
ruxe 作者 Corentin 在 6 月 23 日发布的文章里,展示了一个很 Rust 的处理方式。在这个 Redux-flavored Rust learning library 里,错误的 reducer 组合不会等到运行时撞车,而是在编译期失败。
我更在意的不是 ruxe 本身。它目前更像作者用来学习和验证想法的库。真正有意思的是那个转弯:稳定版 Rust 不能直接写出类型级 H != T,作者就不再证明“没有两个 reducer 重复”,而是让编译器证明“每个状态 slice 恰好有一个匹配 reducer”。
这就是整篇文章的主线。
问题不是 Redux,而是并行写状态
Redux 的模型并不复杂。
一个 store 持有单一状态。外部事件进入系统。reducer 负责把旧状态和事件变成新状态。
它在前端出名,但这个模型不只属于网页应用。控制系统、后端事件流、可回放状态机,也会用类似结构组织状态变化。
ruxe 文章里的业务动机来自工业遥测。能源管理系统里,几十台设备按不同周期上报数据。有些轮询周期低到 50ms,一次读取还可能包含几十个寄存器。
Python 里的 Redux 式 reducer 在这种高频事件流下会变成瓶颈。这个判断不难理解:如果每次 dispatch 都要顺序跑完多个 reducer,延迟接近各 reducer 耗时之和。
并行化的诱惑也很直接。
太阳能、电池、电表、并网控制器等子系统,本来就像独立状态切片。只要各自 reducer 只写自己的 slice,理论上一次 dispatch 的耗时可以从总和降到最慢那一路。
但条件很硬:不能有两个 reducer 写同一个 slice。
Rust 的借用检查器能拦很多值层面的错误。可这里的问题是类型层面的库约束:这一组 reducer 声称要并行,它们的目标状态切片到底有没有重叠?
如果你在写 Rust 状态管理库、任务调度库或并发数据管线,这个问题不能只靠文档约定。更现实的动作是:把 reducer 和状态 slice 的对应关系放进类型签名里,让错误组合尽早暴露。
稳定 Rust 走不通“类型不相等”
最直观的做法叫 AllDistinct。
拿到一个 reducer 类型列表,递归检查每个 reducer 的 Slice associated type,确认它和后面的元素都不同。
在 C++ 模板元编程里,这条路很自然。判断两个类型是否相同,再取反,就得到“不相等”。
稳定版 Rust 没有这么顺。
它没有可直接使用的 H != T 类型约束,也不能依赖 negative impls 来表达“某个 trait 没有被实现”。这不是少一个语法糖。负向推理会牵涉 trait coherence,Rust 稳定版没有把这条路开放成通用工具。
几条路线放在一起看,差别很清楚:
| 路线 | 想证明什么 | 稳定 Rust 下的结果 | 现实判断 |
|---|---|---|---|
AllDistinct | reducer 的 slice 类型两两不同 | 需要类型级不等式,走不通 | 思路直观,但语言不支持 |
| HList + Sculptor | 每个 slice 有且只有一个 reducer | 可交给 trait resolver 检查 | 把负向问题改成正向匹配 |
| 运行时锁或人工约定 | 出错后靠测试、锁或纪律兜底 | 能做,但不够静态 | 对高频状态管线不够干净 |
这里的关键不是炫技,而是换问题。
不要问:有没有两个 reducer 访问了同一个 slice?
改问:对每个 slice,能不能在 reducer 列表里找到唯一一个负责它的 reducer?
找到一个,通过。找不到,trait bound 不满足。找到两个,impl 匹配变得歧义,编译器报错。
这对开发者的动作影响很具体:如果团队正在设计并行 reducer API,不必把所有希望押在锁、运行时检查和代码审查上。可以把“状态切片到 reducer 的唯一匹配”做成类型层约束。代价是 API 和错误信息会更复杂,不能免费拿到工程可用性。
HList 解决列表,唯一匹配解决重叠
ruxe 的实现锚点是 HList 和 Sculptor。
HList 用 HCons<H, T> 和 HNil 组成递归类型列表。它适合表达“不同类型元素组成的一串东西”。普通 tuple 也能装异构元素,但每个长度都要单独处理,做递归推导不方便。
Sculptor 模式常见于 frunk 这类 Rust 生态工具。它的原始用途,是从异构列表里按类型取出某个元素,并保留位置见证。
ruxe 借了这个思路。
对每个状态 slice,编译器沿着 reducer HList 往下找。找到 Slice = TargetSlice 的 reducer,就给出对应位置见证。这个见证让类型系统知道:匹配发生在列表中的哪一格。
重复匹配和缺失匹配会走向不同失败路径:
| 情况 | 编译期表现 | 说明 |
|---|---|---|
| 某个 slice 没有 reducer | trait bound 不满足 | 管线不完整 |
| 某个 slice 匹配到两个 reducer | impl 歧义 | 写入目标重复 |
| 每个 slice 恰好匹配一个 reducer | 类型检查通过 | 满足并行写入互斥约束 |
这套设计的价值,主要落在两类人身上。
一类是 Rust 库作者。尤其是做状态管理、actor runtime、任务调度、数据流执行器的人。ruxe 给出的启发是:当稳定 Rust 不能表达“非 A”时,可以试着把约束改写成“唯一的 A”。
另一类是工业控制、遥测和后端事件处理工程师。他们经常面对一个尴尬场景:业务上各子系统独立,工程上又共享一份状态。此时并行化要不要做,不该只看吞吐,也要看约束能不能被工具验证。
边界也要说清。
它只约束 reducer 与状态切片的写入重叠。它不会自动消除死锁、事件顺序错误、外部 I/O 副作用,也不会替你证明业务逻辑正确。
原文也没有给出真实性能提升倍数。能确定的只是理论模型:顺序执行接近多个 reducer 延迟之和;理想并行接近最慢 reducer 的延迟。中间还会有调度、同步、内存布局和 API 成本。
所以接下来该看的不是 ruxe 会不会变成一个大框架,而是三个更具体的变量:
- 错误信息是否能让普通 Rust 开发者看懂,而不是只让类型体操爱好者满意。
- API 是否能隐藏 HList 和位置见证的复杂度,让业务 reducer 写起来仍像 reducer。
- 这套约束能否和真实异步运行时、I/O、副作用边界配合,而不是只停留在类型演示。
如果这三点做不到,团队很可能仍会退回运行时锁、人工约定和测试兜底。不是因为类型系统没用,而是维护成本压过了静态收益。
回到开头那个数据竞争问题:ruxe 的答案不是让 Rust 学会通用的“不相等”,而是把问题摆成 Rust 稳定版更擅长处理的样子。
不证明其无,只承认其有且唯一。这个换问法,才是本文最值得带走的东西。
