Implementation using Transit
Now let’s see how Transit can help us to improve this.
State and Message Types
Transit uses Variant1
types for both State and Msg instead of
traditional ADTs. This design choice is crucial for
Transit, but for now let’s just focus on the fact that
it’s another way to represent sum types.2
type State = Variant
( "DoorOpen" :: {}
, "DoorClosed" :: {}
)
type Msg = Variant
( "Close" :: {}
, "Open" :: {}
)🗎 test/Examples/Door.purs L27-L35
The empty record {} is used to represent the absence of
any data (payload) associated with the state or message.
Transit Specification
Once the types are defined, we can define the state machine structure using Transit’s type-level DSL. Let’s see what it looks like:
type DoorTransit =
Transit
:* ("DoorOpen" :@ "Close" >| "DoorClosed")
:* ("DoorClosed" :@ "Open" >| "DoorOpen")🗎 test/Examples/Door.purs L37-L40
Breaking down the syntax:
Transitinitializes an empty transition list:*is an infix operator that appends each transition to the list- The
:@operator connects a state to a message, and>|indicates the target state
For instance, we read the first transition as: in state
DoorOpen, when receiving message Close,
transition to state DoorClosed.
This type-level specification fully defines the state machine’s structure. The compiler can now use it to ensure our implementation of the update function matches the specification.
The Update Function
Based on the above specification, we create an update function using
mkUpdate:
update :: State -> Msg -> State
update = mkUpdate @DoorTransit
(match @"DoorOpen" @"Close" \_ _ -> return @"DoorClosed")
(match @"DoorClosed" @"Open" \_ _ -> return @"DoorOpen")🗎 test/Examples/Door.purs L42-L45
Here’s how this works:
mkUpdate @DoorTransitcreates an update function based on theDoorTransitspecification. The@symbol is type application3, passing the specification to the function.Each
matchline handles one transition from the specification. The first two arguments (@"DoorOpen"and@"Close") are type-level symbols (type applications) that specify which state and message to match on. The lambda function defines what happens when that transition occurs.return @"DoorClosed"specifies which state to transition to. Thereturnfunction is part of Transit’s DSL for specifying the target state, and the@symbol again indicates a type-level symbol.Important: The order of match handlers must match the order of transitions in the DSL specification.
The
purescript-variantlibrary provides row-polymorphic sum types. See the documentation for more details.↩︎This design choice is crucial for Transit’s type-level machinery. The key advantage is that Transit can filter the possible cases (both input states/messages and output states) for each handler function. Variants are perfect for this. There is no way to express a subset of cases from a traditional ADT.↩︎
In PureScript, the
@symbol is used for explicit type application, allowing you to pass type-level arguments to functions.↩︎