Transit: Build fast and type-safe state machines

I’m excited to share Transit, a PureScript library for building fast and type-safe state machines.

What is Transit?

Transit lets you specify state machines using a type-level DSL. The compiler then ensures your implementation is complete and correct—no missing cases, no invalid transitions, no documentation drift.

Quick Example

Here’s a simple door state machine:

-- Define your state machine specification at the type level
type DoorTransit =
  Transit
    :* ("DoorOpen" :@ "Close" >| "DoorClosed")
    :* ("DoorClosed" :@ "Open" >| "DoorOpen")

-- Write the update function - the compiler checks it matches the spec!
update :: State -> Msg -> State
update = mkUpdate @DoorTransit
  ( match @"DoorOpen" @"Close" \_ _ ->
      return @"DoorClosed"
  )
  ( match @"DoorClosed" @"Open" \_ _ ->
      return @"DoorOpen"
  )

If you forget to handle a transition or add one that’s not in your specification, you’ll get a compile error.

Key Features

  • Type-Level DSL - Specify state transitions as types
  • Compile-Time Verification - The compiler ensures your update function is complete and valid
  • Automatic Diagrams - Generate state diagrams from your specification
  • Graph Analysis - Analyze state machine properties programmatically
  • Performance - Optimized runtime with zero overhead for type-level machinery
  • Advanced Patterns - Support for monadic updates, error handling, and more

Example: Multi-Way Transitions

Transit supports transitions with multiple possible outcomes:

type CountDownTransit =
  Transit
    :* ("Idle" :@ "Start" >| "Counting")
    :* ("Done" :@ "Reset" >| "Idle")
    :* ( "Counting" :@ "Tick"
          >| "Counting"  -- continue counting
          >| "Done"      -- or finish
       )

update :: State -> Msg -> State
update = mkUpdate @CountDownTransit
  -- ... handlers ...
  ( match @"Counting" @"Tick" \state _ ->
      if (state.count - 1) == 0
        then return @"Done"
        else return @"Counting" { count: state.count - 1 }
  )

The compiler verifies that you only return states listed in your specification.

Resources

Installation

spago install transit

Design Philosophy

If you’re familiar with Servant from Haskell, Transit follows a similar philosophy: just as Servant uses a type-level REST API specification to ensure type-safe routing and generate OpenAPI docs, Transit uses a type-level state machine specification to ensure type-safe update functions and generate state diagrams.


I’d love to hear your thoughts, questions, or feedback!

11 Likes

This is awesome! I had been looking into CREM in the Haskell ecosystem for a project. I’m very happy to see something new here. Great work!

1 Like

Very nice. I love that kind of thing.

1 Like

Regarding CREM and this with a Haskell back end, I will look into building a cross-language state machine schema DSL for guaranteeing valid state transitions in matching Haskell back end/ Purescript front end simultaneously through codegen.

Kind of like protobuffers (or Lambdabuffers) but instead it generates valid corresponding Haskell and Purescript types, instances, state transitions, and other boilerplate (until GHC gets dependent types…then it’s right back to the drawing board with more modern tooling).

I’m sure I’ll hit a wall where all of the corner cases could cost years. But right now, it sounds like a fun way to convert the app I’m building to a sort of polymorphic module where it could generate most of itself (both front end back end) with a bespoke schema format.

1 Like