并行 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 下的结果现实判断
AllDistinctreducer 的 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 没有 reducertrait bound 不满足管线不完整
某个 slice 匹配到两个 reducerimpl 歧义写入目标重复
每个 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 稳定版更擅长处理的样子。

不证明其无,只承认其有且唯一。这个换问法,才是本文最值得带走的东西。