The Elm Architecture consists of four critical functions: init , update , view , and subscriptions , and two critical types: Message and Model .

When a program grows large, the software engineer must determine a strategy for managing the relationship between update and view on the one hand, and Message and Model on the other. This article will not focus significant attention on init and subscriptions , as there exists less of a relationship between those functions’ size and the application’s size. The update and view functions, on the other hand, scale roughly linearly with the size of the application as it grows.

The Dependency Conundrum

While your program remains small and the entirety of your application can fit inside Main.elm , there will be no dependency conundrum.

The problem arises when you start to split Main up into other modules and sub-modules.

The first thing that is likely to happen is that the view function will grow to be more than screen’s worth of code. At this point, you will start to break it up into multiple functions, but at some point there will be so many of those that you will want to move them into another file to reduce the clutter. It is at this point you will run into a dependency issue.

Main.elm contains the definitions for both Message and Model . In order for the view code in another module to continue referencing those types, Main.elm will need to expose them. But, in order to wire your new view method up to Main 's view , Main will also need to import the module that contains that function.

At this juncture, you will be introduced to one of my favorite features of Elm (this is not a facetious statement). The compiler will reject circular dependencies:

Main.elm is importing View.elm to get access to the view helper functions, but View.elm needs to import Main.elm to get access to Message and Model . View methods need to know various pieces of system state in order to make decisions about how to display portions of the view. This necessitates knowledge of the Model . Also, various low-level pieces of view will need to be capable of generating Message s on various events. This will necessitate knowledge of the Message type.

A ↔ B ⇒ A → C, B → C

The simplest way to resolve a circular dependency issue involving two modules is to do the following:

Isolate the elements that both of the source modules (A & B) need access to. Move the shared elements into a new module (C). Make the source modules (A & B) depend on the new module (C).

In practice, what this means for Main.elm and View.elm is that we require some third module. Since the Model and Message types are the shared elements that both need access to, we could make a Types.elm file that is responsible for holding our common types.

Now, Main.elm and View.elm will both depend on Types.elm , and neither depends on the other. This will get us compiling again.

As it happens, though, this is not the best of all possible worlds. What if it were possible to solve the dependency problem another way? What if View.elm didn’t need access to Model and Message ?

Dependency-Inverted Views

What if, instead of relying on a specific instance of its Message constructor from Message.elm , View.elm instead got its click handlers as pass throughs?

Notice that this requires us to have a generic type signature on View.render , but this is a good thing. This code can now function independently of a specific Message type. It’s thus more extensible and more reusable.

We can apply the same strategy when dealing with the relationship between View.elm and Model . Instead of importing Model , we make View.elm responsible for defining the contract for the data, and Main.elm is responsible for supplying values in the appropriate shape.

Dependency-Inverted Updaters

At some point, if your update function grows large enough, you’ll be forced to consolidate the model mutations into helper methods. At this point you can avoid depending on Model by employing a language construct called extensible records.

You may think that Message is immune in update , being stuck in the case statement the way it is, but when you get upwards of fifteen to twenty options in that case statement, it will become time to partition the Message types off into sub-modules as well. I recommend Richard Feldman’s strategy for modularizing your update function. Once you’ve employed it, you will find that you have to manage knowledge of your Message types in order keep to your dependencies pointed “down” rather than “up.”

This strategy, for all its elegant partitioning, does introduce some dependency challenges. To my mind, the “least bad” option is the one I’ve presented here: expose the sub-message constructors to Main , and do the function composition of the Message s in Main . Although I would prefer to hide access to the constructors, all the solutions that hide those constructors end up generating a ton of superfluous code. For example, you could expose the constructors via named methods, or you could make the Sub-Message modules responsible for generating the record needed for view, and then have Main map over the values with the parent constructor. This lattermost strategy has the additional downside of creating a weird, indirect structural dependency between update and view . If you know of a way to hide the constructors and make the Sub-Message solution work without a ton of extra code or weird dependency inversion violations, hit me up on Elm Slack.

Conclusion