gala-state
March 1, 2026 · View on GitHub
State machines that the compiler understands.
GALA's sealed types and pattern matching turn state machines from a design pattern into a language feature. Define your states, define your events, and the compiler guarantees every transition is handled.
sealed type OrderState {
case Pending()
case Confirmed(ConfirmedBy string)
case Shipped(TrackingId string)
case Delivered()
case Cancelled(Reason string)
}
sealed type OrderEvent {
case Confirm(Approver string)
case Ship(Tracking string)
case Deliver()
case Cancel(CancelReason string)
}
func transition(state OrderState, event OrderEvent) Either[string, OrderState] {
return state match {
case Pending() => event match {
case Confirm(by) => Right[string, OrderState](Confirmed(by))
case Cancel(reason) => Right[string, OrderState](Cancelled(reason))
case _ => Left[string, OrderState]("pending orders can only be confirmed or cancelled")
}
case Confirmed(_) => event match {
case Ship(tracking) => Right[string, OrderState](Shipped(tracking))
case Cancel(reason) => Right[string, OrderState](Cancelled(reason))
case _ => Left[string, OrderState]("confirmed orders can only be shipped or cancelled")
}
// ...
}
}
No enums. No flags. No default: that silently swallows new states. Just types.
GALA vs Go
| Feature | GALA | Go |
|---|---|---|
| States | sealed type — each variant carries its own data | iota constants — data lives in a separate struct |
| Transitions | match expression — compiler checks every case | switch — default: silently swallows new variants |
| Exhaustiveness | Compile-time error if you miss a case | Nothing — bugs hide until runtime |
| Error handling | Either[Error, State] — typed, pattern-matchable | (State, error) — caller can ignore the error |
| State data | Each variant carries exactly its fields | Flat struct with all fields present in every state |
| Lines of code | ~60 | ~120 |
Side-by-Side: Transition Function
| GALA | Go |
|---|---|
|
|
Notice what's missing from the Go version: Confirmed can't tell you who confirmed. Shipped can't carry a tracking ID. Cancelled can't carry a reason. In GALA, each state variant carries exactly the data it needs.
Running the Examples
Install GALA (instructions), then:
# E-commerce order FSM
cd examples/order && gala run
# Traffic light controller
cd examples/traffic_light && gala run
# Vending machine with coins
cd examples/vending_machine && gala run
# Go equivalent (for comparison)
cd go_equivalent/order && go run main.go
Examples
Order State Machine
The hero example: an e-commerce order that flows through Pending → Confirmed → Shipped → Delivered, with cancellation possible from Pending or Confirmed states.
=== Order State Machine ===
--- Happy Path ---
Pending + Confirm(Alice) => Confirmed by Alice
Confirmed by Alice + Ship(TRACK-42) => Shipped (tracking: TRACK-42)
Shipped (tracking: TRACK-42) + Deliver => Delivered
--- Invalid Transition ---
Pending + Ship(TRACK-99) => ERROR: pending orders can only be confirmed or cancelled
Pending + Deliver => ERROR: pending orders can only be confirmed or cancelled
--- Cancellation Flow ---
Pending + Confirm(Bob) => Confirmed by Bob
Confirmed by Bob + Cancel(customer request) => Cancelled: customer request
Cancelled: customer request + Ship(TRACK-00) => ERROR: cancelled orders cannot transition
Traffic Light
The simplest state machine: three states, two signals. Shows how emergency overrides work.
=== Traffic Light State Machine ===
--- Normal Cycle ---
Start: RED
Timer: GREEN
Timer: YELLOW
Timer: RED
--- Emergency Override ---
Start: GREEN
Emergency: RED
Timer: GREEN
Vending Machine
The most complex example: accumulating state (coin insertion), conditional transitions (sufficient funds), and side effects.
=== Vending Machine State Machine ===
--- Buy a Soda (75c) ---
Idle => Accepting (25c)
Accepting (25c) => Accepting (50c)
Accepting (50c) => Accepting (75c)
Accepting (75c) => Dispensing Soda (change: 0c)
Dispensing Soda (change: 0c) => Idle
--- Insufficient Funds ---
Idle => Accepting (10c)
Accepting (10c) => ERROR: need 40c more for Candy
Accepting (10c) => Accepting (35c)
Accepting (35c) => Accepting (60c)
Accepting (60c) => Dispensing Candy (change: 10c)
Dispensing Candy (change: 10c) => Idle
--- Return Coins ---
Idle => Accepting (25c)
Accepting (25c) => Accepting (30c)
Returning 30c
Accepting (30c) => Idle
--- Out of Stock ---
Out of Stock => ERROR: machine is out of stock
Why GALA for State Machines?
-
Sealed types ARE states. Each variant is a state, and each variant carries exactly the data that state needs — no flat structs with impossible field combinations.
-
Match IS the transition function. Pattern matching destructures state and event simultaneously, extracting data in the same expression that routes the transition.
-
Exhaustiveness IS safety. The compiler rejects code that doesn't handle every state/event combination. Add a new variant and every
matchin your codebase lights up with errors until you handle it. -
Either IS error handling.
Either[string, OrderState]makes invalid transitions a value, not an exception. The caller must pattern-match the result — they can't accidentally ignore an error. -
Immutability IS the audit trail.
val state = Pending()can't be silently mutated. State transitions produce new values, making the flow explicit and traceable.
Links
- GALA Language — compiler and standard library
- Language Specification — full reference