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:

1 2 3 4 5 |
type alias Model = { name : String , details : Details.Model , invoice : Invoice.Model } |

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

1 2 3 4 5 6 7 8 9 10 |
-- Details.elm type alias Model = { quantity : Int } -- Invoice.elm type alias Model = { price : Float } |

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:

1 2 3 4 5 6 |
update : Int -> Model -> Model update number model = if model.quantity == 0 && number < 0 then model else { model | quantity = model.quantity + number } |

In the **Invoice** module, the `update`

function will take care of updating the total price based on the quantity value passed as an input:

1 2 3 |
update : Int -> Model -> Model update quantity model = { model | price = (toFloat quantity) * 1.5 } |

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:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
update msg model = case msg of Increment -> let newDetails = Details.update 1 model.details newInvoice = Details.mapQuantity Invoice.update model.invoice newDetails in { model | details = newDetails , invoice = newInvoice } Decrement -> -- Same with -1 |

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:

1 2 3 4 5 |
-- Details.elm mapQuantity : (Int -> a -> b) -> a -> Model -> b mapQuantity f input model = f model.quantity input |

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:

1 2 3 4 5 6 7 8 9 10 11 12 13 |
let newDetails = Details.update 1 model.details newInvoice = Details.mapQuantity Invoice.update model.invoice newDetails -- ...more logic here... newOtherModule = -- ERROR: the model I'm passing here is the one updated -- by Details. The compiler won't complain, but the result will be -- wrong. OtherModule.doSomethingBasedOnInvoice newDetails in -- ... |

## 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:

1 2 3 4 5 6 |
type alias Model = { price : Float } type alias SuperModel a = { a | invoice : Model } |

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**:

1 2 3 4 5 6 7 8 9 10 11 |
-- Invoice.elm update : Int -> SuperModel a -> SuperModel a update quantity superModel = let model = superModel.invoice newModel = { model | price = (toFloat quantity) * 1.5 } in { superModel | invoice = newModel } |

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
-- Details.elm update : Int -> SuperModel a -> SuperModel a update number superModel = let model = superModel.details in if model.quantity == 0 && number < 0 then superModel else let newModel = { model | quantity = model.quantity + number } in { superModel | details = newModel } |

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:

1 2 3 4 5 6 7 8 9 10 |
update : Msg -> Model -> Model update msg model = case msg of Increment -> Details.update 1 model |> Details.mapQuantity Invoice.update Decrement -> Details.update -1 model |> Details.mapQuantity Invoice.update |

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.

## 1 Comment

Join the discussion and tell us your opinion.

You can (potentially) clean this up further using pattern-matching: https://ellie-app.com/3Mx5qb9tzx7a1/0