In my daily grind of programming Elm and trolling about in the community watering holes, I often see Msg constructors named SetX , where X is some field in the corresponding Model . I’ve come to believe that messages designed this way are a code smell.

The fact that so many people (past me included) think to design messages this way is not surprising. When learning The Elm Architecture, we learn that messages drive updates to the model, so it seems reasonable that a message constructor’s name would clearly indicate which part of the model it wants to update.

We can say that a Msg type with its constructors named this way lives at the “Changes to Model” level of abstraction. To get an intuition for why this isn’t the best level to define messages at, let’s consider the simple example of a form with a single text input in which the user can enter their name, along with a button to clear the field and another button to capitalize all the letters of the currently entered name:

The imperative naming scheme of the message makes it easy to write the update function that processes it: just set the corresponding model field with the value passed in. But when coming back to the code after a break, we can’t look in a single place to determine what the user actually wanted to accomplish by firing that message; you have to go search for each instance of it in your view and compare it with the update function to get the full context.

This problem is neatly solved by designing messages on a level of abstraction at which each message is named after the intention of some actor. An actor can be the user, an external network service, or foreign js code. So what are the actor intentions in our example? Reading right from the description, the user can

type to change the name in the text input

clear out the name in the text input

capitalize the name in the text input

Applying this change of perspective to the code gets us something like this:

Notice that in this version, the update function encodes a lot more information. Before, it was a dumb setter function for the name field. But now, it explicitly defines the translation between user intentions on the one hand and updates to the model on the other. This translation was happening implicitly before, spread between view and update by passing a different argument to different instances of SetName .

This is a small, contrived example, but I’m willing to bet you can find a place to profitably apply this trick within your own Elm project, or on any app that uses an elm-like architecture. With messages named after the intention of external actors’ actions, not those actions’ effects on the application state, we get declarative views uncluttered with control logic, and a consolidated, self-documenting interpretation of user intentions which is simpler to evolve along with application requirements.

We might well end up making exactly the same changes to the model in different branches, but this is quite ok! Factor out the common code into a helper and you’re good to go (internet points if you can factor out a helper for update in the example).

Sniffing out and cleaning up this smell is one application of a general principle I’ve found helpful in developing Elm apps: priority #1 is getting the levels of abstraction right. Here I mean abstraction in the “naming things” sense. In Elm, “things” are types, and the the combination of records, union types, and functions gives us a powerful vocabulary with which to describe things and their relationships. What’s more, we can iteratively tune abstractions throughout an entire code base, changing a message name here, adding a new message there, and be sure that the compiler will show us how to propagate that change safely.

Oftentimes, when you get the abstractions just right, the code to compose them into a working application falls right into your lap.