Modules

Elm has modules to help you grow your codebase in a nice way. On the most basic level, modules let you break your code into multiple files.

Defining Modules

Elm modules work best when you define them around a central type. Like how the List module is all about the List type. So say we want to build a module around a Post type for a blogging website. We can create something like this:

module Post exposing (Post, estimatedReadTime, encode, decoder)

import Json.Decode as D
import Json.Encode as E


-- POST

type alias Post =
  { title : String
  , author : String
  , content : String
  }


-- READ TIME

estimatedReadTime : Post -> Float
estimatedReadTime post =
  toFloat (wordCount post) / 220

wordCount : Post -> Int
wordCount post =
  List.length (String.words post.content)


-- JSON

encode : Post -> E.Value
encode post =
  E.object
    [ ("title", E.string post.title)
    , ("author", E.string post.author)
    , ("content", E.string post.content)
    ]

decoder : D.Decoder Post
decoder =
  D.map3 Post
    (D.field "title" D.string)
    (D.field "author" D.string)
    (D.field "content" D.string)

The only new syntax here is that module Post exposing (..) line at the very top. That means the module is known as Post and only certain values are available to outsiders. As written, the wordCount function is only available within the Post module. Hiding functions like this is one of the most important techniques in Elm!

Note: If you forget to add a module declaration, Elm will use this one instead:

module Main exposing (..)

This makes things easier for beginners working in just one file. They should not be confronted with the module system on their first day!

Growing Modules

As your application gets more complex, you will end up adding things to your modules. It is normal for Elm modules to be in the 400 to 1000 line range, as I explain in The Life of a File. But when you have multiple modules, how do you decide where to add new code?

I try to use the following heuristics when code is:

  • Unique — If logic only appears in one place, I break out top-level helper functions as close to the usage as possible. Maybe use a comment header like -- POST PREVIEW to indicate that the following definitions are related to previewing posts.
  • Similar — Say we want to show Post previews on the home page and on the author pages. On the home page, we want to emphasize the interesting content, so we want longer snippets. But on the author page, we want to emphasize the breadth of content, so we want to focus on titles. These cases are similar, not the same, so we go back to the unique heuristic. Just write the logic separately.
  • The Same — At some point we will have a bunch of unique code. That is fine! But perhaps we find that some definitions contain logic that is exactly the same. Break out a helper function for that logic! If all the uses are in one module, no need to do anything more. Maybe put a comment header like -- READ TIME if you really want.

These heuristics are all about making helper functions within a single file. You only want to create a new module when a bunch of these helper functions all center around a specific custom type. For example, you start by creating a Page.Author module, and do not create a Post module until the helper functions start piling up. At that point, creating a new module should make your code feel easier to navigate and understand. If it does not, go back to the version that was clearer. More modules is not more better! Take the path that keeps the code simple and clear.

To summarize, assume similar code is unique by default. (It usually is in user interfaces in the end!) If you see logic that is the same in different definitions, make some helper functions with appropriate comment headers. When you have a bunch of helper functions about a specific type, consider making a new module. If a new module makes your code clearer, great! If not, go back. More files is not inherently simpler or clearer.

Note: One of the most common ways to get tripped up with modules is when something that was once the same becomes similar later on. Very common, especially in user interfaces! Folks will often try to create a Frankenstein function that handles all the different cases. Adding more arguments. Adding more complex arguments. The better path is to accept that you now have two unique situations and copy the code into both places. Customize it exactly how you need. Then see if any of the resulting logic is the same. If so, move it out into helpers. Your long functions should split into multiple smaller functions, not grow longer and more complex!

Using Modules

It is customary in Elm for all of your code to live in the src/ directory. That is the default for elm.json even. So our Post module would need to live in a file named src/Post.elm. From there, we can import a module and use its exposed values. There are four ways to do that:

import Post
-- Post.Post, Post.estimatedReadTime, Post.encode, Post.decoder

import Post as P
-- P.Post, P.estimatedReadTime, P.encode, P.decoder

import Post exposing (Post, estimatedReadTime)
-- Post, estimatedReadTime
-- Post.Post, Post.estimatedReadTime, Post.encode, Post.decoder

import Post as P exposing (Post, estimatedReadTime)
-- Post, estimatedReadTime
-- P.Post, P.estimatedReadTime, P.encode, P.decoder

I recommend using exposing pretty rarely. Ideally on zero or one of your imports. Otherwise, it can start getting hard to figure out where things came from when reading though. “Wait, where is filterPostBy from again? What arguments does it take?” It gets harder and harder to read through code as you add more exposing. I tend to use it for import Html exposing (..) but not on anything else. For everything else, I recommend using the standard import and maybe using as if you have a particularly long module name!

results matching ""

    No results matching ""