Scaling Large Elm Applications

In that article I developed an application which works fine, but not very convenient to extend. That is why my goal is to take advantage of a tree structure and optimally break the application down to multiple files with a narrow scope, in order to keep fewer things in mind on changing a particular part of the application.

The goal is to take advantage of a tree structure of an application since my brain processes linear structures with O(n) complexity.

By the way this guide would perfectly solve this kind of a problem for the applications with models of product type and it is actually cool how the app starts kind of consisting of smaller subwidgets, which are closed under a collection of operations, i.e. a submodel has the same type after performing an operation from the collection.

The model of my application is particularly of sum types, which comprises different states: Start, Playing and GameOver – the concepts which potentially can be extracted into smaller subwidgets. The problem is that the application should be able to transit from one state to another, i.e. these subwidgets no longer have the closure under all the commands that trigger the update, since one of the commands should update a submodel to a different state.

The problem is to scale an app with transitions from one state to another, which is quite natural for the games since games often have different states of the whole application with an independent state.

Let’s start from creating three separate files for every state and moving the code there: Start.elm, Playing.elm and GameOver.elm.

Model


The model structure changes from

Main.elm

type Model
    = StartRound
    | PlayingRound PlayingRoundState
    | GameOver GameOverState

type alias Sentence =
    { prefix : String
    , word : String
    , description : String
    , synonyms : Set.Set String
    }

type alias PlayingRoundState =
    { words : List String
    , sentence : Sentence
    , elapsed : Float
    , word : String
    , wrongWord : Bool
    }

type alias GameOverState =
    { hint : Maybe String
    , score : Int
    }

To

Main.elm

import Start as StartWidget
import Playing as PlayingWidget
import GameOver as GameOverWidget

type Model
    = Start
    | Playing PlayingWidget.Model
    | GameOver GameOverWidget.Model

Playing.elm

type alias Sentence =
    { prefix : String
    , word : String
    , description : String
    , synonyms : Set.Set String
    }

type alias Model =
    { words : List String
    , sentence : Sentence
    , elapsed : Float
    , word : String
    , wrongWord : Bool
    }

GameOver.elm

type alias Model =
    { score : Int
    , hint : Maybe String
    }
Messages


Currently, the messages structure is

Main.elm

type Msg
    = NoOp
    | StartGame
    | UpdateWord String
    | AddWord
    | ClearField
    | EndRound (List String) Int
    | Tick Time
    | RestartGame

Let us notice that:

The idea is to create a Transition message for every state and process it on a higher level in order to transit the application to another state.

Main.elm

type Msg
    = StartMsg StartWidget.Msg
    | PlayingMsg PlayingWidget.Msg
    | GameOverMsg GameOverWidget.Msg
    | NoOp

Start.elm

type Msg
    = Transition

Playing.elm

type Msg
    = SentenceMsg Sentence.Msg
    | Tick Time
    | EndGame (List String) Int
    | Transition Int (Maybe String)

GameOver.elm

type Msg
  = Transition
  | Restart
Update

As a result, we receive quite general update function, which either calls update functions of a subwidget depending on the message received or transmits the model into another type of state

Main.elm

update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case ( msg, model ) of
        ( StartMsg StartWidget.Transition, _ ) ->
            let
                ( newModel, subMsg ) =
                    PlayingWidget.init
            in
                ( Playing newModel, Cmd.map PlayingMsg subMsg )

        ( PlayingMsg (PlayingWidget.Transition score hint), _ ) ->
            ( GameOver <| GameOverWidget.init score hint, Cmd.none )

        ( PlayingMsg m, Playing state ) ->
            let
                ( newModel, subMsg ) =
                    PlayingWidget.update m state
            in
                ( Playing newModel, Cmd.map PlayingMsg subMsg )

        ( GameOverMsg GameOverWidget.Transition, _ ) ->
            let
                ( newModel, subMsg ) =
                    PlayingWidget.init
            in
                ( Playing newModel, Cmd.map PlayingMsg subMsg )

        ( GameOverMsg m, GameOver state ) ->
            let
                ( newModel, subMsg ) =
                    GameOverWidget.update m state
            in
                ( GameOver newModel, Cmd.map GameOverMsg subMsg )

        _ ->
            model ! []
View

The app renders view according to the current state and wraps the outgoing messages from subviews using Html.map

Main.elm

view : Model -> Html Msg
view model =
    case model of
        Start ->
            Html.map StartMsg <| StartWidget.view

        Playing state ->
            Html.map PlayingMsg <| PlayingWidget.view state

        GameOver state ->
            Html.map GameOverMsg <| GameOverWidget.view state

Subscriptions are handled similarly (the whole app can be viewed on GitHub )


Conclusion

Since we deal with mere functions and groups of functions (modules), we possess enough flexibility to say that the provided example is not the only way to structure your application. For instance, we could pass PlayingMsg function into update functions of Start or GameOver modules instead of processing Transition messages. And it is great, that we can choose an approach which fits best for solving a particular problem ✊