Thursday, March 15, 2018

Functional Adventures in F# - Application State


In this article we will go through how to handle complex application state in F#. This will in practice be a rewrite of 'Functional Adventures in C# - Application State'. So I will probably skip a lot of the details and focus on the F# implementation.

This post is part of a series:
Functional Adventures in F# - A simple planner
Functional Adventures in F# - Calling F# from C#
Functional Adventures in F# - Using Map Collections
Functional Adventures in F# - Application State
Functional Adventures in F# - Types with member functions
Functional Adventures in F# - Getting rid of loops
Functional Adventures in F# - Getting rid of temp variables
Functional Adventures in F# - The MailboxProcessor
Functional Adventures in F# - Persisting Application State
Functional Adventures in F# - Adding Snapshot Persisting

Overview

Illustration of Application State, different Stores and their model objects. For a lot of illustrations go read the C# article!
Lets be real, nobody read the C# version first. So I'll paste the overview here.
At any given point in time, the whole application state is stored in a class named Application State, it consists of different Stores and they in turn consist of the object model for each store. I.e. Fleet Store might contain Fleets that consists of Ships. All updates to the model are routed to the store in the form of Actions. The application state is immutable, meaning that any object that you get a reference to will never mutate into something else (see previous post in this series on Immutable objects)
So. from the text above we need to define Actions, Application State, and Stores and we should be set to go.

Actions

Actions will be implemented as basic record types. There is no need for anything else fancy. An action should contain all information that is needed to create a new application state.
      newApplicationState = currentApplicationState + action
The code for this part looks like the following
module Actions =
    type InsertCashToPlayerAccount = { Player:GameModel.PlayerId; Amount:GameModel.Cash }
    type Tick = { Time:float; Delta:float }
We defined 2 actions. One that puts money into the players account and the other lets us update each store before rendering a new frame in a game.

Application State

Next up, we will look at the Application State definition, or in this example the GameState definition
module GameState =
    type GameState =
        {
            PlayerStore:PlayerStore
            UnitStore:UnitStore
        }
Here we just define that GameState is a record type that contains 2 stores, the PlayerStore and the UnitStore. Here we will also be adding a way to handle actions, so lets continue with the GameState definition
        
        static member handleAction (state:GameState) (action:obj) =
            {
                PlayerStore = PlayerStore.handleAction state.PlayerStore action
                UnitStore = UnitStore.handleAction state.UnitStore action
            }
Here we add a static member function to the handleAction type (that can be called as: 'GameState.handleAction state action'). We have defined the types here, the state is a GameState and the action is typed to obj, meaning that we can send in any type into this function and it will work. I.e. when we define new actions, we don't need to change anything here. Only if we add a new Store, then we need to add it here obviously.
Also, worth noting is that the action is in turn sent to all Store handlers together with the part of the state that is handled by that Store. Meaning that we can define an action, that is handled by multiple stores. For example the Tick action above.
So basically the handleAction function just builds a new Application State by calling each store and telling them to handle their parts.

Stores

OK, so whats so special about the stores? Nothing really, the Application State is practically a store in itself,
module PlayerStore =
    type PlayerStore =
        {
            Players: Map<PlayerId, Player>
            CurrentTime: float
        }
        static member handleInsertCashToPlayerAccount (state:PlayerStore) (action:InsertCashToPlayerAccount) =
            let player = state.Players.[action.Player]
            let newPlayer = { player with Cash = player.Cash + action.Amount }
            { state with Players = state.Players.Add(player.Id, newPlayer) }            
        static member handleTick (state:PlayerStore) (action:Tick) =
            { state with CurrentTime = action.Time }
        static member handleAction (state:PlayerStore) (action:obj) =
            match action with
            | :? InsertCashToPlayerAccount as a -> PlayerStore.handleInsertCashToPlayerAccount state a
            | :? Tick as a -> PlayerStore.handleTick state a
            | _ -> state

PlayerStore is a record type, just as the GameState type, with a static member handleAction function that has a little different implementation. The key here is the pattern matching on the action, we use the type of the action to determine what handler function should be called to perform whatever it is that the action needs done. As a last state, we have the wild card pattern '_' that returns the old state as it was sent in. I.e. if this actionHandler can't handle the action, nothing is done.
Also notable in the handleInsertCashToPlayerAccount function, the new state is built by taking the current state and applying the action. In this example, finding the player object and constructing a new player object with the added cash in it.

Lastly, lets look at the UnitStore so that we have all the code for our example
module UnitStore =
    type UnitStore =
        {
            Units: Map<GameObjectId, Unit>
            CurrentGameObjectId: GameObjectId
            CurrentTime: float
        }
        static member handleTick (state:UnitStore) (action:Tick) =
            { state with CurrentTime = action.Time }
        static member handleAction (state:UnitStore) (action:obj) =
            match action with
            | :? Tick as a -> UnitStore.handleTick state a
            | _ -> state
Same thing here, the handleAction function takes the UnitStore and the action and then matches an action handler function if one exists to create a new state, otherwise it will just hand back the original state.

Tests
[<TestClass>]
type StoreTests () =
    member this.createDefaultStore =
        { 
            PlayerStore = 
                { 
                    Players = Map.empty.Add(1, { Id = 1; Name = "Player1"; Cash = 0; CurrentSelection = [] })
                    CurrentTime = 0.0
                }
            UnitStore =
                {
                    Units = Map.empty
                    CurrentGameObjectId = 0
                    CurrentTime = 0.0 
                }
        }
For starters, we define a function that creates the initial GameState that will be used by all of our tests.
    [<TestMethod>]
    member this.StoreTests_GameState_UnknownType () =
        let originalState = this.createDefaultStore
        let action = 1
        let newState = GameState.handleAction originalState action
        Assert.AreEqual(originalState, newState)
First test, just check that if we send in an unknown action, we get back the original state object, nothing changed, the exact same object.
    [<TestMethod>]
    member this.StoreTests_GameState_UpdatePlayerStore () =
        let originalState = this.createDefaultStore
        let action = { Player = 1; Amount = 10 }
        let newState = GameState.handleAction originalState action
        Assert.AreNotEqual(originalState, newState)
        Assert.AreEqual(originalState.UnitStore, newState.UnitStore)
        Assert.AreEqual(10, newState.PlayerStore.Players.[1].Cash)
Secondly we test that an action that should update 1 store, does that, but leaves the other stores unchanged.
    [<TestMethod>]
    member this.StoreTests_GameState_UpdateMultipleStores () =
        let originalState = this.createDefaultStore
        let action = { Time = 22.0; Delta = 0.015 }
        let newState = GameState.handleAction originalState action
        Assert.AreNotEqual(originalState, newState)
        Assert.AreEqual(22.0, newState.PlayerStore.CurrentTime)
        Assert.AreEqual(22.0, newState.UnitStore.CurrentTime)
The last test just verifies that an action can be handled by multiple Stores.

Finally

There are probably a lot of things that could be written in a better way, I am still learning F# and finding new ways to do things daily. Overall I find that this language has nice constructs for many of the things I tried to do with C# when I tried functional constructs there. A lot less plumbing here as the language has built in support for many of the things, lets one focus on actually writing functionality instead of plumbing code. I am glad that I tried this out : )

All code provided as-is. This is copied from my own code-base, May need some additional programming to work. Use for whatever you want, how you want! If you find this helpful, please leave a comment, not required but appreciated! :)

Hope this helps someone out there!
Until next time: Work to Live, Don’t Live to Work

No comments:

Post a Comment