Image taken from unsplash, @ashkfor121

The Principles Behind TEA-Combine

Attempting to Programmatically Combine Stateful Components in The Elm Architecture

Mattia Maldini
16 min readOct 18, 2020

--

It’s been a while since I fiddled with Elm. Though I don’t use it regularly I sometimes take a stroll around it to see how it’s doing. This time I stumbled upon tea-combine, a small library made with the purpose of easily glueing together subcomponents of a bigger application.

Since I only use Elm for fun it’s unlikely I’ll ever really need it. Regardless I was curious to understand how it worked under the hood and the author prompted me to write a small tutorial about it, so here we are. This will be an interesting example on how to use the basic type combination offered by a bare functional language.

In the first part I will implement manually what tea-combine does automatically; then I will proceed to explain in detail how such combination works, stepping through some functions that achieve this result.

The Use Case

Let’s take the library’s front page example and try to implement it in vanilla Elm, without fancy combinations. The result will be a barren web page, with just an interactive counter and two checkboxes.

Simple but effective

I encourage you to know what The Elm Architecture is (and Elm syntax in general) before delving deeper here. In a way, Elm can be considered more of a UI framework than a real programming language, and TEA is the template it uses to build applications.

To build our web page we need three things:

  • a Model that embeds the state of the web page (in this case, a number and two booleans)
  • an update functions that reacts to events and changes the Model accordingly
  • a view function that renders our data as html on the web page

The Model should be a custom type that will be handled by update and view. With three elements to keep track of we can define it as a record:

type alias Model =
{ checkboxState1 : Bool, checkboxState2 : Bool, counterState : Int }

Each widget gets its own field in it. Then we define the events that will be fired by the html and the update function that codifies the corresponding reaction.

There are three Msg variants: two of them simply signal that a checkbox has been toggled and the third mentions that the counter has been changed, carrying the new value to be updated.

type Msg
= CheckboxToggle1
| CheckboxToggle2
| CounterNumber Int
update : Msg -> Model -> Model
update msg model =
case msg of
CheckboxToggle1 ->
{ model | checkboxState1 = not model.checkboxState1 }
CheckboxToggle2 ->
{ model | checkboxState2 = not model.checkboxState2 }
CounterNumber counter ->
{ model | counterState = counter }

Brilliant. Finally, we move onto displaying these information with the view function:

view : Model -> Html Msg
view model =
Html.div []
[ Html.span
[]
[ Html.button [ onClick <| CounterNumber <| model.counterState - 1 ] [ Html.text "-" ]
, Html.text <|
Debug.toString model.counterState
, Html.button [ onClick <| CounterNumber <| model.counterState + 1 ] [ Html.text "+" ]
]
, Html.input
[ checked model.checkboxState1
, type_ "checkbox"
, onClick CheckboxToggle1
]
[]
, Html.input
[ checked model.checkboxState2
, type_ "checkbox"
, onClick CheckboxToggle2
]
[]
]

Notice how Html is not really a type on its own, but a type constructor that needs our Msg to be completed. The view returns a special flavor of Html enhanced with the events that we specified.

Patch those three elements together in a Browser.sandbox element and we have a complete Elm app that implements the Simple.elm example for tea-combine. Here’s the full version.

module Main exposing (main)import Browser
import Html exposing (Html)
import Html.Attributes exposing (..)
import Html.Events exposing (..)
main =
Browser.sandbox
{ init =
{ checkboxState1 = False, checkboxState2 = False, counterState = 0 }
, view = view
, update = update
}

-- MODEL
type alias Model =
{ checkboxState1 : Bool, checkboxState2 : Bool, counterState : Int }
-- UPDATEtype Msg
= CheckboxToggle1
| CheckboxToggle2
| CounterNumber Int
update : Msg -> Model -> Model
update msg model =
case msg of
CheckboxToggle1 ->
{ model | checkboxState1 = not model.checkboxState1 }
CheckboxToggle2 ->
{ model | checkboxState2 = not model.checkboxState2 }
CounterNumber counter ->
{ model | counterState = counter }

-- VIEW
view : Model -> Html Msg
view model =
Html.div []
[ Html.span
[]
[ Html.button [ onClick <| CounterNumber <| model.counterState - 1 ] [ Html.text "-" ]
, Html.text <|
Debug.toString model.counterState
, Html.button [ onClick <| CounterNumber <| model.counterState + 1 ] [ Html.text "+" ]
]
, Html.input
[ checked model.checkboxState1
, type_ "checkbox"
, onClick CheckboxToggle1
]
[]
, Html.input
[ checked model.checkboxState2
, type_ "checkbox"
, onClick CheckboxToggle2
]
[]
]

Before we proceed further, I am obligated to specify that this is exactly how Elm is supposed to work: encode the information in the Model and handle it yourself (eventually with helper functions).

In the next steps I’m going to explain how to make subcomponents for counters and checkboxes even though it is an exceedingly overkill procedure for such simple elements. The Elm philosophy is to avoid this approach at all costs, even though it is inevitable once the application grows beyond a certain point.

Here however I’m trying to build a conceptual example, so we’re going to ignore politics and good manners for the sake of science.

Industrializing the Process

While the end result is not bad, you might notice that it could be perfected. For example, the rendering of the two checkboxes has a lot of repeated code; defining an helper function would make it more readable.

The same goes for the counter, even if there is only one. We might need multiple counters in the future though, so making the code cleaner now is a good idea.

The update function and Model could be split as well. It’s not much code now, but if the widget management grew more complex it would become quite bothersome to handle all those cases in full. Let’s add two separate modules, CheckBox.elm and Counter.elm, as smaller MVU (Model-View-Update) patterns. This is exactly what tea-combine does in its example (here and here — take a look).

Now that we effectively created two separate components we need to embed them into a bigger application. It’s a matter of replacing the subcomponent piece in every part of our bigger Elm Architecture:

-- MODELtype alias Model = 
{ checkboxState1 : CheckBox.Model, checkboxState2 : CheckBox.Model, counterState : Counter.Model }

-- UPDATE
type Msg =
CheckboxToggle1 CheckBox.Msg
| CheckboxToggle2 CheckBox.Msg
| CounterNumber Counter.Model

update : Msg -> Model -> Model
update msg model =
case msg of
CheckboxToggle1 unit ->
{ model | checkboxState1 = CheckBox.update unit model.checkboxState1 }
CheckboxToggle2 unit ->
{ model | checkboxState2 = CheckBox.update unit model.checkboxState2 }
CounterNumber counter ->
{ model | counterState = Counter.update counter model.counterState }

-- VIEW
view : Model -> Html Msg
view model =
Html.div []
[ Counter.view model.counterState
, CheckBox.view model.checkboxState1
, CheckBox.view model.checkboxState2
]

Let me once again remind you that this is a purposefully contrived example. The amount of abstraction we are using is not worth given the simplicity of the application; I’m doing this for the sake of argument.

Unfortunately, this does not compile. Running elm make stops with the following error:

Detected errors in 1 module.                                         
-- TYPE MISMATCH -------------------------------------------------- src/Main.elm
The 2nd element of this list does not match all the previous elements:57| [ Counter.view model.counterState
58|> , CheckBox.view model.checkboxState1
59| , CheckBox.view model.checkboxState2
60| ]
This `view` call produces:Html CheckBox.MsgBut all the previous elements in the list are:Html Counter.MsgHint: Everything in the list needs to be the same type of value. This way you
never run into unexpected values partway through. To mix different types in a
single list, create a "union type" as described in:
<http://guide.elm-lang.org/types/union_types.html>

The problem is that the top view function returns Html Msg — with Msg being the main’s module message type. We created the submodule’s view as a completely autonomous Model -> Html Msg function, where Msg is the submodule’s message type , even if it has the same name (namespaces save us here).

For now, we fix this error by passing the message to be used to the view function of the widgets. It’s a temporary solution until we find a better way to compose them.

checkBoxView : CheckBox.Model -> msg -> Html msg
checkBoxView model msg =
Html.input
[ checked model
, type_ "checkbox"
, onClick msg
]
[]

counterView : Counter.Model -> msg -> Html Msg
counterView model msg =
Html.span []
[
Html.button [ onClick <| CounterNumber <| model - 1 ] [ Html.text "-" ]
, Html.text <|Debug.toString model
, Html.button [ onClick <| CounterNumber <| model + 1 ] [ Html.text "+" ]
]
view : Model -> Html Msg
view model =
Html.div []
[ counterView model.counterState <| CounterNumber model.counterState
, checkBoxView model.checkboxState1 <| CheckboxToggle1 ()
, checkBoxView model.checkboxState2 <| CheckboxToggle2 ()
]

Now it works, and it handles its subcomponents majestically! Are we done?

Boilerplate Code

No, we are not done yet. Although it is true that we defined two separate and (almost) autonomous components, there should be still something buzzing you.

Whenever you add a new instance of those components you need to write some “glue” code to attach it to your application: a new field in the Model, a new Msg case, a new corresponding new branch in the update function. The only part that’s completely reusable is the helper rendering function.

image courtesy of @scottsanker from unsplash

Admittedly, it’s not much. Adding a new line here and there, just calling a function with a different parameter to differentiate. If you have multiple components of the same type (i.e. 20 checkboxes) you might also create a checkbox List and handle them with an index. It’s not much, but it’s there.

This is a common problem in Elm applications. Even in Richard Felman’s Real World Example we can see this pattern: the Main.elm file is basically just a huge routing switch for all of the website’s pages.

It’s a 300-lines file of boilerplate code — “glue” code that doesn’t express any significant meaning, and that could be mechanized or abstracted away — over a 4000 LoC project. The official stance of the Elm team is that “it’s not a big deal”. While to an extent this is true, it doesn’t mean we cannot look for a solution.

Lack of Abstraction

This apparently simple problem becomes much harder for a language lacking powerful abstraction tools like Elm. For instance, if we had access to some form of ad hoc polymorphism (that you might know as Typeclasses, Traits, Interfaces or something else depending on your coding background) it would be trivial to define a component as something that is “Updatable” and “Renderable” through self-provided functions. Then, the Model would just contain a list of those components and invoke the provided update and view functions as needed.

Unfortunately, we can’t do that in Elm. For the sake of simplicity and performance its creator decided to leave out those tools and, to the best of everyone’s knowledge, there is no plan to introduce them at some point in the future.

Again, we shouldn’t be discouraged, as tea-combine provides a very smart solution even without these tools.

Automatically Combine the Components

Let’s see how we can mix together multiple components that expose just their version of a MVU application (a Model type, Msg type and update function, and a view function), just like we did with Counter and CheckBox, but without having to manually write out the glue code. We start with the Model.

Combining the Model

In our previous example, the top Model type was a record with a field for each component. A record is nice to handle manually but not exactly easy to automate, as we are forced to create new fields ourselves.

A list seems more approachable before you realize that it can only contain elements of the same type, and that’s no good with heterogeneous components (for homogeneous content,the list option is available in tea-combine as well). The only structure that remains is the tuple.

Indeed, we could define our Model as a tuple containing instances of the subcomponents’ Models, just like this:

type alias Model = (Counter.Model, CheckBox.Model, CheckBox.Model)

This works, but only briefly. If we try to add a fourth element (for example, another checkbox), the compiler stops us:

Detected errors in 1 module.                                         
-- BAD TUPLE ------------------------------------------------------ src/Main.elm
I only accept tuples with two or three items. This has too many:23| type alias Model = (Counter.Model, CheckBox.Model, CheckBox.Model, CheckBox.Model)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
I recommend switching to records. Each item will be named, and you can use the
`point.x` syntax to access them.
Note: Read <https://elm-lang.org/0.19.0/tuples> for more comprehensive advice on
working with large chunks of data in Elm.

You read that correctly: tuples in Elm are only two or three items wide.

I wouldn’t call myself an expert, but I like to study different approaches and I fiddle with many programming languages. While I often marvel at their exotic features, it’s the first time I stumble upon an exotic limitation. The rationale behind this is that bigger tuples are rarely needed in practice, and records are better at this job anyway.

We briefly stop to thank Elm for knowing what’s better for us and try to work things out differently.

There is in fact another way to stuff arbitrary elements in a tuple: nesting tuples. This

type alias Model = (((Counter.Model, CheckBox.Model), CheckBox.Model), CheckBox.Model)

is a tuple containing a tuple containing a tuple containing a counter and a checkbox, and a checkbox, and a checkbox. Quite a mouthful, but allowed nonetheless.

This finally brings us to the first piece of tea-combine: the initWith function:

initWith : model2 -> model1 -> Both model1 model2                       initWith m2 m1 = ( m1, m2 )

This function is polymorphic on its two parameters (any type will do) and simply builds a tuple out of them. Both is just a type alias for tuples of two elements.

Take a look at the usage of this function in the Simple.elm example.

Counter.init 0
|> initWith (CheckBox.init False)
|> initWith (CheckBox.init False)

This creates a structure exactly like the one we defined before, ((Counter.Model, CheckBox.Model), Checkbox.Model), and it can be arbitrarily nested. If you are somewhat accustomed to type theory, you can see the combined Model is a product type of all the submodules’ Models.

Combining the Update

When using with The Elm Architecture we are working with two main custom types: Model and Msg. You might be tempted to combine the Msg types in the same way as we did with the Model, but that’s not quite right.

While the application Model must contain a piece of each component at all times, a Msg is from only one of the components at any given moment. In other words, it can be Either one of them. The resulting type is a sum (or tagged union) of all those message types.

Tea-combine uses Either for this job. You can imagine an Either type just like a tuple, but only one of the possible elements is present at any given time.

Here is how tea-combine does this, with the updateWith function:

updateWith : Update model2 msg2 -> Update model1 msg1 -> Update (Both model1 model2) (Either msg1 msg2)                       updateWith u2 u1 =
Either.unpack (Tuple.mapFirst << u1) (Tuple.mapSecond << u2)

This is a little more complex than initWith, mostly because it must return a function instead of a normal value. Update model msg is just an alias for msg->model->model, which is the specified type for update functions.

The main point is the unpack function of the Either module. Its type signature is (a -> c) -> (b -> c) -> Either a b -> c: it takes two functions that return the same type c and builds a function that takes an Either and returns said c. In human words, it’s saying: give me a way to handle both cases of an Either type and I will handle the choice for you; if it’s an a I will apply the first function, if it’s a b the second one, returning a c regardless.

The parameters given to unpack are two partially composed functions. I will focus on the first one, and give the second for granted (the principle is identical).

The first function is (Tuple.mapFirst << u1), and it’s supposed to fill the place of the (a->c) argument for unpack. Here a is msg1 and b is Both model1 model2. The << operator stands for function composition; written with a lambda function it would be

\msg -> \(m1, _) -> u1 msg m1

Which means: take a msg , then take a tuple with m1 as the first element and apply those to u1. The pieces are all there, and the composition operator turns them into a single function that adheres to the requirements for unpack. The second part is the same, only on the second element of the tuple.

To recap: updateWith takes two update functions on separate models and messages and turns them into a single function on the product of the models and the sum of them messages — all the models and one of the messages coming from any of the components.

This construction can be nested and indeed it is when applied several times.

Counter.update
|> updateWith CheckBox.update
|> updateWith CheckBox.update

This results in a function with the signature Update (Both (Both Counter.Model CheckBox.Model) CheckBox.Model)) (Either (Either Counter.Message CheckBox.Message) CheckBox.Message). Quite a mouthful again, but it’s automatically handed to us. We need not to worry about this composition at all, just roll with it.

Composing the View

Adding up view function is just a repetition of the update case, but with a twist. There is a viewBoth function that mashes together two views just like initWith does for Models and updateWith does for updates.

viewBoth : View model1 msg1 -> View model2 msg2 -> Both model1 model2 -> Both (Html (Either msg1 msg2)) (Html (Either msg1 msg2))                       viewBoth v1 v2 ( m1, m2 ) = 
( Html.map Left <| v1 m1, Html.map Right <| v2 m2)

This is nothing new, so I’m not going to unwrap it in detail. The view function usually handles lists of Html elements, so we need to transform this part a bit. There is another function that supports this task, joinViews

joinViews : View model1 msg1 -> View model2 msg2 -> Both model1 model2 -> List (Html (Either msg1 msg2))                       joinViews v1 v2 m = 
let
( h1, h2 ) = viewBoth v1 v2 m
in
[ h1, h2 ]

Don’t get distracted by the (as usual) huge type signature, and just look at the body: take two views, combine them and return them as a List.

Then, one more piece: once we have a list of two views we need a way to append another one. This is a job for withView

withView : View model2 msg2 -> (model1 -> List (Html msg1)) -> Both model1 model2 -> List (Html (Either msg1 msg2))                       withView v2 hs ( m1, m2 ) = 
List.append (List.map (Html.map Left) <| hs m1)
[ Html.map Right <| v2 m2 ]

This is a little more convoluted, so I’ll explain it step by step.

This function takes a simple view and the output from joinViews (a function transforming a Model into a list of Html elements) and returns a combined view that acts on the combination of Model and Msg.

It’s important to remember that the functions under scrutiny are almost exclusively polymorphic; a model1 type parameter could be the simple state of a single component or a composition of multiple models.

Onto the body: the main purpose of this function is to append an element, so of course it starts by calling List.append: for its first argument (the element to be added) it applies hs to m1, obtaining a List of Html msg1; then it applies Left the contents (the messages) of each Html in the list through a double map, turning them into instances of Either.

All of this is a new element for the list to be returned. That is built by invoking v2 on m2; this creates a single Html msg2, that is turned into Html (Either msg1 msg2) with the Right map and then into a list between the two square brackets.

Simple, isn’t it?

We finally have all the pieces to explain the view function of the Simple.elm example:

Html.div []
<< (joinViews Counter.view CheckBox.view
|> withView CheckBox.view
)

joinViews creates the initial list to which CheckBox.view is then appended. Everything is combined with Html.div [] for a final function that will convert a Model (made up by the combination of models) into the final Html rendering.

Conclusion

Phew! Is it over? For this article yes, but tea-combine goes even further. First of all, we never even considered the possibility of adding subscriptions and commands to our components. Those are handled in a similar fashion by the Effectful module.

Then there is the option to manage an array of components of the same family (instead of heterogeneous, arbitrary components) with viewEach, viewSome, updateEach and updateAll: the former two allow to render all or part of the Model array as html, while the latter update an array of Models with a single update function or list of updates, respectively.

Right now you should be able to define multiple, heterogeneous, stateful components with their own MVU pattern and combine them at leisure in bigger applications. The idea is very simple but clever, and I enjoyed untangling those function signatures like they were puzzles.

That being said, there are some downsides to the approach. Mostly it’s about the composition: it’s supposed to be automatic, but there might be cases where you need to look inside the resulting Model. In these situations the only way around is to know how the tuples are built and extract what you need manually. Remember that even if you don’t add type signatures the elm compiler can infer them for you, so you don’t need to calculate them yourself.

You also need to be careful about the order of composition. In the Simple.elm example the three widgets are composed in the same order for the three fields of the sandbox record; failing to do so (e.g. starting with a CheckBox instead of Counter in the update function) will result in a cryptic compiler error complaining about a type mismatch: this is the result of different pieces having their elements in different order, which make them incompatible with one another. Although it carries the same information, (Counter.Model, CheckBox.Model) is different from (CheckBox.Model, Counter.Model).

To sum it up, tea-combine automatically combine MVU components to be reused into bigger applications; the construction is mechanic but it falls onto the developer to ensure it is identical every time, and it might required to be manually unwrapped to perform certain tasks.

A little cumbersome, but certainly not worse than writing all the boilerplate code yourself.

--

--

Mattia Maldini

Computer Science Master from Alma Mater Studiorum, Bologna; interested in a wide range of topics, from functional programming to embedded systems.