Skip to content
AyoKoding

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:

  1. 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 old Draft (it stays in scope). In Rust, fn submit(self) -> Result<AwaitingApproval, Error> moves the Draft — 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)
  2. Typestate as a fundamentally Rust-only pattern. Each state is a distinct struct type, not a variant of one enum. Draft, Submitted, Approved, Issued, Cancelled are 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.
  3. 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)
  4. ? operator for guarded transitions. F# Result.bind chains the guard pipeline; Rust uses ? to short-circuit the transition function on guard failure. (Source: Rust By Example — ? operator)
  5. async / Future is not a monad. Async state machine runners in F# use async { }; in Rust they use async fn with explicit Future futures. There is no monadic abstraction over both Async and Resulttokio provides ad-hoc combinators.
  6. 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 requires Vec<Event> (owned), Arc<[Event]> (shared immutable), or the im crate 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:

  • BeginnerPurchaseOrder state machine: F# discriminated unions as state sets, State -> Event -> State pure transition functions, guard conditions with Result, exhaustive match expressions, invalid-transition rejection.
  • Intermediate — adds Invoice state machine: three-way match guards, state-entry/exit side-effects modelled as returned command lists, FSM composition, parallel machine coordination.
  • Advanced — adds Supplier lifecycle and Payment state 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)

Intermediate (Examples 26–50)

Advanced (Examples 51–75)

Structure of Each Example

Every example follows a five-part format:

  1. Brief Explanation — what FSM concept the example demonstrates (2-3 sentences)
  2. State Diagram — Mermaid stateDiagram-v2 with accessible color palette (where appropriate)
  3. Annotated Code — parallel tabs showing F# (canonical), Clojure, and TypeScript, each with 1.0-2.25 comment lines per code line
  4. Key Takeaway — the core principle to retain (1-2 sentences)
  5. Why It Matters — design rationale and consequences (50-100 words)

Last updated May 16, 2026

Command Palette

Search for a command to run...