[This post is adapted from my book, Practical Elm for Busy Developers.]
As your Elm code base grows, you will want to start taking advantage of modules to split the code into manageable smaller parts.
In Elm, files and modules have a one-to-one correspondence. If you want to break out a piece of code into a separate file, you have to organise it as a module in that file.
The general approach I would suggest is to start your project with a single file (Main.elm
), and keep adding to it until you see that it’s becoming long and inconvenient to navigate, and you also identify some fairly independent piece of code that can be carved off into a module.
For the sake of this post, let’s consider that we’re working on an application that involves parsing some kind of input in different formats such as JSON and XML. At first all of the code is in one file:
src
Main.elm
Perhaps we start writing JSON decoders in Main.elm
as well, but then realise that JSON parsing is a fairly self-contained piece of functionality that can be extracted into a module.
Keeping in mind that we’re going to parse more than one input format, it makes sense to create a directory called “Parsers” and put our new JSON module there. The resulting source tree looks like this:
src
Parsers
Json.elm
Main.elm
In Json.elm
, we have to name the module and also specify which parts of it are exposed to the outside world (we’ll look at how that’s done a bit further down):
module Parsers.Json exposing (..)
In order to make the functions defined in Json.elm
available in Main.elm
, we need to import our newly created module the same way as we’ve been importing third party modules:
import Parsers.Json exposing (..)
Note that the path to the file has to match up with the dot-separated name of the module, so the directory name has to be “Parsers” with a capital “P”. If we change the directory name, we have to update the module name correspondingly, and vice versa.
We’ve used the exposing
keyword in two different contexts: in JSON.elm
when naming the module, and in Main.elm
when importing it.
In Plan.elm
, the exposing
keyword can be used to restrict what’s visible outside the module.
For example, let’s assume we have a few definitions for decoding the input data:
type Plan
= PCte CteNode
| PResult ResultNode
| PSort SortNode
type alias PlanJson =
{ executionTime : Float
, plan : Plan
, planningTime : Float
, triggers : List String
}
decodePlanJson : Decode.Decoder PlanJson
decodePlanJson =
decode PlanJson
|> optional "Execution Time" Decode.float 0
|> required "Plan" decodePlan
|> optional "Planning Time" Decode.float 0
|> optional "Triggers" (Decode.list Decode.string) []
Instead of exposing everything, we can whitelist the identifiers which are exposed by the module. For example, to expose just decodePlanJson
and nothing else, we need to write:
module Parsers.Json exposing (decodePlanJson)
We can also add type aliases and types into the mix:
module Parsers.Json exposing (decodePlanJson, PlanJson, Plan)
This will make both PlanJson
and Plan
available for use in type annotations, and the PlanJson
constructor will be made available as well.
There is a nuance for types like Plan
as opposed to type aliases - while we’ve exported the Plan
constructor, we haven’t exposed any of the tags like PCte
and PResult
. So we will be able to use Plan
as a type of record fields, and we will be able to use it in type signatures, but we will not be able to construct values of this type. This makes Plan
a so called opaque type.
If we want to be able to construct values of this type, then we need to change our whitelist:
module Parsers.Json exposing (decodePlanJson, PlanJson, Plan(..))
Note the (..)
after Plan
- it signifies that we want to expose all of the stuff included in that type definition.
When importing the module, there are a few variations as well.
The simplest import statement looks like this:
import Parsers.Json
This is called a qualified import, and it makes all the identifiers exposed by the module available in the current file, but they have to be prefixed with the module name: Parsers.Json.decodePlanJson
, Parsers.Json.PCte
.
This is a bit unwieldy, so we can also alias the name of the module with something easier to type:
import Parsers.Json as P
Now we can write P.decodePlanJson
and P.PCte
.
We can dispense with the module name altogether by using an open import:
import Parsers.Json exposing (..)
Importing a lot of identifiers can cause name clashes with other modules, however, so instead we can use an open import with a list of specific identifiers that we need:
import Parsers.Json exposing (decodePlanJson, PlanJson)
This import statement means that we can use decodePlanJson
and PlanJson
but not PCte
or anything else.
Elm provides the module functionality to allow us to organise the code and hide away the implementation details. Each module is written in its own file, and module naming corresponds to the file and directory structure. Elm gives you the ability to restrict what is exposed by the module. When you import a module, you have several options to deal with namespacing, and the ability to restrict what you import.