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.