State — threading state through pipelines
Pure functions don’t mutate. Yet many real operations naturally require state: generating sequential
IDs, building up a data structure one step at a time, simulating a counter or stack, threading
configuration through a pipeline. The usual alternatives are to pass the state as an extra argument
to every function, or to reach for a mutable variable. Neither is satisfying. State<S, A> offers
a third option: describe the stateful computation as a value, then run it once at the end.
The problem with threading state manually
Section titled “The problem with threading state manually”When state must flow through several steps, the usual approaches either tangle the signature of every function or introduce shared mutation:
The first approach is verbose and breaks when you add or remove a step — every caller must be updated. The second replaces a typing burden with a correctness burden: nothing stops two functions from racing on the shared variable.
What a State is
Section titled “What a State is”A State<S, A> is a function that takes an initial state of type S and produces both a value A
and a new (or unchanged) state. Nothing runs until you explicitly provide the initial state. The
return tuple [value, nextState] makes the state transition explicit — there is no implicit shared
mutable variable.
Creating State computations
Section titled “Creating State computations”State.resolve lifts a pure value without touching the state:
State.get reads the current state as the produced value:
State.gets reads a projection of the state — useful when your state type is a record and you only
need one field:
State.put replaces the state entirely:
State.modify applies a function to the state to produce the next state, similar to how Array
reducers work:
Transforming with map
Section titled “Transforming with map”map changes the value a computation produces without affecting the state transition:
Sequencing with chain
Section titled “Sequencing with chain”chain is where State earns its keep. It threads the output state of one computation into the
input of the next, so you can write a sequence of stateful steps without passing the state
explicitly at each one:
Each call to push extends the stack in turn. The final State.get reads the accumulated result.
Here is a more realistic example: building a shopping cart by chaining item additions:
Running a State computation
Section titled “Running a State computation”Three runners extract results from a State:
State.run returns both the value and the final state as a tuple:
State.evaluate returns only the produced value — use this when you care about the result but not
the final state:
State.execute returns only the final state — use this when you care about the side-effect on the
state but not the value:
Generating sequential IDs
Section titled “Generating sequential IDs”A common use case for State is generating unique integer IDs while building a data structure:
The counter starts at 0 and is incremented by each call to nextId. The final value is the list of
nodes — the counter itself is discarded.
When to use State
Section titled “When to use State”- You have a sequence of operations that need to read and update a shared piece of state without passing it as an extra parameter to every function.
- You want to model stateful algorithms (stack machines, counters, ID generators, config accumulators) as pure pipelines.
- You need a description of a stateful computation that you can pass around and run later with different initial states.
Keep using plain variables when the state is local to a single function body with no composition
need — a simple let count = 0; count++ is clearer than State.modify(n => n + 1) when there is
nothing to compose. State earns its place when the stateful steps are themselves functions that
you want to name, reuse, or pass around before running.