Overview
Want to master Finite State Machines through the lens of Functional Programming? This by-example guide teaches FSM in four functional languages — F# (canonical), Clojure, TypeScript, and Haskell — through annotated code and diagram examples organized by complexity level, using a shared Procure-to-Pay (P2P) domain so every example builds on the same problem. Each example presents all four languages as parallel tabs; F# carries the deepest annotations and the framing prose, while Clojure, TypeScript, and Haskell are first-class variants.
Why FP for FSMs?
FSMs fit Functional Programming naturally. A state machine is a function State -> Event -> State — a pure transition function. All four languages in this tutorial express this naturally: F# uses discriminated unions and pattern matching; Clojure uses keyword enums and multimethods; TypeScript uses union types and switch exhaustiveness checks; Haskell uses sum-type ADTs and case expressions. The OOP approach uses a class per state with virtual dispatch; the FP approach collapses that into a single transition function with a match / multimethod / switch / case expression in whichever language you choose. The FP version is shorter, testable in isolation, and impossible to put in an invalid state when the state type is sealed.
Rust as an FP-Adjacent Member — With Concept Adjustments
Rust shares deep DNA with the FP languages on this track: enum is a sum type (Blandy & Orendorff, Programming Rust, Ch. 10), match is exhaustive pattern matching, and Result<T, E> is the Railway-Oriented Programming primitive natively. FSM idioms that translate one-to-one — states as enum variants, the pure State -> Event -> Result<NextState, Error> transition function, guard conditions as match arms returning Err(...) — appear without adjustment. But Rust adds one capability the FP languages on this track do not have: typestate-encoded state machines via ownership.
Six concept adjustments when you read this FSM-in-FP track through a Rust lens:
- Ownership turns illegal state transitions into compile errors. In F#, the pure transition function
submit : Draft -> Result<AwaitingApproval, Error>returns a new value; the caller can still hold the oldDraft(it stays in scope). In Rust,fn submit(self) -> Result<AwaitingApproval, Error>moves theDraft— the caller cannot use the old state because it has been consumed. The compile-time guarantee is strictly stronger. (Source: Pretty State Machine Patterns in Rust — Hoverbear | Type-Driven API Design in Rust — Will Crichton) - Typestate as a fundamentally Rust-only pattern. Each state is a distinct struct type, not a variant of one
enum.Draft,Submitted,Approved,Issued,Cancelledare five separate types; transitions are functions between them; the type system enforces the legal transition graph without runtime checks. F# / Haskell GADT-style equivalents exist but are more verbose and less ergonomic. This is the canonical Rust FSM idiom and lives in the in-procedural-by-example track. - No higher-kinded types (HKT). Generic transition functions that abstract over the effect (
async,Result, both) need HKT in F#/Haskell, which Rust lacks. Effect-polymorphic FSM runners require ad-hoc combinators, not generic monadic abstraction. (Source: GAT stabilisation — Matsakis & Huey) ?operator for guarded transitions. F#Result.bindchains the guard pipeline; Rust uses?to short-circuit the transition function on guard failure. (Source: Rust By Example —?operator)async/Futureis not a monad. Async state machine runners in F# useasync { }; in Rust they useasync fnwith explicitFuturefutures. There is no monadic abstraction over bothAsyncandResult—tokioprovides ad-hoc combinators.- No persistent FSM history by default. F# lists/maps share via GC, so keeping an event log (
State list) of every prior transition is cheap. Rust requiresVec<Event>(owned),Arc<[Event]>(shared immutable), or theimcrate for persistent vectors.
Where the enum-based FSM idiom translates cleanly, Rust appears as an additional language tab alongside F# / Clojure / TypeScript / Haskell. Where typestate is the design force, the example links to the procedural track for the full Rust-canonical treatment.
Domain Context
All examples model the procurement-platform-be backend. Employees request goods, managers approve, suppliers fulfill, and finance pays. This single domain thread — rather than a new toy problem per example — lets you compare FSM patterns across levels without re-learning the context.
Learning Path
Three progressive levels, each adding a new aggregate:
- Beginner —
PurchaseOrderstate machine: F# discriminated unions as state sets,State -> Event -> Statepure transition functions, guard conditions withResult, exhaustivematchexpressions, invalid-transition rejection. - Intermediate — adds
Invoicestate machine: three-way match guards, state-entry/exit side-effects modelled as returned command lists, FSM composition, parallel machine coordination. - Advanced — adds
Supplierlifecycle andPaymentstate machine: hierarchical states via nested DUs, parallel regions, history states, FSM persistence and event-sourcing intersection, saga coordination in F#.
Examples by Level
Beginner (Examples 1–25)
- Example 1: States as a Discriminated Union
- Example 2: The Minimal FSM Record
- Example 3: The Transition Table as a Map
- Example 4: The Pure Transition Function
- Example 5: Exhaustiveness Checking with Match
- Example 6: Approval-Level Guard
- Example 7: Guarded Transition with Result
- Example 8: Line-Item Guard
- Example 9: Immutable Lines After Issue
- Example 10: Cancel From Any Pre-Paid State
- Example 11: Dispute Transition and Resolution
- Example 12: The Full Transition Table in F#
- Example 13: Event Log and Audit Trail
- Example 14: FSM Record with Validation
- Example 15: Event DU and Typed Transition
- Example 16: Entry Action on AwaitingApproval
- Example 17: Exit Action on Issued
- Example 18: Testing FSM Transitions in F#
- Example 19: Deriving Total from Line Items
- Example 20: Constructing the Initial PO with Validation
- Example 21: State as a Nested DU
- Example 22: Logging State Transitions for Observability
- Example 23: Replaying Events to Reconstruct State
- Example 24: State Machine Visualisation (Generating a DOT Graph)
- Example 25: The PO FSM as a Protocol
Intermediate (Examples 26–50)
- Example 26: Invoice States and the Three-Way Match
- Example 27: The Three-Way Match Guard
- Example 28: Guarded Invoice Transition
- Example 29: Linking Invoice and PurchaseOrder State Machines
- Example 30: Invoice FSM with Tolerance Check
- Example 31: Invoice FSM with Result Chaining
- Example 32: Declarative Machine Definition as a Map
- Example 33: Guards in Declarative Machine Config
- Example 34: State Entry Actions as Command Lists
- Example 35: Modelling Invoice Resubmission History
- Example 36: FSM as Protocol Enforcement
- Example 37: PO Lifecycle — PartiallyReceived State
- Example 38: Combining PO and Invoice with an Event Bus
- Example 39: Testing Invoice-PO Coordination
- Example 40: Validation Error Accumulation
- Example 41: State Machine Composition — Invoice Inside PO Lifecycle
- Example 42: Timeout Guards
- Example 43: Building an Invoice FSM Runner
- Example 44: Coverage Snapshot — PO + Invoice Machine States
- Example 45: Idempotent Transitions
- Example 46: Event Versioning — Migrating FSM State
- Example 47: Read-Only State Queries
- Example 48: Two-Machine Sequence Diagram
- Example 49: Encoding SLA in FSM Metadata
- Example 50: Summary — FSM as System Architecture
Advanced (Examples 51–75)
- Example 51: Supplier States and Risk-Tier Semantics
- Example 52: Supplier State Consequences on PO Selection
- Example 53: Supplier Risk Score Guard
- Example 54: Hierarchical States — Supplier with Sub-States
- Example 55: History States — Restoring Previous Sub-State After Suspension
- Example 56: Language-Agnostic Data-Driven FSM Pattern
- Example 57: Supplier Blacklisting Cascade — Forcing POs to Disputed
- Example 58: Payment States and the Disbursement Lifecycle
- Example 59: Payment Retry Limit Guard
- Example 60: Parallel Regions — Payment + Notification
- Example 61: FSM Persistence — Serialising State to JSON
- Example 62: Event Sourcing Intersection — Rebuilding Payment from Events
- Example 63: Statechart — Combining Hierarchical + Parallel + History
- Example 64: Full P2P Machine Coverage Check
- Example 65: MurabahaContract State Machine
- Example 66: Installment Counter and Self-Loop
- Example 67: Linking MurabahaContract to PurchaseOrder
- Example 68: Actor Model — FSM as a Mailbox Processor
- Example 69: Optimistic Concurrency — Version Numbers
- Example 70: Saga Pattern — Coordinating PO + Invoice + Payment
- Example 71: State Machine Snapshot and Resume
- Example 72: FSM Visualisation — Mermaid from Code
- Example 73: FSM-Driven API Response Codes
- Example 74: Testing All Four Machines Together
- Example 75: Statechart Summary — All Four Machines
Structure of Each Example
Every example follows a five-part format:
- Brief Explanation — what FSM concept the example demonstrates (2-3 sentences)
- State Diagram — Mermaid
stateDiagram-v2with accessible color palette (where appropriate) - Annotated Code — parallel tabs showing F# (canonical), Clojure, and TypeScript, each with 1.0-2.25 comment lines per code line
- Key Takeaway — the core principle to retain (1-2 sentences)
- Why It Matters — design rationale and consequences (50-100 words)
Last updated May 16, 2026