Polymorphic Application State in PureScript
When I was building Quarto, I needed to store state information differently for single player and two player games. Originally, I had two different type aliases for the state...
type GameState = {
ondeck :: Maybe PieceID,
board :: Board,
gameinprogress :: Boolean,
gametype :: GameType | a
}
type TwoPlayerGameState = {
ondeck :: Maybe PieceID,
board :: Board,
gameinprogress :: Boolean,
gametype :: GameType,
userConnection :: Maybe ConnectedPeer,
opponentConnection :: Maybe Connection
}
Notice the repetition of some of the properties. In the game, I also had several functions that needed to be used for both the single player and two player game states. For example, the function that listened for Piece
and Board
events needed to work the same way.
onEvent :: forall a. EventType -> Ref GameState -> (Ref GameState -> { event :: Event | a } -> GameEffects Unit) -> Consumer { event :: Event | a } GameEffects Unit
onEvent :: forall a. EventType -> Ref TwoPlayerGameState -> (Ref TwoPlayerGameState -> { event :: Event | a } -> GameEffects Unit) -> Consumer { event :: Event | a } GameEffects Unit
Obviously, this is problematic since I didn't want to repeat the same code in both functions. Ideally, I could use the same function for both types.
The way I solved this was to use "record polymorphism." Basically, that means instead of using GameState
and TwoPlayerGameState
I created a new type that would match both of those records for the properties that they share. I decied to call that type BGameState
for "Base Game State"...
type BGameState a = {
ondeck :: Maybe PieceID,
board :: Board,
gameinprogress :: Boolean,
gametype :: GameType | a
}
Notice how BGameState
takes an additional parameter. You can think of this parameter as "any other record properties that can be added to the record". So then I redefined my original types in terms of the base...
type GameState = BGameState ()
type TwoPlayerGameState = BGameState (
userConnection :: Maybe ConnectedPeer,
opponentConnection :: Maybe Connection
)
Then I was able to use BGameState
in functions that could operate on both GameState
and TwoPlayerGameState
, since BGameState
only matches on the shared properties.
onEvent :: forall a b. EventType -> Ref (BGameState b) -> (Ref (BGameState b) -> { event :: Event | a } -> GameEffects Unit) -> Consumer { event :: Event | a } GameEffects Unit
As a side node, notice when I redefined GameState
and TwoPlayerGameState
, I used parenthesis to define the additional record properties. I won't explain that today, but you can read more about it in PureScript by Example - 5.7 Record Patterns and Row Polymorphism and PureScript by Example - 8.14 Objects And Rows.