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:

StartGame message is triggered during Start state and transmits the model into Playing state

message is triggered during state and transmits the model into state RestartGame message is triggered during GameOver state and transmits the model into Playing state

message is triggered during state and transmits the model into state UpdateWord AddWord ClearField and Tick messages are triggered during Playing state and do not change the type of state, but EndRound changes

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 ✊

Please enable JavaScript to view the comments powered by Disqus.