Pangram verdict · v3.3
We believe that this document is primarily AI-generated with some human-written content
AI likelihood · overall
AIArticle text · 1,865 words · 6 segments analyzed
How I taught Rust’s type system to refuse my own parallel-Redux data races, with one false start and one mind-shift.
There’s a class of bug I’ve spent more nights chasing than I care to remember. The kind that only happens under load, vanishes when you attach a debugger, and takes three engineers a weekend to corner. Data races. Rust’s borrow checker prevents most of them at the value level. But not all of them, and definitely not the question that interested me here: can the compiler refuse to build a parallel reducer pipeline where two reducers might write to the same piece of state? Turns out yes. This post is the story of how I got there in ruxe, my Redux-flavored Rust learning library. What is Redux? Redux is a state management pattern. It got famous in frontend JavaScript but the shape is more general; any system where state changes through discrete events fits the model. flowchart LR User([User code]) -->|dispatch event| Store Store -->|state + event| Reducer Reducer -->|new state| Store Store -->|read| User
Three rules make Redux what it is. One: a single source of truth. The state is owned by the store, nothing else. Two: the state is immutable from outside. You don’t poke it directly. You dispatch events (the JS world calls them “actions”) that describe what happened. Three: state transitions go through a pure reducer, a function (state, event) → new state. Same input, same output, no side effects. That third rule is what makes Redux famously debuggable. Record the event stream, replay it, and you get the same final state every time. Time-travel debugging. Crash diagnosis from production traces. Reproducible bugs. Anyone who has ever tried to debug a stateful UI without that property knows why people keep reinventing Redux. Why I cared At my day job I work on energy-management systems running on industrial sites. One brick of the stack uses a Redux-like pattern in Python: the control layer, where several controllers run concurrently and need a shared, consistent view of the plant state. Events flow in, a reducer pipeline computes the new state, controllers read from it. The numbers add up faster than you’d think.
A typical site has dozens of pieces of equipment, each polled on its own period, and some periods go down to 50ms. Each device can publish several dozen registers per read. That’s a continuous, high-volume event stream, and a pure Python Redux struggles to keep up. We worked around it. We cache reads and trigger the reduction at a coarser frequency than the underlying telemetry. It works, but the pattern gets less pure: the state is no longer up-to-date with the latest telemetry, and we trade away part of what made Redux attractive in the first place. Profiling told us where the time was actually spent: the reducer phase. The plant has many independent subsystems. Solar, battery, meter, grid controller, and so on. Each subsystem holds a slice of the global state, and each has its own reducer that only touches its slice. flowchart LR Event[Event] --> R1[Solar reducer] --> S1[Solar slice] Event --> R2[Battery reducer] --> S2[Battery slice] Event --> R3[Meter reducer] --> S3[Meter slice]
Here’s the observation that started everything: the reducers are independent. Solar’s reducer never touches the battery slice. Battery’s reducer never touches the meter slice. Each event is a single pass through all of them. Run them sequentially and you get N times the latency. Run them in parallel and you get max(latency_i). So: parallelize the reducers. Same event flow from the outside, same deterministic results, just N times less wall-clock time per dispatch. There’s one catch. Parallel plus shared state equals data races. If a reducer accidentally writes to another slice, you have a classic concurrency bug. Non-deterministic outputs, heisenbugs in production, debug sessions you’ll remember on your deathbed. In most languages, that’s where the type system gives up. C++ hands you mutexes and atomics and wishes you luck. Higher-level languages give you synchronisation primitives and a memory model, but the burden of avoiding races stays on the developer; the compiler doesn’t enforce anything, the team’s discipline does. Rust’s type system can encode the property directly. The compiler itself can refuse to build code that would race. That’s what I set out to prove in ruxe.
The mental model: slices and disjointness Two concepts power everything that follows. Worth defining clearly before we go further. A slice is a sub-field of the state. If your state holds a counter, a user, and a notifications field, those are three slices. A slice reducer is a reducer that only sees its slice. It cannot touch other slices. The type system forbids it: the function signature exposes only &Slice, nothing else. flowchart LR subgraph State SA[counter] SB[user] SC[notifications] end R1[CounterReducer] -.touches.-> SA R2[UserReducer] -.touches.-> SB R3[NotificationsReducer] -.touches.-> SC
The property we want is disjointness: for any pair of slice reducers in our setup, they target different slices. No two reducers ever touch the same slice. flowchart LR subgraph Disjoint["✅ Disjoint — safe to parallelize"] D1[Reducer A] -.-> SA1[Slice A] D2[Reducer B] -.-> SB1[Slice B] end subgraph NotDisjoint["❌ Overlap — data race possible"] N1[Reducer A] -.-> SX[Slice X] N2[Reducer B] -.-> SX end
Disjointness is necessary and sufficient for safe parallel execution. If it holds, parallel is sound. If it doesn’t, parallel races. The engineering question becomes: can I get the compiler to refuse a parallel root reducer that violates disjointness? First idea: AllDistinct Here’s what I had in mind when I started, and why I thought it would work. I came to Rust from C++. In C++, this kind of check is template metaprogramming bread and butter. std::is_same_v<H, T> gives you type equality at compile time. !std::is_same_v<H, T> gives you inequality. You wrap that in a static_assert (or a concept in C++20) and the compiler does the rest.
The pattern transfers naturally to any sufficiently rich type system. Rust looked similar enough on the surface. Recursive trait impls are everywhere. Vec<T> is Clone if T is Clone. Option<T> is Send if T is Send. The pattern of “this type has property P if its parts have property P” is built into the language. So if I write a list of reducer types, say a tuple (R1, R2, R3), I should be able to define a trait AllDistinct that holds when no two Ri::Slice types are the same. Walk the tuple recursively. At each step, check that the head’s slice is different from every slice in the tail. Recurse. The shape I had in mind, in pseudo-Rust (real tuples can’t be recursed over like this; I was hand-waving the walking part, which the negation wall ends up making moot anyway): trait AllDistinct {}
impl AllDistinct for () {} // empty tuple is trivially distinct
impl<H, Tail> AllDistinct for (H, Tail) where Tail: AllDistinct, H: NotIn<Tail>, // and H is not in the tail {} Looks reasonable. The recursion bottoms out on the empty tuple. Each step requires the rest to be distinct and the head to not be in the rest. Standard structural recursion. I built it. Got to NotIn<H>. Froze. NotIn<H> requires saying “for every element X in the tail, H ≠ X”. In Rust syntax, that would be something like: trait NotIn<T> {}
impl<H, Tail, T> NotIn<T> for (H, Tail) where Tail: NotIn<T>, H !
= T, // ← this is not a thing {} The compiler: error: expected one of `!`, `(`, `+`, `::`, `:`, `<`, `==`, or `=`, found `!=` Stable Rust has no syntax for type inequality in bounds. No H != T. No way to write “this trait is implemented if that other one is not implemented”. The unstable feature negative_impls has existed for years but stays parked behind coherence concerns. Don’t hold your breath. This isn’t just a missing operator. The trait system reasons about impls monotonically: adding more impls in the codebase can only enable more code to compile, never disable any existing code. Negative reasoning (“this trait is not implemented”) would break that property: someone adding a new positive impl could retroactively invalidate code elsewhere that relied on the absence. Rust sidesteps the whole class of issues by not allowing the reasoning in the first place. This is where Rust diverges sharply from C++. In C++, !std::is_same_v<H, T> is a value you can compute and static_assert on. The equivalent in Rust has no surface syntax, and the reason isn’t carelessness — it’s a design choice. AllDistinct, in stable Rust, isn’t expressible. At least not the shape I sketched. The pivot I sat with this for a while. The wall was real. But sometimes a wall is just the wrong door. I was trying to prove a negative (no duplicates). Rust speaks positives. What if I reformulated the same property the other way around?
Formulation Form In stable Rust
“No two reducers target the same slice” Negative Impossible (needs negation)
“Each state slice has exactly one matching reducer” Positive (bijection) Achievable (existence of impls)
These are logically equivalent: a bijection between slices and reducers means each slice has exactly one match and no two reducers share a slice. The second condition falls out of the first. So instead of asking “are there duplicates?”, I should ask “is there a perfect matching?”. The first question requires the impossible negation. The second only asks for the existence of certain trait impls, which is Rust’s bread and butter.
Concretely: walk the state’s slice list (which the user declares explicitly), and for each slice type, find the reducer in the tuple whose Slice associated type matches it. Three outcomes:
Lookup result Action Compile-time outcome
Exactly one reducer matches Use it, move on OK
Two or more match — Ambiguous impl error
Zero match — Trait bound not satisfied
The errors come from Rust’s trait coherence rules. I don’t have to write explicit checks; the trait resolver enforces them on every lookup it performs. HList: the right tool To walk a slice list at compile time I need a structure the compiler can walk recursively. Tuples don’t qualify: each arity is a distinct type, and you’d need a separate impl for (A,), (A, B), (A, B, C), and so on. What I want is a list whose recursive structure is part of its type, so trait resolution can descend it on its own. That structure exists in the Rust ecosystem. It’s called an HList, and it’s a known pattern. A heterogeneous list is what it sounds like. A linked list where each cell can hold a value of a different type. The shape: struct HCons<H, T> { head: H, tail: T } struct HNil; If you squint, this is a Lisp cons cell, but typed and at the type level. A three-element HList looks like: HCons<i32, HCons<String, HCons<f64, HNil>>> // ^ ^ ^ ^ // head nested cons nested cons terminator A macro lets you write this less painfully: HList!(i32, String, f64) // expands to: // HCons<i32, HCons<String, HCons<f64, HNil>>> Visually it’s a nested doll: flowchart LR L1[HCons] --> H1[i32] & T1[HCons] T1 --> H2[String] & T2[HCons] T2 --> H3[f64] & T3[HNil]
This structure is what tuples can’t be. Walkable by the compiler via recursive trait impls.