Nine Guidelines for Modular Elm Development

Clean Code Lessons I’ve Learned

billperegoy
im-becoming-functional
9 min readApr 25, 2017

--

Why I Like Structure

I came to Elm after years of playing with mostly very opinionated frameworks. I love Ruby on Rails. The framework provides constraints that make it easy for me to maintain my own code. More importantly, the constraints provided by Rails allow me to jump into someone else’s code and pretty quickly see the lay of the land. I can start with the routes file to see what’s exposed to the outside world. I can look at the models to see the underlying data model. This is a great starting point for a large application. In a Rails project, there is no question of what goes where. Every piece of code has a pre-designated home. That removes both cognitive overload and debate on so many trivial issues.

Enter Elm

So is Elm opinionated? In many areas it is. It’s strongly typed and enforces the Redux-like pattern for managing state. These are constraints I heartily endorse as they result in cleaner and easier to debug code. But in other areas, Elm has no opinions. What should your application directory structure be? How should you split the model, update function and view structurally? When does it make sense to use subcomponents? These are all exercises left to the code writer.

As I started to create larger Elm applications, this bothered me. I wanted to think about the function of my code and use best practices to guide me to clean code structure. Also, being new to functional programming, I didn’t even have the object-oriented paradigm to guide me in arranging methods and state. I had a big bag of functions and no one to tell me how to break these bags into smaller bags that made sense.

Start with No Structure

In my earliest Elm programs, I just threw everything in one big file and got things working. That actually works pretty well. I was the sole developer and having everything together actually made for fast development. I could easily find anything using simple editor searches. I’d do that for a while until I got the the point where it just seemed too messy to show anyone. At the point where it became embarrassing to show off, I’d decide to start breaking things down.

Early Attempts at Restructuring

My early attempts at restructuring were haphazard at best. I’d first try to split out the model, view and update functionality into separate modules. I’d then try to further categorize the functions into buckets with related functionality. Along the way, I’d run into lost of problems. I’d find myself with modules that had circular dependencies and code that was even harder to find than when it was in one big file. I made lots of diagrams like this to try to help me out.

In the end, I usually came up with something that seemed reasonable, but in the end, I still had no rules to help me organize the next project any better. I still am a strong believer that the “one big file” approach is a great way to start off. But I really want some rules to help me do the restructuring.

Documenting the Structure

As I moved from the “one big file” to the “clean code” stage of my latest project, I decided to try to codify the rules I came up with as I restructured code. This post is an attempt to turn my findings into a set of rules I will use for future projects. I’m hoping that having a set of rules will allow me to start with a clean structure and avoid that big refactor as I modularize my code. I’ll next walk through the philosophies I’ve developed and the guidelines I use to enforce this philosophy.

Guideline 1 — A Clean Top Level App

The real beauty of an Elm application is the simplicity of the top level application. The whole program can be understood in terms of the four main functions: init, update, view and subscriptions. I like the top-level of my app to simply reference functions stored in other files/modules. So my typical Elm app has a Main.elm file that looks like this.

-- Main.elm
module Main exposing (..)
import Model exposing (..)
import Subscriptions
import Update
import View
main : Program Never Model Msg
main =
Html.program
{ init = Model.init
, view = View.view
, update = Update.update
, subscriptions = Subscriptions.subscriptions
}

And my application has this directory structure.

<my-app>
src
Main.elm
model
Model.elm
subscriptions
Subscriptions.elm
update
Update.elm
view
View.elm

Guideline 2— Expose Module Functions Sparingly

You’ll note in the above example, I chose to not expose module functions in most cases. While I could have used this approach.

import Model exposing (..)
import Subscriptions exposing (..)
import Update exposing (..)
import View exposing (..)

I instead chose to not expose functions. While this requires the more verbose “module dot function syntax”, I find that I get two big wins for this verbosity.

  • Fewer opportunities for name collisions
  • Guidance in locating functions

I like to leave clues for myself and other maintainers as to how my code is structured and this small extra verbosity pays big dividends.

There are a few places where I make exceptions. I tend to expose my model components since these are the big concept items in my application. I’d rather reference User or BlogPost instead of Model.User and Model.BlogPost. I also tend to expose the Html library functions. It makes a lot more sense to use h1 and onClick rather than Html.h1 and Html.Attributes.onClick to retain readable views.

Guideline 3— Message Type Definitions Live with Model

This first thing I like to do as I refactor is to pull the list of message types out into a separate file. To me, the most important piece of any Elm application is this list of message types. Looking at this union type gives you the clearest idea of the function of any Elm program.

I also like to include this union type in the same module as the top-level record defining my model. At one point I tried to separate these into separate modules but it usually resulted in much pain fighting circular module definitions.

I also put the init function here as it is tightly tied to the model record definition.

Guideline 4— Break Model into Subcomponents (possibly)

For small applications, this is probable overkill, but as my programs get bigger, I find that breaking out each major part of my model into a separate module helps to avoid a single large module. This might result in this sort of structure in the model subdirectory.

--
-- model/Model.elm
--
module Model exposing (..)
type Msg
= AddUser User
| SetCurrentUser Int
import User exposing (..)
type alias Model =
{ users : List User
, currentUser : Maybe Int
}
init : ( Model, Cmd Msg )
init =
{ users = []
, currentUser = Nothing
}
--
-- model/User.elm
--
module User exposing (..)
import Post exposing (..)
type alias User =
{ id : Int
, name : String
, posts : List Post
}
--
-- model/Post.elm
--
type alias Post =
{ id : Int
, content : String
}

Guideline 5— JSON Decoders Live near Model Component

I used to always struggle with where to store my JSON decoders and command functions to perform http requests. Given they require access to both the message types and the model, I find they logically fit in the model directory. Rather than clutter the individual model modules, I create a separate module for each type that requires a decoder. Here’s an example.

module UserDecoder exposing (httpRequest, decoder)import Http
import Json.Decode
import Json.Decode.Pipeline
--import Constants
import Model exposing (..)
import User exposing (..)
httpRequest : Cmd Msg
httpRequest =
let
url =
Constants.urlBase ++ "/api/v1/user"
in
Http.send FetchUserData (Http.get url user Decoder)
listDecoder : Json.Decode.Decoder (List User)
listDecoder =
Json.Decode.list decoder
decoder : Json.Decode.Decoder User
decoder =
Json.Decode.Pipeline.decode User
|> Json.Decode.Pipeline.required "id" Json.Decode.int
|> Json.Decode.Pipeline.required "name" Json.Decode.string

This servers to separate things into a logic file structure and also gives you convenient names to use else where such as UserDecoder.httpRequest and UserDecoder.decoder.

Guideline 6— Simple Update Function Top Level

For any application I write, the first thing that gets bloated is the update function. For a significant application, you will have a lot of message types and potentially complex code to process each type. That can result in a scary, hard to read function. In order to make this more readable, I make the top-level update function simple. Each message type invokes a single function call. These function calls live into one or more modules that also reside in the update directory. Here’s an example of the kind of refactoring I do. The initial update function has this form.

-- update/Update.elm
module Update exposing (update)
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
SubmitUserInputField ->
{ model
| myUserName = model.userInputField
, userInputField = ""
, myUserId = userIdFromName model.userInputField
(Array.toList model.users)
} ! []

I factor out the body of the case like this.

-- update/Utils.elm
module Update.Utils exposing (..)
submitUserInputField : Model -> ( Model, Cmd Msg )
submitUserInputField model =
{ model
| myUserName = model.userInputField
, userInputField = ""
, myUserId = userIdFromName model.userInputField
(Array.toList model.users)
}
! []

I’m then left with this much simpler top-level update function.

-- Update.elm
module Update exposing (update)
import Update.Utils
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
SubmitUserInputField ->
Update.Utils.submitUserInputField model

Depending upon the size of the update function, I may create multiple modules divided by functional area. But I usually start simple and break things up as they become too complex.

Guideline 7— Simple Top Level View

When I create my view function, I create a lot of hierarchy so that the top level view function is referencing functions that clearly show the structure of the view. Here’s an example of a typical view function.

-- view/View.elmmodule View exposing (view)view : Model -> Html Msg
view model =
div [ class "container" ]
[ header model
, sidebar model
, mainContent model
]

I like simple functions that clearly describe the major portions of the page. I initially develop each of these functions inside the same module until they get big enough that I feel like I need to break things up further.

Guideline 8 — Break View into Logical Modules

After I work on my view for a while, I usually find that the view module gets too large to easily understand and maintain. At this point I will break out the different parts of my top level view into separate modules. For the example above, I would break it out as follows (with each module going in its own file).

-- view/Header.elmmodule Header exposing (view)
view : Model -> Html Msg
view model =
<view code>
-- view/Sidebar.elmmodule View.Header exposing (view)
view : Model -> Html Msg
view model =
<view code>
-- view/MainContent.elmmodule View.MainContent exposing (view)
view : Model -> Html Msg
view model =
<view code>

With these modules in place, we now have a simple top-level view like this.

-- view/View.elmmodule View exposing (view)import View.Header
import View.Sidebar
import View.MainContent
view : Model -> Html Msg
view model =
div [ class "container" ]
[ View.Header.view model
, View.Sidebar.view model
, View.Sidevbar.view model
]

This gives us a very simple and concise view module with obvious clues as to where the submodules live.

Guideline 9— Use Namespaces to Simplify Function Names

One great side effect of moving to a more modular architecture is that gives us the ability to name or functions more direct simple names and use the module namespaces to handle potential name clashes. In the example above, you’ll note that each module exposes a single function called view and we use the namespace to differentiate it. I use this pattern all the time and it makes my code feel much cleaner and better organized.

Summary

After using Elm pretty steadily for over a year, I’ve been through many iterations and refactors. The guidelines I now follow have evolved quite a bit. Nothing I documented above is set in stone. In fact, I suspect if you ask me again in three months I’ll have different ideas on how to best organize an Elm all. Also, I fully expect you won’t agree with all of these and have other guidelines you deem most important. Feel free to chime in with any comments or suggestions as I’m always looking for new ideas to improve my Elm code.

--

--

billperegoy
im-becoming-functional

Polyglot programmer exploring the possibilities of functional programming.