better-py

State Monad

Thread state through computations functionally

The State monad represents stateful computations in a pure functional way. Instead of using mutable state, you thread state through a chain of operations explicitly, making state changes visible and predictable.

State[S, A] is a function from state S to a tuple of (value, new_state). The value is the computation result, and the new_state is the updated state.

Creating & Accessing State

Wrapping State Functions

The State constructor wraps a function that receives the current state and returns a value with the new state.

from better_py import State

# A stateful computation
increment = State(lambda count: (count, count + 1))

# Run with initial state
result, new_state = increment.run(0)
result      # 0
new_state   # 1

Use the constructor when you need full control over how state transforms.

Getting Current State

The State.get method returns the current state as both the value and the new state.

from better_py import State

get_state = State.get()
value, state = get_state.run(42)
value   # 42
state   # 42

Use this when you need to read the state without modifying it.

Replacing State

The State.put method replaces the state with a new value.

from better_py import State

set_state = State.put(100)
value, state = set_state.run(0)
value   # None
state   # 100

Use this when you need to completely replace the state.

Modifying State

The State.modify method applies a function to transform the state.

from better_py import State

increment = State.modify(lambda x: x + 1)
value, state = increment.run(5)
value   # None
state   # 6

Use this when you need to update the state based on its current value.

Running State

Getting Value and State

The run method executes the state computation and returns both the result and final state.

from better_py import State

state = State(lambda s: (s * 2, s + 1))
value, new_state = state.run(5)
value       # 10
new_state   # 6

This is the most common way to execute state computations when you need both results.

Getting Only the Value

The eval method returns only the computation result, discarding the final state.

from better_py import State

state = State(lambda s: (s * 2, s + 1))
value = state.eval(5)  # 10

Use this when you care about the result but not the final state.

Getting Only the Final State

The execute method returns only the final state, discarding the computation result.

from better_py import State

state = State(lambda s: (s * 2, s + 1))
final_state = state.execute(5)  # 6

Use this when you care about the state transformation but not the intermediate result.

Transforming State

Mapping Values

The map method transforms the computation result without affecting state.

from better_py import State

state = State(lambda s: (s, s + 1))
mapped = state.map(lambda x: x * 2)
value, new_state = mapped.run(5)
value       # 10
new_state   # 6

Use map when you need to transform the result without touching the state.

Chaining Operations

The flat_map method chains state operations, threading state through each step.

from better_py import State

def increment(s):
    return (s, s + 1)

def double(s):
    return (s * 2, s)

state = State(increment).flat_map(lambda x: State(double))
value, new_state = state.run(5)
value       # 6
new_state   # 6

Use flat_map for sequential operations where later steps depend on earlier results.

Real-World Pattern: Key-Value Store

from better_py import State

def get(key):
    return State.get().map(lambda store: store.get(key))

def put(key, value):
    return State.modify(lambda store: {**store, key: value})

def delete(key):
    return State.modify(lambda store: {k: v for k, v in store.items() if k != key})

# Store operations
operations = (put("name", "Alice")
    .flat_map(lambda _: put("age", 30))
    .flat_map(lambda _: get("name"))
    .flat_map(lambda name: put("greeting", f"Hello, {name}"))
    .flat_map(lambda _: get("greeting")))

value, state = operations.run({})
value   # "Hello, Alice"
state   # {"name": "Alice", "age": 30, "greeting": "Hello, Alice"}

This pattern shows State's strength for managing complex state transitions: each operation modifies the store functionally, and the state is threaded through automatically without mutable variables.

When to Use

Use State when:

  • You need to manage state functionally
  • You want to avoid mutable variables
  • You're building pure functional code
  • You need to thread state through operations
  • You want explicit state management
  • You're implementing algorithms with state

Don't use State when:

  • Mutable state is acceptable (use plain variables)
  • State doesn't change between operations
  • You're writing simple scripts
  • The state complexity doesn't warrant the abstraction

See Also

  • Reader - For dependency injection
  • Writer - For logging/accumulation
  • IO - For side effect management

On this page