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”Each step receives the current state and returns the next state alongside its result. There’s no
shared variable; the state flows through the chain explicitly, and nothing runs until you call
State.run at the end:
The return tuple [value, nextState] makes the state transition explicit — no side effects, no
mutation.
Creating State computations
Section titled “Creating State computations”State.get reads the current state as the produced value; State.modify updates it. These are
the two you’ll reach for most often:
State.gets projects a field from the state — useful when your state is a record and you only need
one piece. State.put replaces the state entirely. State.resolve lifts a plain value without
touching state. These are less common; get and modify cover most cases.
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.
If you build up a chain and forget to call State.run at the end, you have a function, not a
value — nothing runs and no type error tells you why.
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.