Make the part, then join them

Breaking Up Update in Elm

James Carlson
5 min readDec 6, 2018

--

I’ve been working for some time on an Elm app whose update function has grown to over two thousand lines of code, code that processes over eighty messages. Surprisingly, this works out rather well, so long as you keep the code organized, with signposts placed as comments at strategic locations. This makes it is easy to move around in the file and to find the parts you want to work on. The structure is not so different from organizing the code into a set of files, where file names play the role of the signposts.

Despite this, I had been thinking of breaking up my huge update function. Then, this morning, over a steaming cup of fresh coffee, I read the first part of Sonnym’s post Types in Elm: Decomposition and Ad Hoc Polymorphism, in which he describes his method for doing just this. I decided to try his method, with the goal of adding a new component, Bozo, to the app I had been working on. Bozo would have a model with states Start, Up, and Down. It would have its own update function, which would act on the messages GoUp and GoDown. It would also have its own view functions — one to display the state, and two buttons labeled “Up” and “Down” to change the state. The code would be organized in three modules, Bozo.Model, Bozo.Update, and Bozo.View. Finally, integration of Bozo and the main app should be accomplished through a very small interface — just a few lines of code.

Carrying out the plan

To carry out the plan, we first made the Bozo.Model module, listed below. Notice that it has no imports, hence no dependencies of any kind.

module Bozo.Model exposinqqg (..)type alias BozoModel =
{ state : BozoState }
type BozoMsg
= MoveUp
| MoveDown
type BozoState
= Start
| Up
| Down
stringOfState : BozoState -> String
stringOfState bozoState =
case bozoState of
Start ->
"Start"
Up ->
"Up"
Down ->
"Down"
init : BozoModel
init =
{ state = Start }

The next step was to construct the view module (see below). Notice that it depends on Mathew Griffith’s elm-ui library for style, and also on Bozo.Model. There are no other dependencies. In particular, Bozo.View does not depend on the main app.

module Bozo.View exposing (..)import Element exposing (..)
import Element.Background as Background
import Element.Font as Font
import Element.Input as Input
import Bozo.Model exposing (BozoModel, BozoMsg(..), stringOfState)
view : BozoModel -> Element msg
view model =
el [] (text <| "Bozo state: " ++ (stringOfState model.state))
buttonUp : Element BozoMsg
buttonUp =
Input.button []
{ onPress = Just MoveUp
, label = el buttonStyle (text "Up")
}
buttonDown : Element BozoMsg
buttonDown =
Input.button []
{ onPress = Just MoveDown
, label = el buttonStyle (text "Down")
}
buttonStyle =
[ Background.color (rgb255 80 80 80)
, Font.color (rgb 255 255 255)
, paddingXY 8 4
]

To complete the definition of Bozo, we add the update function:

module Bozo.Update exposing (..)import Bozo.Model exposing (..)
import Model exposing (Model, Msg)
update : BozoMsg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
MoveUp ->
( updateState Up model, Cmd.none )
MoveDown ->
( updateState Down model, Cmd.none )
updateState : BozoState -> Model -> Model
updateState nextState model =
let
bozoModel =
model.bozo
nextBozoModel =
{ bozoModel | state = nextState }
in
{ model | bozo = nextBozoModel }

This module has one internal and one external dependency: Bozo.Model and Model, respectively. Note that the only dependency of the entire Bozo.* packet is on the Model module of the main app.

Connecting Bozo to the main app

So far, so good. The main app, with Bozo.* added, compiles. It is now time to connect the new code to the main app. This will require small changes to the Model, View, and Update modules of the main app. For the Model, there are four additions, the first of which is an import:

(1) import Bozo.Model exposing (BozoModel, BozoMsg)

Next, we add a bozo field to the main model:

(2) type alias Model =
{ message : String
, bozo : BozoModel
, ...

This requires an addition to the init code:

(3) initialModel =
{ message = "Hey, starting up!"
, bozo = Bozo.Model.init
, ...

Finally, we add Bozo BozoMsg to Msg:

(4) type Msg
= NoOp
| Test
| Bozo BozoMsg
...

Connecting the update functions

To connect the two update functions, we add two lines to the main update module. The first is the line import Bozo.Update. The second is as below:

update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
NoOp ->
( model, Cmd.none )
Bozo bozoMsg ->
Bozo.Update.update bozoMsg model
...

Connecting the Views

The footer of the app was a natural place for connecting the view of the main app to Bozo’s views:

footer model = 
Element.row footerStyle [
...
, Element.map Bozo Bozo.View.buttonUp
, Element.map Bozo Bozo.View.buttonDown
, Bozo.View.view model.bozo
]

Notice that we had to apply Element.map Bozo to the button views to ensure that they have the correct type.

Results of the experiment

The results of the experiment (which required an additional cup of coffee) were excellent. The state of the Bozo “component” showed up in the footer, along with the two buttons. Clicking the buttons modified the state accordingly. The design goals were met: a set of modules Bozo.* with a very small dependency (one line) on the main app, and a relatively small number of lines added to the main app needed to perform a “mind meld” of the two. The process of constructing the Browser.* packet and integrating it with the main app was straightforward and hassle-free, given structural plan laid out in Sonnym’s post. With a validated strategy for breaking up my huge update function, I feel that I can now proceed with confidence.

Addendum

Several members of the Elm Slack commented on the fact that Bozo.* is not completely isolated from the main app. (Thanks @augustin82 !!) One can do better as follows. First, in Bozo.Update, delete the line import Model exposing(Model, Msg). Disconnect achieved! Second, in the main Update module, add the line

import Bozo.Model exposing (BozoModel, BozoMsg)

just as done in the main Model. Third, still working in Update, make the definition

bozoMap : Model -> ( BozoModel, Cmd BozoMsg ) -> ( Model, Cmd Msg )
bozoMap model ( bozoModel, bozoMsg ) =
( { model | bozo = bozoModel }, Cmd.map Bozo bozoMsg )

Fourth, change the Bozo bozoMsg clause of the main update function to read as follows:

Bozo bozoMsg ->
Bozo.Update.update bozoMsg model.bozo
|> bozoMap model

With these changes, one achieves complete isolation of Bozoin the sense that it imports nothing from the main app. Of course the main app must import Bozo.* if it is to make use of it.

I thank members of the Elm Slack for their insightful comments on this post.

--

--