State monad
Game engines, pure state management
____ _____ _ _____ _____ __ __ ___ _ _ _ ____ ____
/ ___|_ _|/ \|_ _| ____| | \/ |/ _ \| \ | | / \ | _ \/ ___|
\___ \ | | / _ \ | | | _| | |\/| | | | | \| | / _ \ | | | \___ \
___) || |/ ___ \| | | |___ | | | | |_| | |\ |/ ___ \| |_| |___) |
|____/ |_/_/ \_\_| |_____| |_| |_|\___/|_| \_/_/ \_\____/|____/
So, you've mastered monads. You can chain computations like a pro, handle errors gracefully, and your code flows like poetry. You're feeling pretty good about yourself, maybe even a little smug. Then someone mentions "state management" and suddenly you're back to playing hot potato with mutable variables, passing state objects around like they're contagious, and wondering if there's a better way.
Well, good news! There is. Meet the State monad[1]. It has a Reader-like side, because each step can inspect the current state, and a Writer-like side, because each step produces state for the next one. The important difference is that Writer accumulates output with a monoid, while State threads one evolving state value through a sequence of computations.
The State monad is like having a really good secretary who not only keeps track of everything for you but also knows exactly what you need next. No more "wait, what was I doing again?" moments. No more accidentally overwriting important data. Just smooth, stateful computations that feel natural and composable.
The Problem #
Imagine you're building a video game where you need to track the player's health, score, inventory, and position. Every action—collecting items, taking damage, moving—needs to modify this state while also potentially returning some result.
// Manual state management approach
class GameState {
health: number = 100;
score: number = 0;
inventory: string[] = [];
position: { x: number, y: number } = { x: 0, y: 0 };
}
function collectItem(state: GameState, item: string): { result: string, newState: GameState } {
const newState = { ...state };
newState.inventory.push(item);
newState.score += 10;
return { result: `Collected ${item}`, newState };
}
function takeDamage(state: GameState, damage: number): { result: string, newState: GameState } {
const newState = { ...state };
newState.health = Math.max(0, newState.health - damage);
return { result: newState.health > 0 ? "Still alive" : "Game over", newState };
}
function movePlayer(state: GameState, dx: number, dy: number): { result: string, newState: GameState } {
const newState = { ...state };
newState.position.x += dx;
newState.position.y += dy;
return { result: `Moved to (${newState.position.x}, ${newState.position.y})`, newState };
}
// Manual state threading
function playGame(): GameState {
let state = new GameState();
let result1 = collectItem(state, "sword");
state = result1.newState;
console.log(result1.result);
let result2 = takeDamage(state, 20);
state = result2.newState;
console.log(result2.result);
let result3 = movePlayer(state, 5, 3);
state = result3.newState;
console.log(result3.result);
return state;
}
This approach works, but it's tedious and error-prone. We have to manually thread the state through every operation, remember to use the updated state, and handle the state transformation boilerplate repeatedly.
What if we could abstract away the state threading and focus on the core game logic? This is exactly what the State monad provides.
From Reader and Writer to State #
In our monads article, we explored how Reader applicative and Writer applicative handle different aspects of computation:
- Reader: Computations that consume a shared environment
- Writer: Computations that produce accumulated output
The State monad brings both intuitions into one state-threading pattern:
- Read the current state (like Reader)
- Produce the next state (Writer-like, but replacing/threading rather than accumulating)
- Thread state sequentially through computations (monadic chaining)
Categorical Perspective #
From a category theory perspective, the State monad State s a represents computations that:
- Input: Take an initial state of type
s - Output: Produce a result of type
aand a new state of types - Structure:
State s a ≅ s -> (a, s)- a function from state to a pair of result and new state
This captures the essence of stateful computation: functions that transform state while producing results.
State Monad Definition #
The State monad is defined as:
newtype State s a = State { runState :: s -> (a, s) }
Where:
sis the type of the stateais the type of the result valuerunStateunwraps the state computation function
Core Operations #
There are three fundamental operations:
- get: Read the current state
- put: Replace the state
- modify: Update the state with a function
-- Get the current state
get :: State s s
get = State $ \s -> (s, s)
-- Replace the state
put :: s -> State s ()
put newState = State $ \_ -> ((), newState)
-- Modify the state with a function
modify :: (s -> s) -> State s ()
modify f = State $ \s -> ((), f s)
Monad Instance #
instance Functor (State s) where
fmap f (State g) = State $ \s ->
let (a, s') = g s
in (f a, s')
instance Applicative (State s) where
pure a = State $ \s -> (a, s)
State f <*> State g = State $ \s ->
let (func, s') = f s
(a, s'') = g s'
in (func a, s'')
instance Monad (State s) where
return = pure
State g >>= f = State $ \s ->
let (a, s') = g s
State h = f a
in h s'
The key insight is in the bind operation: it runs the first computation, extracts both the result and the new state, passes the result to the next computation, and threads the state through the sequence.
Examples #
Let's implement the State monad[3] in different languages and see how it simplifies our game example.
import Control.Monad.State
-- Game state type
data GameState = GameState
{ health :: Int
, score :: Int
, inventory :: [String]
, position :: (Int, Int)
} deriving (Show)
-- Initial game state
initialState :: GameState
initialState = GameState 100 0 [] (0, 0)
-- State operations using the State monad
collectItem :: String -> State GameState String
collectItem item = do
state <- get
put $ state { inventory = item : inventory state
, score = score state + 10 }
return $ "Collected " ++ item
takeDamage :: Int -> State GameState String
takeDamage damage = do
state <- get
let newHealth = max 0 (health state - damage)
put $ state { health = newHealth }
return $ if newHealth > 0 then "Still alive" else "Game over"
movePlayer :: Int -> Int -> State GameState String
movePlayer dx dy = do
state <- get
let (x, y) = position state
newPos = (x + dx, y + dy)
put $ state { position = newPos }
return $ "Moved to " ++ show newPos
-- Monadic composition - state is automatically threaded
playGame :: State GameState [String]
playGame = do
msg1 <- collectItem "sword"
msg2 <- takeDamage 20
msg3 <- movePlayer 5 3
return [msg1, msg2, msg3]
-- Run the game
main :: IO ()
main = do
let (messages, finalState) = runState playGame initialState
mapM_ putStrLn messages
putStrLn $ "Final state: " ++ show finalState
// State monad implementation
class State<S, A> {
constructor(private runState: (state: S) => [A, S]) {}
// Run the state computation
run(initialState: S): [A, S] {
return this.runState(initialState);
}
// Extract just the result, discarding final state
eval(initialState: S): A {
return this.run(initialState)[0];
}
// Extract just the final state, discarding result
exec(initialState: S): S {
return this.run(initialState)[1];
}
// Functor: map over the result value
map<B>(f: (a: A) => B): State<S, B> {
return new State(s => {
const [a, newS] = this.runState(s);
return [f(a), newS];
});
}
// Monad: bind/flatMap
flatMap<B>(f: (a: A) => State<S, B>): State<S, B> {
return new State(s => {
const [a, newS] = this.runState(s);
return f(a).run(newS);
});
}
// Utility for chaining with ignored results
then<B>(next: State<S, B>): State<S, B> {
return this.flatMap(_ => next);
}
// Static constructor for pure values
static pure<S, A>(value: A): State<S, A> {
return new State(s => [value, s]);
}
// Static constructor for state operations
static get<S>(): State<S, S> {
return new State(s => [s, s]);
}
static put<S>(newState: S): State<S, void> {
return new State(_ => [undefined, newState]);
}
static modify<S>(f: (s: S) => S): State<S, void> {
return new State(s => [undefined, f(s)]);
}
}
// Game state interface
interface GameState {
health: number;
score: number;
inventory: string[];
position: { x: number, y: number };
}
const initialGameState: GameState = {
health: 100,
score: 0,
inventory: [],
position: { x: 0, y: 0 }
};
// Game operations using State monad
function collectItem(item: string): State<GameState, string> {
return State.get<GameState>()
.flatMap(state =>
State.put<GameState>({
...state,
inventory: [...state.inventory, item],
score: state.score + 10
})
)
.map(() => `Collected ${item}`);
}
function takeDamage(damage: number): State<GameState, string> {
return State.get<GameState>()
.flatMap(state => {
const newHealth = Math.max(0, state.health - damage);
return State.put<GameState>({ ...state, health: newHealth })
.map(() => newHealth > 0 ? "Still alive" : "Game over");
});
}
function movePlayer(dx: number, dy: number): State<GameState, string> {
return State.get<GameState>()
.flatMap(state => {
const newPosition = {
x: state.position.x + dx,
y: state.position.y + dy
};
return State.put<GameState>({ ...state, position: newPosition })
.map(() => `Moved to (${newPosition.x}, ${newPosition.y})`);
});
}
// Monadic game sequence
function playGame(): State<GameState, string[]> {
return collectItem("sword")
.flatMap(msg1 =>
takeDamage(20)
.flatMap(msg2 =>
movePlayer(5, 3)
.map(msg3 => [msg1, msg2, msg3])
)
);
}
// Alternative with async-like syntax using generators
function* playGameGenerator(): Generator<State<GameState, string>, string[], any> {
const msg1 = yield collectItem("sword");
const msg2 = yield takeDamage(20);
const msg3 = yield movePlayer(5, 3);
return [msg1, msg2, msg3];
}
// Run the game
const gameResult = playGame().run(initialGameState);
console.log("Messages:", gameResult[0]);
console.log("Final state:", gameResult[1]);
using System;
using System.Collections.Generic;
using System.Linq;
// State monad implementation
public class State<S, A>
{
private readonly Func<S, (A Result, S NewState)> computation;
public State(Func<S, (A, S)> computation)
{
this.computation = computation;
}
// Run the state computation
public (A Result, S NewState) Run(S initialState)
{
return computation(initialState);
}
// Extract just the result
public A Eval(S initialState)
{
return Run(initialState).Result;
}
// Extract just the final state
public S Exec(S initialState)
{
return Run(initialState).NewState;
}
// Functor: map over the result
public State<S, B> Map<B>(Func<A, B> f)
{
return new State<S, B>(s =>
{
var (result, newState) = computation(s);
return (f(result), newState);
});
}
// Monad: bind/flatMap
public State<S, B> FlatMap<B>(Func<A, State<S, B>> f)
{
return new State<S, B>(s =>
{
var (result, newState) = computation(s);
return f(result).Run(newState);
});
}
// LINQ Support
public State<S, B> SelectMany<B>(Func<A, State<S, B>> f)
{
return FlatMap(f);
}
public State<S, C> SelectMany<B, C>(Func<A, State<S, B>> f, Func<A, B, C> projection)
{
return FlatMap(a => f(a).Map(b => projection(a, b)));
}
public State<S, B> Select<B>(Func<A, B> f)
{
return Map(f);
}
// Static constructors
public static State<S, A> Pure(A value)
{
return new State<S, A>(s => (value, s));
}
public static State<S, S> Get()
{
return new State<S, S>(s => (s, s));
}
public static State<S, Unit> Put(S newState)
{
return new State<S, Unit>(_ => (Unit.Default, newState));
}
public static State<S, Unit> Modify(Func<S, S> f)
{
return new State<S, Unit>(s => (Unit.Default, f(s)));
}
}
// Unit type for void-like operations
public struct Unit
{
public static readonly Unit Default = new Unit();
}
// Game state class
public class GameState
{
public int Health { get; set; }
public int Score { get; set; }
public List<string> Inventory { get; set; }
public (int X, int Y) Position { get; set; }
public GameState()
{
Health = 100;
Score = 0;
Inventory = new List<string>();
Position = (0, 0);
}
public GameState(GameState other)
{
Health = other.Health;
Score = other.Score;
Inventory = new List<string>(other.Inventory);
Position = other.Position;
}
public override string ToString()
{
return $"GameState {{ Health = {Health}, Score = {Score}, " +
$"Inventory = [{string.Join(", ", Inventory)}], Position = {Position} }}";
}
}
public static class GameOperations
{
// Game operations using State monad
public static State<GameState, string> CollectItem(string item)
{
return from state in State<GameState, GameState>.Get()
from _ in State<GameState, Unit>.Put(new GameState(state)
{
Inventory = state.Inventory.Concat(new[] { item }).ToList(),
Score = state.Score + 10
})
select $"Collected {item}";
}
public static State<GameState, string> TakeDamage(int damage)
{
return from state in State<GameState, GameState>.Get()
let newHealth = Math.Max(0, state.Health - damage)
from _ in State<GameState, Unit>.Put(new GameState(state) { Health = newHealth })
select newHealth > 0 ? "Still alive" : "Game over";
}
public static State<GameState, string> MovePlayer(int dx, int dy)
{
return from state in State<GameState, GameState>.Get()
let newPos = (state.Position.X + dx, state.Position.Y + dy)
from _ in State<GameState, Unit>.Put(new GameState(state) { Position = newPos })
select $"Moved to {newPos}";
}
// Monadic game sequence using LINQ syntax
public static State<GameState, List<string>> PlayGame()
{
return from msg1 in CollectItem("sword")
from msg2 in TakeDamage(20)
from msg3 in MovePlayer(5, 3)
select new List<string> { msg1, msg2, msg3 };
}
}
class Program
{
static void Main()
{
var initialState = new GameState();
var (messages, finalState) = GameOperations.PlayGame().Run(initialState);
Console.WriteLine("Game Messages:");
foreach (var message in messages)
{
Console.WriteLine($" {message}");
}
Console.WriteLine($"\nFinal State: {finalState}");
}
}
Visualizing State Monad #
1. State threads through each computation
State s a = s -> (a, s)
input state state computation result + next state
+-------------+ +-------------------+ +-------------------+
| S0 | --> | run step1 with S0 | --> | (A1, S1) |
+-------------+ +-------------------+ +---------+---------+
|
| thread S1
v
+-------------------+ +-------------------+
| run step2 with S1 | --> | (A2, S2) |
+-------------------+ +---------+---------+
|
| thread S2
v
+-------------------+ +-------------------+
| run step3 with S2 | --> | (A3, S3) |
+-------------------+ +-------------------+
The result value A is exposed to the next bind.
The state value S is quietly passed forward.
2. Game example flow
+----------------------------------------------------------------+
| health: 100 | score: 0 | inventory: [] | pos: (0, 0) |
+----------------------------------------------------------------+
|
| collectItem("sword")
| result: "Collected sword"
v
+----------------------------------------------------------------+
| health: 100 | score: 10 | inventory: ["sword"] | pos: (0, 0) |
+----------------------------------------------------------------+
|
| takeDamage(20)
| result: "Still alive"
v
+----------------------------------------------------------------+
| health: 80 | score: 10 | inventory: ["sword"] | pos: (0, 0) |
+----------------------------------------------------------------+
|
| movePlayer(5, 3)
| result: "Moved to (5, 3)"
v
+----------------------------------------------------------------+
| health: 80 | score: 10 | inventory: ["sword"] | pos: (5, 3) |
+----------------------------------------------------------------+
3. Manual threading vs bind
Manual threading:
state0
|
v
step1(state0) -> (result1, state1)
|
v
step2(state1) -> (result2, state2)
|
v
step3(state2) -> (result3, state3)
Every call must use the state returned by the previous call.
State monad:
step1 >>= step2 >>= step3
bind runs each step,
passes its result to the next function,
and threads the new state into the next computation.
Common State Monad Patterns #
Accumulator Pattern #
The accumulator pattern uses the State monad to maintain running totals, counts, or other accumulated values while processing data. In Haskell, these examples use Control.Monad.State[2].
import Control.Monad.State
-- Sum all numbers while keeping track of count
sumWithCount :: [Int] -> State (Int, Int) Int
sumWithCount [] = do
(total, _) <- get
return total
sumWithCount (x:xs) = do
(total, count) <- get
put (total + x, count + 1)
sumWithCount xs
-- Usage example
main :: IO ()
main = do
let numbers = [1,2,3,4,5]
(result, finalState) = runState (sumWithCount numbers) (0, 0)
putStrLn $ "Sum: " ++ show result
putStrLn $ "Final state (sum, count): " ++ show finalState
-- Output: Sum: 15, Final state: (15, 5)
// State monad implementation for the accumulator pattern
class State<S, A> {
constructor(private runState: (state: S) => [A, S]) {}
run(initialState: S): [A, S] {
return this.runState(initialState);
}
map<B>(f: (a: A) => B): State<S, B> {
return new State(s => {
const [a, newS] = this.runState(s);
return [f(a), newS];
});
}
flatMap<B>(f: (a: A) => State<S, B>): State<S, B> {
return new State(s => {
const [a, newS] = this.runState(s);
return f(a).run(newS);
});
}
then<B>(next: State<S, B>): State<S, B> {
return this.flatMap(_ => next);
}
static pure<S, A>(value: A): State<S, A> {
return new State(s => [value, s]);
}
static get<S>(): State<S, S> {
return new State(s => [s, s]);
}
static put<S>(newState: S): State<S, void> {
return new State(_ => [undefined, newState]);
}
}
// Accumulator state type
interface AccumulatorState {
sum: number;
count: number;
}
// Sum with count using State monad
function sumWithCount(numbers: number[]): State<AccumulatorState, number> {
if (numbers.length === 0) {
return State.get<AccumulatorState>().map(state => state.sum);
}
const [head, ...tail] = numbers;
return State.get<AccumulatorState>()
.flatMap(state =>
State.put<AccumulatorState>({
sum: state.sum + head,
count: state.count + 1
})
.then(sumWithCount(tail))
);
}
// Usage example
const numbers = [1, 2, 3, 4, 5];
const initialState: AccumulatorState = { sum: 0, count: 0 };
const [result, finalState] = sumWithCount(numbers).run(initialState);
console.log(`Sum: ${result}`);
console.log(`Final state:`, finalState);
// Output: Sum: 15, Final state: { sum: 15, count: 5 }
using System;
using System.Collections.Generic;
using System.Linq;
// State monad implementation for the accumulator pattern
public class State<S, A>
{
private readonly Func<S, (A Result, S NewState)> computation;
public State(Func<S, (A, S)> computation)
{
this.computation = computation;
}
public (A Result, S NewState) Run(S initialState)
{
return computation(initialState);
}
public State<S, B> Map<B>(Func<A, B> f)
{
return new State<S, B>(s =>
{
var (result, newState) = computation(s);
return (f(result), newState);
});
}
public State<S, B> FlatMap<B>(Func<A, State<S, B>> f)
{
return new State<S, B>(s =>
{
var (result, newState) = computation(s);
return f(result).Run(newState);
});
}
// LINQ Support
public State<S, B> SelectMany<B>(Func<A, State<S, B>> f)
{
return FlatMap(f);
}
public State<S, C> SelectMany<B, C>(Func<A, State<S, B>> f, Func<A, B, C> projection)
{
return FlatMap(a => f(a).Map(b => projection(a, b)));
}
public State<S, B> Select<B>(Func<A, B> f)
{
return Map(f);
}
public static State<S, A> Pure(A value)
{
return new State<S, A>(s => (value, s));
}
public static State<S, S> Get()
{
return new State<S, S>(s => (s, s));
}
public static State<S, Unit> Put(S newState)
{
return new State<S, Unit>(_ => (Unit.Default, newState));
}
}
// Unit type for void-like operations
public struct Unit
{
public static readonly Unit Default = new Unit();
}
// Accumulator state
public class AccumulatorState
{
public int Sum { get; set; }
public int Count { get; set; }
public AccumulatorState(int sum = 0, int count = 0)
{
Sum = sum;
Count = count;
}
public override string ToString() => $"AccumulatorState(Sum: {Sum}, Count: {Count})";
}
public static class AccumulatorExample
{
// Sum with count using State monad
public static State<AccumulatorState, int> SumWithCount(IEnumerable<int> numbers)
{
var numbersList = numbers.ToList();
if (!numbersList.Any())
{
return State<AccumulatorState, AccumulatorState>.Get()
.Select(state => state.Sum);
}
var head = numbersList.First();
var tail = numbersList.Skip(1);
return from state in State<AccumulatorState, AccumulatorState>.Get()
from _ in State<AccumulatorState, Unit>.Put(new AccumulatorState(state.Sum + head, state.Count + 1))
from result in SumWithCount(tail)
select result;
}
// Usage example
public static void Main()
{
var numbers = new[] { 1, 2, 3, 4, 5 };
var initialState = new AccumulatorState();
var (result, finalState) = SumWithCount(numbers).Run(initialState);
Console.WriteLine($"Sum: {result}");
Console.WriteLine($"Final state: {finalState}");
// Output: Sum: 15, Final state: AccumulatorState(Sum: 15, Count: 5)
}
}
Generator Pattern #
The generator pattern uses the State monad to generate sequences of unique values, such as IDs or random numbers.
import Control.Monad.State
-- ID generator state
newtype IdGen = IdGen { nextId :: Int } deriving (Show)
-- Generate a single unique ID
generateId :: State IdGen Int
generateId = do
IdGen current <- get
put $ IdGen (current + 1)
return current
-- Generate multiple IDs
generateIds :: Int -> State IdGen [Int]
generateIds 0 = return []
generateIds n = do
firstId <- generateId
restIds <- generateIds (n - 1)
return (firstId : restIds)
-- Usage example
main :: IO ()
main = do
let initialGen = IdGen 1
(ids, finalGen) = runState (generateIds 5) initialGen
putStrLn $ "Generated IDs: " ++ show ids
putStrLn $ "Final generator state: " ++ show finalGen
-- Output: Generated IDs: [1,2,3,4,5], Final generator state: IdGen {nextId = 6}
class State<S, A> {
constructor(private runState: (state: S) => [A, S]) {}
run(initialState: S): [A, S] {
return this.runState(initialState);
}
map<B>(f: (a: A) => B): State<S, B> {
return new State(s => {
const [a, newS] = this.runState(s);
return [f(a), newS];
});
}
flatMap<B>(f: (a: A) => State<S, B>): State<S, B> {
return new State(s => {
const [a, newS] = this.runState(s);
return f(a).run(newS);
});
}
then<B>(next: State<S, B>): State<S, B> {
return this.flatMap(_ => next);
}
static pure<S, A>(value: A): State<S, A> {
return new State(s => [value, s]);
}
static get<S>(): State<S, S> {
return new State(s => [s, s]);
}
static put<S>(newState: S): State<S, void> {
return new State(_ => [undefined, newState]);
}
}
// ID generator state
interface IdGenState {
nextId: number;
}
// Generate a single unique ID
function generateId(): State<IdGenState, number> {
return State.get<IdGenState>()
.flatMap(state => {
const currentId = state.nextId;
return State.put<IdGenState>({ nextId: currentId + 1 })
.map(() => currentId);
});
}
// Generate multiple IDs
function generateIds(count: number): State<IdGenState, number[]> {
if (count <= 0) {
return State.pure<IdGenState, number[]>([]);
}
return generateId()
.flatMap(id =>
generateIds(count - 1)
.map(restIds => [id, ...restIds])
);
}
// Usage example
const initialGen: IdGenState = { nextId: 1 };
const [ids, finalGen] = generateIds(5).run(initialGen);
console.log(`Generated IDs: ${JSON.stringify(ids)}`);
console.log(`Final generator state:`, finalGen);
// Output: Generated IDs: [1,2,3,4,5], Final generator state: { nextId: 6 }
using System;
using System.Collections.Generic;
using System.Linq;
// Unit type for void-like operations
public struct Unit
{
public static readonly Unit Default = new Unit();
}
public class State<S, A>
{
private readonly Func<S, (A Result, S NewState)> computation;
public State(Func<S, (A, S)> computation)
{
this.computation = computation;
}
public (A Result, S NewState) Run(S initialState)
{
return computation(initialState);
}
public State<S, B> Map<B>(Func<A, B> f)
{
return new State<S, B>(s =>
{
var (result, newState) = computation(s);
return (f(result), newState);
});
}
public State<S, B> FlatMap<B>(Func<A, State<S, B>> f)
{
return new State<S, B>(s =>
{
var (result, newState) = computation(s);
return f(result).Run(newState);
});
}
// LINQ Support
public State<S, B> SelectMany<B>(Func<A, State<S, B>> f)
{
return FlatMap(f);
}
public State<S, C> SelectMany<B, C>(Func<A, State<S, B>> f, Func<A, B, C> projection)
{
return FlatMap(a => f(a).Map(b => projection(a, b)));
}
public State<S, B> Select<B>(Func<A, B> f)
{
return Map(f);
}
public static State<S, A> Pure(A value)
{
return new State<S, A>(s => (value, s));
}
public static State<S, S> Get()
{
return new State<S, S>(s => (s, s));
}
public static State<S, Unit> Put(S newState)
{
return new State<S, Unit>(_ => (Unit.Default, newState));
}
}
// ID generator state
public class IdGenState
{
public int NextId { get; set; }
public IdGenState(int nextId = 1)
{
NextId = nextId;
}
public override string ToString() => $"IdGenState(NextId: {NextId})";
}
public static class Program
{
// Generate a single unique ID
public static State<IdGenState, int> GenerateId()
{
return from state in State<IdGenState, IdGenState>.Get()
let currentId = state.NextId
from _ in State<IdGenState, Unit>.Put(new IdGenState(currentId + 1))
select currentId;
}
// Generate multiple IDs
public static State<IdGenState, List<int>> GenerateIds(int count)
{
if (count <= 0)
{
return State<IdGenState, List<int>>.Pure(new List<int>());
}
return from id in GenerateId()
from restIds in GenerateIds(count - 1)
select new List<int> { id }.Concat(restIds).ToList();
}
// Usage example
public static void Main()
{
var initialGen = new IdGenState(1);
var (ids, finalGen) = GenerateIds(5).Run(initialGen);
Console.WriteLine($"Generated IDs: [{string.Join(", ", ids)}]");
Console.WriteLine($"Final generator state: {finalGen}");
// Output: Generated IDs: [1, 2, 3, 4, 5], Final generator state: IdGenState(NextId: 6)
}
}
Parser State Pattern #
The parser pattern uses the State monad to track position and remaining input while parsing structured data.[a]
import Control.Monad.State
import Data.Maybe (isJust)
-- Parser state containing input and position
data ParseState = ParseState
{ input :: String
, position :: Int
} deriving (Show)
-- Parse a specific character
parseChar :: Char -> State ParseState (Maybe Char)
parseChar expected = do
state' <- get
case drop (position state') (input state') of
(c:_) | c == expected -> do
put $ state' { position = position state' + 1 }
return $ Just c
_ -> return Nothing
-- Parse a string by parsing each character
parseString :: String -> State ParseState (Maybe String)
parseString str = do
results <- mapM parseChar str
return $ if all isJust results
then Just str
else Nothing
-- Parse a number (simplified)
parseNumber :: State ParseState (Maybe Int)
parseNumber = do
state' <- get
let remaining = drop (position state') (input state')
digits = takeWhile (`elem` "0123456789") remaining
if null digits
then return Nothing
else do
put $ state' { position = position state' + length digits }
return $ Just (read digits)
-- Usage example
main :: IO ()
main = do
let initialState = ParseState "hello123world" 0
(result1, state1) = runState (parseString "hello") initialState
(result2, state2) = runState parseNumber state1
(result3, finalState) = runState (parseString "world") state2
putStrLn $ "Parsed 'hello': " ++ show result1
putStrLn $ "Parsed number: " ++ show result2
putStrLn $ "Parsed 'world': " ++ show result3
putStrLn $ "Final state: " ++ show finalState
-- Output: Parsed 'hello': Just "hello", Parsed number: Just 123, etc.
class State<S, A> {
constructor(private runState: (state: S) => [A, S]) {}
run(initialState: S): [A, S] {
return this.runState(initialState);
}
map<B>(f: (a: A) => B): State<S, B> {
return new State(s => {
const [a, newS] = this.runState(s);
return [f(a), newS];
});
}
flatMap<B>(f: (a: A) => State<S, B>): State<S, B> {
return new State(s => {
const [a, newS] = this.runState(s);
return f(a).run(newS);
});
}
then<B>(next: State<S, B>): State<S, B> {
return this.flatMap(_ => next);
}
static pure<S, A>(value: A): State<S, A> {
return new State(s => [value, s]);
}
static get<S>(): State<S, S> {
return new State(s => [s, s]);
}
static put<S>(newState: S): State<S, void> {
return new State(_ => [undefined, newState]);
}
}
// Parser state containing input and position
interface ParseState {
input: string;
position: number;
}
// Parse a specific character
function parseChar(expected: string): State<ParseState, string | null> {
return State.get<ParseState>()
.flatMap(state => {
const remaining = state.input.slice(state.position);
if (remaining.length > 0 && remaining[0] === expected) {
return State.put<ParseState>({
input: state.input,
position: state.position + 1
}).map(() => expected);
} else {
return State.pure<ParseState, string | null>(null);
}
});
}
// Parse a string by parsing each character
function parseString(str: string): State<ParseState, string | null> {
if (str.length === 0) {
return State.pure<ParseState, string | null>(str);
}
const [head, ...tail] = str;
return parseChar(head)
.flatMap(charResult => {
if (charResult === null) {
return State.pure<ParseState, string | null>(null);
}
return parseString(tail.join(''))
.map(tailResult => tailResult === null ? null : str);
});
}
// Parse a number (simplified)
function parseNumber(): State<ParseState, number | null> {
return State.get<ParseState>()
.flatMap(state => {
const remaining = state.input.slice(state.position);
const digits = remaining.match(/^\d+/);
if (digits === null) {
return State.pure<ParseState, number | null>(null);
}
const digitStr = digits[0];
return State.put<ParseState>({
input: state.input,
position: state.position + digitStr.length
}).map(() => parseInt(digitStr, 10));
});
}
// Usage example
const initialState: ParseState = { input: "hello123world", position: 0 };
const parser = parseString("hello")
.flatMap(result1 =>
parseNumber()
.flatMap(result2 =>
parseString("world")
.map(result3 => ({
hello: result1,
number: result2,
world: result3
}))
)
);
const [results, finalState] = parser.run(initialState);
console.log("Parse results:", results);
console.log("Final state:", finalState);
// Output: Parse results: { hello: "hello", number: 123, world: "world" }
using System;
using System.Linq;
using System.Text.RegularExpressions;
// Unit type for void-like operations
public struct Unit
{
public static readonly Unit Default = new Unit();
}
public class State<S, A>
{
private readonly Func<S, (A Result, S NewState)> computation;
public State(Func<S, (A, S)> computation)
{
this.computation = computation;
}
public (A Result, S NewState) Run(S initialState)
{
return computation(initialState);
}
public State<S, B> Map<B>(Func<A, B> f)
{
return new State<S, B>(s =>
{
var (result, newState) = computation(s);
return (f(result), newState);
});
}
public State<S, B> FlatMap<B>(Func<A, State<S, B>> f)
{
return new State<S, B>(s =>
{
var (result, newState) = computation(s);
return f(result).Run(newState);
});
}
// LINQ Support
public State<S, B> SelectMany<B>(Func<A, State<S, B>> f)
{
return FlatMap(f);
}
public State<S, C> SelectMany<B, C>(Func<A, State<S, B>> f, Func<A, B, C> projection)
{
return FlatMap(a => f(a).Map(b => projection(a, b)));
}
public State<S, B> Select<B>(Func<A, B> f)
{
return Map(f);
}
public static State<S, A> Pure(A value)
{
return new State<S, A>(s => (value, s));
}
public static State<S, S> Get()
{
return new State<S, S>(s => (s, s));
}
public static State<S, Unit> Put(S newState)
{
return new State<S, Unit>(_ => (Unit.Default, newState));
}
}
// Parser state containing input and position
public class ParseState
{
public string Input { get; set; }
public int Position { get; set; }
public ParseState(string input, int position = 0)
{
Input = input;
Position = position;
}
public override string ToString() => $"ParseState(Input: \"{Input}\", Position: {Position})";
}
public static class Main
{
// Parse a specific character
public static State<ParseState, string?> ParseChar(char expected)
{
return from state in State<ParseState, ParseState>.Get()
let remaining = state.Input.Substring(Math.Min(state.Position, state.Input.Length))
let success = remaining.Length > 0 && remaining[0] == expected
from _ in success ? State<ParseState, Unit>.Put(new ParseState(state.Input, state.Position + 1))
: State<ParseState, Unit>.Put(state)
select success ? expected.ToString() : null;
}
// Parse a string by parsing each character
public static State<ParseState, string?> ParseString(string str)
{
if (string.IsNullOrEmpty(str))
{
return State<ParseState, string?>.Pure(str);
}
var head = str[0];
var tail = str.Substring(1);
return from charResult in ParseChar(head)
from tailResult in charResult != null ? ParseString(tail) : State<ParseState, string?>.Pure(null)
select charResult != null && tailResult != null ? str : null;
}
// Parse a number (simplified)
public static State<ParseState, int?> ParseNumber()
{
return from state in State<ParseState, ParseState>.Get()
let remaining = state.Input.Substring(Math.Min(state.Position, state.Input.Length))
let match = Regex.Match(remaining, @"^\d+")
let success = match.Success
from _ in success ? State<ParseState, Unit>.Put(new ParseState(state.Input, state.Position + match.Length))
: State<ParseState, Unit>.Put(state)
select success ? (int?)int.Parse(match.Value) : null;
}
// Usage example
public static void RunExample()
{
var initialState = new ParseState("hello123world");
var parser = from hello in ParseString("hello")
from number in ParseNumber()
from world in ParseString("world")
select new { Hello = hello, Number = number, World = world };
var (results, finalState) = parser.Run(initialState);
Console.WriteLine($"Parse results: Hello={results.Hello}, Number={results.Number}, World={results.World}");
Console.WriteLine($"Final state: {finalState}");
// Output: Parse results: Hello=hello, Number=123, World=world
}
}
Configuration Management Pattern #
The configuration pattern uses the State monad to manage application settings and feature toggles dynamically.
import Control.Monad.State
import qualified Data.Set as Set
-- Application configuration state
data AppConfig = AppConfig
{ apiUrl :: String
, timeout :: Int
, features :: Set.Set String
} deriving (Show)
-- Enable a feature if not already enabled
enableFeature :: String -> State AppConfig Bool
enableFeature feature = do
config <- get
if Set.member feature (features config)
then return False -- Already enabled
else do
put $ config { features = Set.insert feature (features config) }
return True
-- Configure multiple features
configureApp :: State AppConfig String
configureApp = do
darkMode <- enableFeature "darkMode"
notifications <- enableFeature "notifications"
analytics <- enableFeature "analytics"
return $ "Configured: darkMode=" ++ show darkMode ++
", notifications=" ++ show notifications ++
", analytics=" ++ show analytics
-- Usage example
main :: IO ()
main = do
let initialConfig = AppConfig "https://api.example.com" 5000 Set.empty
(result, finalConfig) = runState configureApp initialConfig
putStrLn result
putStrLn $ "Final config: " ++ show finalConfig
-- Output shows which features were newly enabled
class State<S, A> {
constructor(private runState: (state: S) => [A, S]) {}
run(initialState: S): [A, S] {
return this.runState(initialState);
}
map<B>(f: (a: A) => B): State<S, B> {
return new State(s => {
const [a, newS] = this.runState(s);
return [f(a), newS];
});
}
flatMap<B>(f: (a: A) => State<S, B>): State<S, B> {
return new State(s => {
const [a, newS] = this.runState(s);
return f(a).run(newS);
});
}
then<B>(next: State<S, B>): State<S, B> {
return this.flatMap(_ => next);
}
static pure<S, A>(value: A): State<S, A> {
return new State(s => [value, s]);
}
static get<S>(): State<S, S> {
return new State(s => [s, s]);
}
static put<S>(newState: S): State<S, void> {
return new State(_ => [undefined, newState]);
}
}
// Application configuration interface
interface AppConfig {
apiUrl: string;
timeout: number;
features: Set<string>;
}
// Enable a feature if not already enabled
function enableFeature(feature: string): State<AppConfig, boolean> {
return State.get<AppConfig>()
.flatMap(config => {
if (config.features.has(feature)) {
return State.pure(false); // Already enabled
} else {
const newFeatures = new Set(config.features);
newFeatures.add(feature);
return State.put<AppConfig>({
...config,
features: newFeatures
}).map(() => true);
}
});
}
// Configure multiple features
function configureApp(): State<AppConfig, string> {
return enableFeature("darkMode")
.flatMap(darkMode =>
enableFeature("notifications")
.flatMap(notifications =>
enableFeature("analytics")
.map(analytics =>
`Configured: darkMode=${darkMode}, notifications=${notifications}, analytics=${analytics}`
)
)
);
}
// Usage example
const initialConfig: AppConfig = {
apiUrl: "https://api.example.com",
timeout: 5000,
features: new Set<string>()
};
const [result, finalConfig] = configureApp().run(initialConfig);
console.log(result);
console.log("Final config:", {
...finalConfig,
features: Array.from(finalConfig.features)
});
// Output shows which features were newly enabled
using System;
using System.Collections.Generic;
using System.Linq;
// Unit type for void-like operations
public struct Unit
{
public static readonly Unit Default = new Unit();
}
public class State<S, A>
{
private readonly Func<S, (A Result, S NewState)> computation;
public State(Func<S, (A, S)> computation)
{
this.computation = computation;
}
public (A Result, S NewState) Run(S initialState)
{
return computation(initialState);
}
public State<S, B> Map<B>(Func<A, B> f)
{
return new State<S, B>(s =>
{
var (result, newState) = computation(s);
return (f(result), newState);
});
}
public State<S, B> FlatMap<B>(Func<A, State<S, B>> f)
{
return new State<S, B>(s =>
{
var (result, newState) = computation(s);
return f(result).Run(newState);
});
}
// LINQ Support
public State<S, B> SelectMany<B>(Func<A, State<S, B>> f)
{
return FlatMap(f);
}
public State<S, C> SelectMany<B, C>(Func<A, State<S, B>> f, Func<A, B, C> projection)
{
return FlatMap(a => f(a).Map(b => projection(a, b)));
}
public State<S, B> Select<B>(Func<A, B> f)
{
return Map(f);
}
public static State<S, A> Pure(A value)
{
return new State<S, A>(s => (value, s));
}
public static State<S, S> Get()
{
return new State<S, S>(s => (s, s));
}
public static State<S, Unit> Put(S newState)
{
return new State<S, Unit>(_ => (Unit.Default, newState));
}
}
// Application configuration
public class AppConfig
{
public string ApiUrl { get; set; }
public int Timeout { get; set; }
public HashSet<string> Features { get; set; }
public AppConfig(string apiUrl = "", int timeout = 0, HashSet<string>? features = null)
{
ApiUrl = apiUrl;
Timeout = timeout;
Features = features ?? new HashSet<string>();
}
public override string ToString()
{
return $"AppConfig(ApiUrl: \"{ApiUrl}\", Timeout: {Timeout}, Features: [{string.Join(", ", Features)}])";
}
}
public static class Program
{
// Enable a feature if not already enabled
public static State<AppConfig, bool> EnableFeature(string feature)
{
return from config in State<AppConfig, AppConfig>.Get()
let alreadyEnabled = config.Features.Contains(feature)
from _ in alreadyEnabled ? State<AppConfig, Unit>.Put(config)
: State<AppConfig, Unit>.Put(new AppConfig(config.ApiUrl, config.Timeout,
new HashSet<string>(config.Features) { feature }))
select !alreadyEnabled;
}
// Configure multiple features
public static State<AppConfig, string> ConfigureApp()
{
return from darkMode in EnableFeature("darkMode")
from notifications in EnableFeature("notifications")
from analytics in EnableFeature("analytics")
select $"Configured: darkMode={darkMode}, notifications={notifications}, analytics={analytics}";
}
// Usage example
public static void Main()
{
var initialConfig = new AppConfig("https://api.example.com", 5000);
var (result, finalConfig) = ConfigureApp().Run(initialConfig);
Console.WriteLine(result);
Console.WriteLine($"Final config: {finalConfig}");
// Output shows which features were newly enabled
}
}
Stateful Validation Pattern #
The validation pattern uses the State monad to accumulate validation errors while building up a context of validated data.
import Control.Monad.State
import qualified Data.Map as Map
-- Validation state containing errors and validated context
data ValidationState = ValidationState
{ errors :: [String]
, context :: Map.Map String String
} deriving (Show)
-- Validate that a field is required (not empty)
validateRequired :: String -> String -> State ValidationState Bool
validateRequired field value = do
state' <- get
if null value
then do
put $ state' { errors = errors state' ++ [field ++ " is required"] }
return False
else do
put $ state' { context = Map.insert field value (context state') }
return True
-- Validate email format
validateEmail :: String -> State ValidationState Bool
validateEmail email = do
state' <- get
let isValid = '@' `elem` email && '.' `elem` email
if not isValid
then do
put $ state' { errors = errors state' ++ ["Invalid email format"] }
return False
else return True
-- Validate a complete user
validateUser :: String -> String -> State ValidationState Bool
validateUser name email = do
nameValid <- validateRequired "name" name
emailValid <- validateRequired "email" email
emailFormatValid <- if emailValid then validateEmail email else return False
return $ nameValid && emailValid && emailFormatValid
-- Usage example
main :: IO ()
main = do
let initialState = ValidationState [] Map.empty
(isValid, finalState) = runState (validateUser "John" "john@example.com") initialState
putStrLn $ "Is valid: " ++ show isValid
putStrLn $ "Errors: " ++ show (errors finalState)
putStrLn $ "Context: " ++ show (context finalState)
class State<S, A> {
constructor(private runState: (state: S) => [A, S]) {}
run(initialState: S): [A, S] {
return this.runState(initialState);
}
map<B>(f: (a: A) => B): State<S, B> {
return new State(s => {
const [a, newS] = this.runState(s);
return [f(a), newS];
});
}
flatMap<B>(f: (a: A) => State<S, B>): State<S, B> {
return new State(s => {
const [a, newS] = this.runState(s);
return f(a).run(newS);
});
}
then<B>(next: State<S, B>): State<S, B> {
return this.flatMap(_ => next);
}
static pure<S, A>(value: A): State<S, A> {
return new State(s => [value, s]);
}
static get<S>(): State<S, S> {
return new State(s => [s, s]);
}
static put<S>(newState: S): State<S, void> {
return new State(_ => [undefined, newState]);
}
}
// Validation state containing errors and context
interface ValidationState {
errors: string[];
context: Map<string, string>;
}
// Validate that a field is required (not empty)
function validateRequired(field: string, value: string): State<ValidationState, boolean> {
return State.get<ValidationState>()
.flatMap(state => {
if (!value || value.trim() === '') {
const newState: ValidationState = {
errors: [...state.errors, `${field} is required`],
context: new Map(state.context)
};
return State.put<ValidationState>(newState).map(() => false);
} else {
const newState: ValidationState = {
errors: [...state.errors],
context: new Map(state.context).set(field, value)
};
return State.put<ValidationState>(newState).map(() => true);
}
});
}
// Validate email format
function validateEmail(email: string): State<ValidationState, boolean> {
return State.get<ValidationState>()
.flatMap(state => {
const isValid = email.includes('@') && email.includes('.');
if (!isValid) {
const newState: ValidationState = {
errors: [...state.errors, 'Invalid email format'],
context: new Map(state.context)
};
return State.put<ValidationState>(newState).map(() => false);
}
return State.pure(true);
});
}
// Validate a complete user
function validateUser(name: string, email: string): State<ValidationState, boolean> {
return validateRequired("name", name)
.flatMap(nameValid =>
validateRequired("email", email)
.flatMap(emailValid => {
if (emailValid) {
return validateEmail(email)
.map(emailFormatValid => nameValid && emailValid && emailFormatValid);
} else {
return State.pure(nameValid && emailValid);
}
})
);
}
// Usage example
const initialState: ValidationState = {
errors: [],
context: new Map()
};
const [isValid, finalState] = validateUser("John", "john@example.com").run(initialState);
console.log(`Is valid: ${isValid}`);
console.log(`Errors: ${JSON.stringify(finalState.errors)}`);
console.log(`Context:`, Array.from(finalState.context.entries()));
using System;
using System.Collections.Generic;
using System.Linq;
// Unit type for void-like operations
public struct Unit
{
public static readonly Unit Default = new Unit();
}
public class State<S, A>
{
private readonly Func<S, (A Result, S NewState)> computation;
public State(Func<S, (A, S)> computation)
{
this.computation = computation;
}
public (A Result, S NewState) Run(S initialState)
{
return computation(initialState);
}
public State<S, B> Map<B>(Func<A, B> f)
{
return new State<S, B>(s =>
{
var (result, newState) = computation(s);
return (f(result), newState);
});
}
public State<S, B> FlatMap<B>(Func<A, State<S, B>> f)
{
return new State<S, B>(s =>
{
var (result, newState) = computation(s);
return f(result).Run(newState);
});
}
// LINQ Support
public State<S, B> SelectMany<B>(Func<A, State<S, B>> f)
{
return FlatMap(f);
}
public State<S, C> SelectMany<B, C>(Func<A, State<S, B>> f, Func<A, B, C> projection)
{
return FlatMap(a => f(a).Map(b => projection(a, b)));
}
public State<S, B> Select<B>(Func<A, B> f)
{
return Map(f);
}
public static State<S, A> Pure(A value)
{
return new State<S, A>(s => (value, s));
}
public static State<S, S> Get()
{
return new State<S, S>(s => (s, s));
}
public static State<S, Unit> Put(S newState)
{
return new State<S, Unit>(_ => (Unit.Default, newState));
}
}
// Validation state containing errors and context
public class ValidationState
{
public List<string> Errors { get; set; }
public Dictionary<string, string> Context { get; set; }
public ValidationState(List<string>? errors = null, Dictionary<string, string>? context = null)
{
Errors = errors ?? new List<string>();
Context = context ?? new Dictionary<string, string>();
}
public override string ToString()
{
return $"ValidationState(Errors: [{string.Join(", ", Errors.Select(e => $"\"{e}\""))}], " +
$"Context: {{{string.Join(", ", Context.Select(kv => $"{kv.Key}: \"{kv.Value}\""))}}})";
}
}
public static class Program
{
// Validate that a field is required (not empty)
public static State<ValidationState, bool> ValidateRequired(string field, string value)
{
return from state in State<ValidationState, ValidationState>.Get()
let isEmpty = string.IsNullOrWhiteSpace(value)
let newState = isEmpty
? new ValidationState(
state.Errors.Concat(new[] { $"{field} is required" }).ToList(),
new Dictionary<string, string>(state.Context))
: new ValidationState(
new List<string>(state.Errors),
new Dictionary<string, string>(state.Context) { [field] = value })
from _ in State<ValidationState, Unit>.Put(newState)
select !isEmpty;
}
// Validate email format
public static State<ValidationState, bool> ValidateEmail(string email)
{
return from state in State<ValidationState, ValidationState>.Get()
let isValid = !string.IsNullOrEmpty(email) && email.Contains("@") && email.Contains(".")
let newState = isValid
? state
: new ValidationState(
state.Errors.Concat(new[] { "Invalid email format" }).ToList(),
new Dictionary<string, string>(state.Context))
from _ in State<ValidationState, Unit>.Put(newState)
select isValid;
}
// Validate a complete user
public static State<ValidationState, bool> ValidateUser(string name, string email)
{
return from nameValid in ValidateRequired("name", name)
from emailValid in ValidateRequired("email", email)
from emailFormatValid in emailValid ? ValidateEmail(email) : State<ValidationState, bool>.Pure(false)
select nameValid && emailValid && emailFormatValid;
}
// Usage example
public static void Main()
{
var initialState = new ValidationState();
var (isValid, finalState) = ValidateUser("John", "john@example.com").Run(initialState);
Console.WriteLine($"Is valid: {isValid}");
Console.WriteLine($"Final state: {finalState}");
}
}
Behavior Trees #
Behavior trees are a popular AI pattern used in game development[4] to create complex, hierarchical decision-making systems for NPCs (Non-Player Characters). The State monad is perfect for implementing behavior trees because each behavior needs to:
- Read the current AI state (position, health, targets, etc.)
- Modify the state based on decisions (move, attack, change targets)
- Return success/failure status to parent behaviors
- Chain behaviors together in a decision hierarchy
Let's build a simple AI behavior tree using the State monad:
import Control.Monad.State
-- AI state for game NPCs
data AIState = AIState
{ aiHealth :: Int
, aiPosition :: (Int, Int)
, aiTarget :: Maybe (Int, Int)
, aiCooldown :: Int
} deriving (Show)
-- Individual AI behaviors using State monad
-- Each behavior returns Bool: True = success, False = failure
-- 1. Target acquisition behavior
findTarget :: State AIState Bool
findTarget = do
state <- get
-- Simplified target finding logic - in a real game, this would
-- search for nearby enemies, check line of sight, etc.
put $ state { aiTarget = Just (10, 10) }
return True -- Always succeeds in this simple example
-- 2. Movement behavior
moveToTarget :: State AIState Bool
moveToTarget = do
state <- get
case aiTarget state of
Nothing -> return False -- No target to move to
Just (tx, ty) -> do
let (x, y) = aiPosition state
-- Simple pathfinding: move one step closer to target
newX = if x < tx then x + 1 else if x > tx then x - 1 else x
newY = if y < ty then y + 1 else if y > ty then y - 1 else y
put $ state { aiPosition = (newX, newY) }
return True -- Movement succeeded
-- 3. Combat behavior
attackIfInRange :: State AIState Bool
attackIfInRange = do
state <- get
case aiTarget state of
Nothing -> return False -- No target to attack
Just target -> do
let distance = calculateDistance (aiPosition state) target
-- Check if target is in range AND attack is off cooldown
if distance <= 2 && aiCooldown state == 0
then do
put $ state { aiCooldown = 3 } -- Set attack cooldown
return True -- Attack succeeded
else return False -- Either out of range or on cooldown
-- 4. Composite behavior: Complete AI decision tree
aiTurn :: State AIState String
aiTurn = do
-- Step 1: Try to find a target
hasTarget <- findTarget
if hasTarget
then do
-- Step 2: Try to attack if possible
attacked <- attackIfInRange
if attacked
then return "Attacked!"
else do
-- Step 3: If can't attack, try to move closer
moved <- moveToTarget
return $ if moved then "Moved toward target" else "Standing still"
else return "Looking for target"
-- Helper function
calculateDistance :: (Int, Int) -> (Int, Int) -> Int
calculateDistance (x1, y1) (x2, y2) = abs (x1 - x2) + abs (y1 - y2)
-- Usage example
runAI :: IO ()
runAI = do
let initialState = AIState 100 (0, 0) Nothing 0
(action, finalState) = runState aiTurn initialState
putStrLn $ "AI Action: " ++ action
putStrLn $ "Final AI State: " ++ show finalState
How Behavior Tree Works #
The behavior tree follows this decision hierarchy:
AI Turn
├── Find Target (Always succeeds)
└── Has Target?
├── YES → Try Attack
│ ├── Attack Success? → "Attacked!"
│ └── Attack Failed → Try Move
│ ├── Move Success? → "Moved toward target"
│ └── Move Failed → "Standing still"
└── NO → "Looking for target"
Behavior Trees - Why State Monad #
-
Automatic State Threading: Each behavior automatically receives the current AI state and passes the updated state to the next behavior.
-
Composability: Complex behaviors can be built by combining simpler behaviors using monadic operations.
-
Decision Chaining: The
donotation makes it easy to express "try this, if it fails try that" logic. -
Pure Functions: All behaviors remain pure functions, making them easy to test and reason about.
This example shows how the State monad enables complex AI behaviors while keeping the code clean and composable. Each behavior can focus on its specific logic while the State monad handles all the state management automatically.
State Monad vs Other Approaches #
Comparison Table #
| Approach | State Threading | Error Handling | Composability | Readability |
|---|---|---|---|---|
| Manual | Manual/Error-prone | Manual | Poor | Poor |
| State Monad | Automatic | Needs another layer | Excellent | Excellent |
| Object-Oriented | Implicit/Mutable | Exceptions | Moderate | Moderate |
| Functional (Pure) | Explicit passing | Result types | Good | Good |
State Monad Usage #
Use State Monad when:
- You have sequential operations that modify shared state
- State transformations are the primary concern
- You want pure functional state management
- You need composable stateful computations
Consider alternatives when:
- State is simple and rarely changes
- You need shared mutable state across threads
- Performance is critical and State monad overhead matters
- Your team is not familiar with monadic patterns
Conclusion #
The State monad solves the problem of threading state through sequential computations. By abstracting away the manual state passing, it allows us to focus on the core logic while maintaining the benefits of pure functions.
Key benefits of the State monad:
- Automatic State Threading: No more manual state passing and potential errors
- Composability: State operations compose naturally using monadic combinators
- Purity: Stateful computations remain pure functions
- Abstraction: Clean separation between state management and business logic
- Flexibility: Easy to combine with other monads (IO, Error handling, etc.)
Source code #
Reference implementation (opens in a new tab)
Notes
- These examples are simple cursor parsers: a failed parse returns `Nothing`, but State alone does not provide automatic backtracking. If you need rollback-on-failure, use a parser abstraction that restores state on failure, or choose the transformer ordering deliberately. For example, `MaybeT (State s)` preserves state changes before failure, while `StateT s Maybe` discards the final state when the computation fails. · Back