Elm & Components

Søren Debois
8 min readApr 8, 2016

--

The Elm Architecture (TEA) is conceptually very nice, but it forces us
to write large amounts of boilerplate whenever we need to use a “component”. In this post, I’ll explain the details of how we get rid of most of that boilerplate in the elm-mdl library.

Adding a button the TEA-way.

To appreciate why boilerplate is a problem in TEA, let’s first see what adding a component looks like in standard TEA. (The component of elm-mdl can all be used as “standard” TEA components if you wish.) Let’s say we are writing a TEA app, and we want to add an elm-mdl button. Here’s all the boilerplate we need to write:

Add the button to our model.

type alias Model = 
{ ...
, button : Button.Model
}
model : Model
model =
{ ...
, button = Button.model True -- Button with ripples
}

Add the button to our Action type.

type Action = 
...
| ButtonAction Button.Action

Add the button to our update function.

update action model =
case action of
...
ButtonAction action' ->
let
(button', fx) =
Button.update action' model.button
model' =
case action' of
Button.Click ->
-- Somehow update model to reflect the click
_ ->
model
in
( { model' | button = button' }
, Effects.map ButtonAction fx
)

We also need to render the button in our view function, but that’s not really boilerplate.

Here is a full example including the above code

Adding a button using Component Support

Now let’s do the same exercise, this time using the component support of the elm-mdl library. Using component support requires one-time boilerplate which we assume is already set up. In that case, all we need to do is:

Create a component instance.

increase : Button.Instance Mdl Action
increase =
Button.instance 0 MDL Button.flat (Button.model True)
[ Button.fwdClick ClickAction ]

And that’s it. There’s no more boilerplate you need to write, all that’s left to do is use the component’s view function (increase.view) in your view function. I’d claim this is substantially better than plain TEA.

What the above piece of code does is create a new instance of an elm-mdl button with id 0, rendering as Button.flat with initial model (Button.model True); when the button is clicked the action ClickAction is invoked.

The one thing you do pay is that you have to manually give component instances unique ids (here 0).

Setting up component support

What about the setup? Like in the TEA case, you have to add state container for the components you use to your model, you have to add an elm-mdl action to your Action, and you have to dispatch that action. The difference is that you don’t have to do it for every component you use, you need to do it only once, after which you can add any number of component instances without ever touching your Model, Action, and update again.

Here is the setup you need.

Add a single elm-mdl model container to your model.

type alias Model = 
{ ...
, mdl : Material.Model Action
}

model : Model
model =
{ ...
, mdl = Material.model
}

Add a single elm-mdl action to your Action.

type Action
= ...
| MDL (Material.Action Action)

Add a single elm-mdl dispatcher to your update.

update : Action -> Model -> (Model, Effects.Effects Action)
update action model =
case action of
...
MDL action' ->
let (mdl', fx) =
Material.update MDL action' model.mdl
in
( { model | mdl = mdl' } , fx )

That’s it. We emphasise that this is one-time: Adding more components (buttons, textfields, …) does not require you to repeat this boilerplate: The single field model.mdl contains the models of all components you use; the Action MDL contains actions for all components you use, and the single clause of the update function dispatches actions for all components you use. No matter how many components.

Here is a full example including the above code

Under the hood

If all you wanted to know was how to use the elm-mdl library, you can skip to the links at the end now. If you want to know how we did this, read on.

The straightforward way would be to simply make one giant Material.Model type which contains dictionaries for every component of elm-mdl:

type alias Material.Model = 
{ button : Dict Int Button.Model
, textfield : Dict Int Textfield.Model
...
}
type Material.Action
= ButtonAction Button.Action
| TextfieldAction Textfield.Action
...
update action model =
case action of
ButtonAction ->
...

We can then similarly make a big Action type and update function.

This approach is not viable because it forces users of component support to compile and include the code of every elm-mdl component, not just the ones they are using—compiled JavaScript size matters. (It also makes me queasy when adding components to the library, where now I have to add unpleasant boilerplate for each component.) We need a solution where you can use component support, but retain the option to somehow include only components you choose.

Let’s think about that. Here’s a type representing a TEA component:

type alias TEAComponent model action a = 
{ view : Signal.Address action -> model -> a
, update : action -> model -> (model, Effects action)
, model0 : model
}

The problem we want to solve is that we want to collect an arbitrary number of these with different types “model”, “action”, and “a” into a single TEAComponent, with single model and action types.

Embedding models

Let’s work on the model first. This is the easy bit: We can write a function which converts, say, a view function to a new function which knows how to extract its model from a Dict within a larger model.

type alias Embedding model container action a = 
{ view : View container action a
, update : Update container action
, getModel : container -> model
, setModel : model -> container -> container
}
type alias Indexed a =
Dict Int a
embedIndexed
: View model action a — view function,
-> Update model action — update function
-> (container -> Indexed model) — getter
-> (Indexed model -> container -> container)— setter
-> model — initial model
-> Int — instance id
-> Embedding model container action a
embedIndexed view update get set model0 id =
...

Note the types: We take as input view and update functions which work for “model”; we output an Embedding, which has view and update functions that work on “container”. Also note that the resulting Embedding internalises the id; the resulting view, update, and get/setModel functions know it internally, but it is not exposed to users anywhere.

When a component in elm-mdl declares its instance function, it does not know about the particular container model. It just requires that the container model has a spot for that kind of component. E.g., the view and update functions of Button work with any Model that has the type:

type alias Container c = 
{ c | button : Indexed Button.Model }

This gives us the flexibilty required: An end-user interesting in minising compiled JS size can create his own minimal model, that has fields for exactly the elm-mdl components he uses.

Embedding actions

The embedding above embeds a model in a larger model. How do we deal with actions? Actions exists exclusively to be dispatched by their corresponding update function. Boilerplate forwarding (as we saw in the TEA example in the very beginning) invariably looks like this:

update action model = 
case action of
...
ButtonAction action' ->
let
(button', fx) =
Button.update action' model.button
in
( { model | button = button' }
, Effects.map ButtonAction fx
)

If all we ever do with actions is apply update to them, we can just do that before dispatching the action in the view function—Elm is a functional language; partial application is a thing. Morally, we want to do this:

type Action = 
A (Model -> (Model, Effects Action))
update : Action -> Model -> (Model, Effects Action)
update action model =
case action of
A f ->
f model
... view addr model =
...
Button.view (Signal.forwardTo addr Button.update) ...
...

Note the types: Our new Action no longer mentions any specific component; that information lives instead inside the closure carried by A. Also note the uniformity: These Actions can be applied to any set of Embedded components that are embedded in the same type.

Obviously, we have to make view functions emit these partially applied update functions instead of their regular actions, but that is straightforward. (Actually, not really. But you can work it out from the types.)

Making observations

Sometimes, we do want to know about actions, though. E.g, we would like to know then a Button is clicked. The Action type of the actual elm-mdl component implementation looks like this:

type Action model obs = 
A (model -> (model, Effects (Action model obs), Maybe obs))

That is, the closure carried by A produces not only the usual updated model and effects, but also an “observation”, which users instantiate to their own Action type.

Putting it all together

We have now found a way to embed both the model and action types of TEA components in a way amenable to a single generic update function, providing observations if necessary. Given a TEA function, the single function instance does this lifting:

type alias Instance model container action obs a = 
{ view : View container obs a
, get : container -> model
, set : model -> container -> container
, map : (model -> model) -> container -> container
, fwd : action -> obs
}
instance
: View model action a
-> Update model action
-> (container -> Indexed model)
-> (Indexed model -> container -> container)
-> Int
-> (Action container obs -> obs)
-> model
-> List (Observer action obs)
-> Instance model container action obs a
instance view update get set id lift model0 observers =
...

Again, note the types: Given view and update functions that work “model” and “action” types, we produce an instance which embeds model in “container” and lifts action to “obs”. The instance function of Button is defined in terms of this generic function:

Button.instance id lift view model0 observers = 
Component.instance
view update
.button (\x y -> {y | button = x})
id lift
model0 observers

Conclusion

I’ve outlined above how elm-mdl provides completely standard TEA Model-Action-update-view representation of components, while at the same time providing a uniform representation of the same thing. For users, the net benefit is reduced boilerplate: You add the component Model, update, and Action to your existing app once, then add as many components as you like.

Elm has been criticised for not supporting a decently compositional component model. I think the elm-mdl component model demonstrates both that you can get pretty close with what we’ve got in 0.16, but also that you have to go through some rather non-obvious encodings to get there. As such, I’m not sure whether this post is a point for or against Elms current feature-set.

Links

--

--