elm christmas

Modules

←Previous postNext post →

In this article, we will examine Elm’s module system, which is pretty neat.

A 5 min read written by
Ingar Almklov

In Elm all code resides in some module and all modules have their own files. A module can export one or more values, types etc. Understanding how this works will improve the readability and maintainability of our code bases.

The example we will be using throughout this article is a fictive project with this file structure:

$ tree
.
├── elm.json
└── src
    ├── Group
    │   ├── State.elm
    │   └── View.elm
    ├── Main.elm
    ├── Shared
    │   └── View.elm
    └── User
        ├── State.elm
        └── View.elm

3 directories, 6 files

Module names

First of all, a module's name must match its location. This means that if you are importing a module from your code base that's called Shared.View you know that this module is located in a file called View.elm in a folder called Shared. Because of this you never have to use relative paths to access modules.

Let's say we're in User/View.elm and want to use some function from Shared/View.elm. In JavaScript you would have to use relative paths:

import SharedView from '../Shared/View';

In my experience these paths are realistically much longer (for example '../../../../../user/header') and this makes it hard to see at a glance where this module is actually located. Is this the UserHeader from our Shared folder or our Components folder or our Utils folder? What's hiding behind all the dots?!

In Elm instead of giving some name to some module located at some path you just say which module you want:

import Shared.View

-- Or our other example

import Utils.User.Header

Choosing what to export

In Elm, like other languages, we can choose what we want to export from a module and what we want to keep internal.

The first line of a module must always start with module, then the name of the module (which, as we saw, must match its location), then exposing and a comma-separated list of what we want to make accessible to the outside world. Any type or value not listed here will only be accessible inside the module.

For example:

module User.State exposing (Model, UserType(..), update)

This module exposing a type called Model, a custom type called UserType (and all its constructors) and something called update (probably a function).

Choosing what to import

When you want to use something from another module there are two ways of doing it: qualified or unqualified.

Qualified means that you explicitly say which module the value is coming from, whereas unqualified means that you use the value "without a prefix".

Qualified:

Html.div [] [ Html.text "Hello, world!" ]

Unqualified:

div [] [ text "Hello, world!" ]

For the qualified version to work you only need to put

import Html

near the top of your file.

If you want to use some value unqualified, there are two ways:

import Html exposing (div, text) -- only imports `div` and `text`

-- or

import Html exposing (..) -- imports everything from the Html module

Note that if you do

import Html exposing (div)

you can still access for example text like so: Html.text "Hello!".

When to use what

The general rule-of-thumb is to always use qualified access. When you read code someone else wrote (or maybe even your own from a couple of days ago), you'll have a hard time figuring out where the function run is coming from. If they (or you) instead wrote Simulation.run you see at a glance which run function is being used.

The one exception to this rule I use myself is when doing HTML stuff. I usually do something like

import Html exposing (div, p, text)
import Html.Attributes exposing (class)
import Html.Events exposing (onClick)

or even (..) for one or more of these. This is because when reading view functions I find it easier to see what kind of HTML structure I'm trying to end up with.

Aliasing

Another cool trick is to give aliases to modules. Let's go back to our original example with the User folder containing View.elm and State.elm. If View.elm exposes some function view and State.elm exposes some function update, our initial attempt to use it might look like this:

import User.State
import User.View

-- ...

update msg model =
    case msg of
        UserMsg userMsg ->
            let
                updatedUser =
                    User.State.update userMsg model.user
            in
            -- ...

-- ...

view model =
    div
        []
        [ User.View.view model.user
        ]

If we instead imported the modules like this:

import User.State as User
import User.View as User

we can do

User.update userMsg model.user

-- and

User.view model.user

Pretty neat, right? Bear in mind, though, that this will not work well if both of the modules export something that has the same name.

Aliasing external modules

Aliasing works just as well with external modules as with your own.

Another great use for it is when using for example list-extra or random-extra. These modules (and the other *-extra modules from elm-community) are made with aliasing in mind, so they have no exported members that collide with the built-in modules they are "augmenting".

import List.Extra as List
import Random.Extra as Random

This allows you to use for example both List.filter, which is from elm/core, and List.find, which is from elm-community/list-extra.

←Previous postNext post →