TL;TR: using type parameters we can simplify the main update function, moving the logic to the submodule.

The code for this post is available on a github repository.


 

Achieving modularity in Elm is at the same time easy and hard.

It is easy because it’s almost impossible to break things even with big refactorings but on the other hand the constraints of pure functional programming can be challenging when deciding how to split the application into submodules.

The model

Imagine you have a Model describing a basic shopping cart:

Where the models in modules Details and Invoice are defined as:

This is an oversimplified example, just to show a basic modules subdivision, where we have only two messages: Increment and Decrement.

The isolated-model approach

The update functions

In the Details module, the update function will take care of updating the quantity field summing the input number, just preventing it from being assigned a negative amount:

In the Invoice module, the update function will take care of updating the total price based on the quantity value passed as an input:

Finally, the main update function is in charge of running the submodules’ functions and put together the data for the new model. This is the code handling the Increment message:

Let’s break it down: first, I calculate the new model for Details since its result will be used to calculate the price and I store it in newDetails.

Subsequently, I calculate the new model for Invoice using the helper function Details.mapQuantity since I don’t want the inner structure of Details model to be exposed. The helper function itself is simply:

Let alone the fact that the code can be written in a more concise (and somehow difficult to read) way, this pattern (which I called the Isolated Model) makes the main update function verbose and prone to errors, given that a simple application could be composed of tens of modules.

The most common mistake that I do is to assign the wrong model to the final model output:

The SuperModel approach

In Elm, I can tell the compiler that I want a record with some specific properties defined regardless of what else it contains:

It means that SuperModel is a union of a record of type a and a record with a field invoice of type Model.

We can now define the updates functions in terms of SuperModel a:

As you can see, the functions are a bit more verbose than before: we just moved the complexity into the single modules, thus allowing the Main update function to become simpler:

The main advantage in this scenario is that we can pipe the update functions (and even the map one) since they all accept the same input.

I’ve found this pattern pretty clean and usable because now my Main is easier to read and all the functions are guaranteed to receive as input the latest updated Model.

There is still room for logical errors though: for instance, I could have two interdependent updates be run in the wrong order. But with the piping syntax, I find them easier to spot and fix.