A software program is like an organism. When it’s first brought into the digital world, it’s tiny. It might contain a few functions for solving a problem whose scope is very narrow. It then starts to grow, one function at a time. Before we know it, it turns into a behemoth capable of tackling problems with monstrous complexity.
It’s often easier to read and change small programs. But as programs grow into thousands of lines of code, they become hard to understand and maintain without a good organizational structure. Some of the features in Elm we explored earlier, such as immutability, pure functions, tests, and a powerful type system help us write robust programs, but they don’t enable us to organize our code in a maintainable way.
Elm provides three more features that are designed specifically for better code organization: modules, packages, and the Elm Architecture. The Elm Architecture will be covered in chapter 5. We will cover the other two in this section.
A value is the most basic concept in Elm. 1
, a
, "Hannibal"
, and [ 1, 2, 3 ]
are all values. An expression allows us to compute a value by grouping together other values, operators, and constants. 3 * x + 5 * y + 10
is an expression. We can even use if
, case
, and let
to combine multiple expressions and execute them only when certain conditions are met.
As we write more expressions, we’ll inevitably want to reuse some of them in multiple places. We can achieve reusability by using functions to encapsulate a bunch of expressions and give names to the collective task those expressions accomplish.
As our program grows, our data structures also tend to become more complex. To be able to easily describe complex data structures, we need to first define relationships between different kinds of values used in our program through the use of types. As the number of functions and type definitions grow, we need to start grouping together the ones that perform similar tasks into modules.
A module is essentially a collection of functions, constants, type definitions, and other values that can be reused in multiple contexts. For example, all functions that perform some kind of operation on a list of values are grouped into a module called List. We might even want to combine multiple modules that solve similar problems into a package. For example, various modules that provide functionalities for writing and manipulating HTML code are grouped together in a package called Html. We can share a package with other programmers by publishing it through the online catalog.
So far, in this book, we’ve created a few modules without understanding how they actually work. In this section, we’ll attempt to understand the syntax for creating new modules and how they get imported into other modules. Along the way, we’ll learn some best practices that will make the code in our modules more maintainable.
Creating a Module
In the Recursive Types section, we created our own data structure called MyList
that looked very much like the built-in type List
.
type MyList a
= Empty
| Node a (MyList a)
Unfortunately, we can’t use any of the functions defined in the List
module with MyList
, because all those functions expect a list of type List
. Let’s rewrite a function from the List
module so that it also works on MyList
. The source code for Elm’s standard library is freely available. We can read it to figure out how functions in the List
module are implemented. Here is the original implementation for List.isEmpty
function:
isEmpty : List a -> Bool
isEmpty xs =
case xs of
[] ->
True
_ ->
False
Note: You can find the source code for all modules included in the elm/core
package here.
Quite simple, right? If the list is empty it returns True
, otherwise it returns False
. Let’s reimplement isEmpty
so that it works with MyList
too. Before we do that though, we need to create a module first. Create a file named MyList.elm
in the beginning-elm/src
directory and add the code below to it.
module MyList exposing (MyList(..), isEmpty, sum)
type MyList a
= Empty
| Node a (MyList a)
sum : MyList Int -> Int
sum myList =
case myList of
Empty ->
0
Node intValue remainingNodes ->
intValue + sum remainingNodes
isEmpty : MyList a -> Bool
isEmpty xs =
case xs of
Empty ->
True
_ ->
False
We created a module called MyList
that contains the definition for the MyList
type and two functions: sum
and isEmpty
. In the Recursive Types section, we had already defined MyList
and sum
in Playground.elm
. Now that we have a separate module that will contain all code related to the MyList
type, it makes sense to move them over to MyList.elm
. Don’t forget to remove the definitions for the MyList
type and sum
function from Playground.elm
.
You already know how MyList
and sum
work. If not, you can refresh your memory by reading the Recursive Types section again. isEmpty
is a re-implementation of the List.isEmpty
function. The only difference between our implementation and Elm’s is that we use Empty
to represent emptiness whereas Elm uses []
.
Note: It’s perfectly OK to use the same name for a module and a type. In fact you will see this pattern over and over again with many of the modules Elm provides such as Array, Html, and Task. However, Elm doesn’t allow us to have two modules or types with the same name.
MyList
, isEmpty
, and sum
aren’t accessible outside the module they’re defined in, unless we expose them. Exposing a function is straightforward. We include its name between parentheses after the keyword exposing
. But exposing a type requires us to append (..)
to the type name. By adding (..)
, we’re asking Elm to expose the Empty
and Node
data constructors as well.
It’s important to note that Elm doesn’t allow us to export only a subset of data constructors from a type. Therefore, the following code is invalid.
module MyList exposing (MyList(Empty), isEmpty, sum)
We tried to expose only Empty
from the MyList
type. Elm doesn’t allow that because data constructors are primarily used for pattern matching and pattern matches need to be exhaustive. Take the sum
function from the Recursive Types section for example.
sum : MyList Int -> Int
sum myList =
case myList of
Empty ->
0
Node intValue remainingNodes ->
intValue + sum remainingNodes
If Elm had allowed us to expose only Empty
, we would have had to use the catch-all-pattern to account for the Node
constructor. That would have prevented us from correctly implementing the sum
function.
sum : MyList Int -> Int
sum myList =
case myList of
Empty ->
0
_ ->
...
Isn’t it great that the designers of Elm have put so much thought into how to guide us toward good code? You’ll encounter many more smart decisions like this as you learn more about Elm.
Importing a Module
To be able to use the new isEmpty
function outside of MyList
module, we need to import MyList
. Let’s do that in Playground.elm
.
module Playground exposing
.
.
import MyList
We can verify that MyList
was imported successfully by loading the Playground
module up in a browser. Run elm reactor
from the beginning-elm
directory in terminal if it isn’t running already, and go to http://localhost:8000/src/Playground.elm
. If you don’t see any errors, that means the module was imported successfully. We’re now ready to use the isEmpty
function in the Playground
module. Add the following code right above main
in Playground.elm
.
list1 : MyList.MyList a
list1 =
MyList.Empty
list2 : MyList.MyList number
list2 =
MyList.Node 9 MyList.Empty
Now, call the isEmpty
function from main
to check if the list referenced by list1
is empty or not.
main : Html.Html msg
main =
MyList.isEmpty list1
|> Debug.toString
|> Html.text
Refresh the page at http://localhost:8000/src/Playground.elm
and you should see True
. Let’s apply isEmpty
to list2
to make sure that the false condition is also working.
main : Html.Html msg
main =
MyList.isEmpty list2
|> Debug.toString
|> Html.text
Refresh the page at http://localhost:8000/src/Playground.elm
and you should see False
. The way we are creating lists now is slightly more verbose compared to how we created them in the Recursive Types section.
-- Before
Node 9 Empty
-- Now
MyList.Node 9 MyList.Empty
The reason why we need to prefix Node
and Empty
with MyList
is because the MyList
type is now being used inside a code file instead of the repl. Before, when we imported the Playground
module into repl, we used (..)
after the exposing
keyword so that everything inside that module is exposed.
> import Playground exposing (..)
This allowed us to drop MyList
prefix and use the data constructors directly. To drop the prefix in Playground.elm
too, we need to explicitly expose the type when we import a module. Add exposing (MyList(..))
to the end of the line that imports MyList
in Playground.elm
.
module Playground exposing
.
.
import MyList exposing (MyList(..))
Notice the syntax for exposing a type — or any other value for that matter — in a module that uses that type is exactly the same as the one used in the module that created the type.
module MyList exposing (MyList(..), isEmpty, sum)
type MyList a
= Empty
| Node a (MyList a)
.
.
Now we can get rid of the prefix from list1
and list2
in Playground.elm
.
list1 : MyList a
list1 =
Empty
list2 : MyList number
list2 =
Node 9 Empty
Refresh the page at http://localhost:8000/src/Playground.elm
and you should still see False
. We were also able to remove the MyList
prefix from type annotations now that we’ve explicitly exposed the type.
-- Before
list1 : MyList.MyList a
-- Now
list1 : MyList a
What about MyList.isEmpty
inside main
?
main : Html.Html msg
main =
MyList.isEmpty list2
|> Debug.toString
|> Html.text
Can we get rid of the prefix there too? Sure. Just add isEmpty
to the list of values being exposed in Playground.elm
.
module Playground exposing
.
.
import MyList exposing (MyList(..), isEmpty)
.
.
main : Html.Html msg
main =
isEmpty list2
|> Debug.toString
|> Html.text
The figure below explains how the module import syntax works in detail.
The only thing that’s not exposed to the Playground
module right now is the sum
function. Let’s expose that too. Add sum
to the list of things being exposed in Playground.elm
.
import MyList exposing (MyList(..), isEmpty, sum)
At this point, we have exposed all values from the MyList
module. There is actually a shorthand (..)
for exposing everything in a module. Let’s use that instead of exposing each value individually.
module Playground exposing
.
.
import MyList exposing (..)
You’ve seen (..)
in various places throughout the book. Now you know what it means. We cannot, however, use the (..)
shorthand to expose everything in a file where the module is defined. Syntactically speaking, the following code is invalid.
module MyList exposing (..)
But if you do type that code and save the file, elm-format
will automatically replace ..
with the names of all values in MyList.elm
. By not allowing us to expose everything through the use of (..)
, Elm is encouraging us to carefully consider which values to expose from a module. We should expose as little as possible so that we don’t create unnecessary dependencies between modules. A code base with fewer dependencies is easier to maintain.
While we are at it, let’s get rid of the Html
prefix too by exposing the text
function and Html
type in Playground.elm
.
module Playground exposing
.
.
import Html exposing (Html, text)
.
.
main : Html msg
main =
isEmpty list2
|> Debug.toString
|> text
It’s tempting to remove the Debug
prefix too like this:
module Playground exposing
.
.
import Debug exposing (toString)
.
.
main : Html msg
main =
isEmpty list2
|> toString
|> text
But we shouldn’t do that. The Debug
module isn’t meant to be used in production. By not removing Debug
as a prefix, we’re making it clear that this code needs to be removed or modified before it gets deployed to production.
Qualified vs Unqualified Import
What happens if we also want to use the isEmpty
function from the List
module inside Playground.elm
? Let’s find out. Import the List
module and add a constant named list3
right above main
. After that apply isEmpty
to list3
in main
like this:
module Playground exposing
.
.
import List exposing (isEmpty)
import MyList exposing (..)
.
.
list3 : List a
list3 =
[]
main : Html msg
main =
isEmpty list3
|> Debug.toString
|> text
Refresh the page at http://localhost:8000/src/Playground.elm
and you’ll see the following error.
Elm is confused. It doesn’t know which isEmpty
to use because we exposed it from both List
and MyList
. This is a drawback of exposing values when importing a module. To resolve this issue, we need to be explicit about which isEmpty
we intend to use. Prefix isEmpty
with List
in main
.
main : Html msg
main =
List.isEmpty list3
|> Debug.toString
|> text
Refresh the page at http://localhost:8000/src/Playground.elm
one more time and the error should go away.
- Qualified Import
- When we import a module without exposing anything inside it, it’s called a qualified import. That means we have to prefix all functions, types, constants, and other values from that module with the module name.
-- Qualified Imports
import Html
import MyList
list2 : MyList.MyList number
list2 =
MyList.Node 9 MyList.Empty
main : Html.Html msg
main =
MyList.isEmpty list2
|> Debug.toString
|> Html.text
- Unqualified Import
- When we explicitly expose values while importing a module it’s called an unqualified import, which means we don’t need to prefix the module name.
-- Unqualified Imports
import Html exposing (Html, text)
import MyList exposing (..)
list2 : MyList number
list2 =
Node 9 Empty
main : Html msg
main =
isEmpty list2
|> Debug.toString
|> text
Writing code in unqualified style is concise, but it’s also dangerous as we saw above with the name clash. The question is, when should we prefer qualified to unqualified? It’s best practice to write code in qualified style by default. Not only it gets rid of errors caused by name clashes, but also provides some level of documentation. When we see code written in unqualified style like this:
import Html exposing (Html, text)
import List exposing (isEmpty)
import MyList exposing (..)
import SomeOtherModule exposing (..)
main : Html msg
main =
isEmpty list2
|> Debug.toString
|> text
It’s not obvious where isEmpty
comes from and what it does. Does it come from List
, MyList
, or SomeOtherModule
? But if we use the qualified style, we know exactly where isEmpty
comes from. We might even be able to deduce what it does based on its origin.
import Html
import List
import MyList
import SomeOtherModule
main : Html.Html msg
main =
List.isEmpty list2
|> Debug.toString
|> Html.text
Now we know that isEmpty
definitely comes from List
and it checks whether a list is empty or not. But there are scenarios where using the unqualified style could make our code more readable. In Building a Simple Web Page section, we wrote the following code.
module HomePage exposing (main)
import Html exposing (..)
import Html.Attributes exposing (..)
view : a -> Html msg
view model =
div [ class "jumbotron" ]
[ h1 [] [ text "Welcome to Dunder Mifflin!" ]
, p []
[ text "Dunder Mifflin Inc. (stock symbol "
, strong [] [ text "DMI" ]
, text <|
"""
) is a micro-cap regional paper and office
supply distributor with an emphasis on servicing
small-business clients.
"""
]
]
main : Html msg
main =
view "dummy model"
As you can see, the Html
and Html.Attributes
modules are imported using the unqualified style. The reason behind that is, these two modules contain non-overlapping functions. Therefore, the chances of function names clashing over each other is minimal. Furthermore, the function names in these modules resemble HTML markup pretty closely which makes reading the code that represents HTML in Elm quite intuitive. But if we were to write the code above in qualified style, it would be hard to read.
module BeginningElm exposing (main)
import Html
import Html.Attributes
view : a -> Html.Html msg
view model =
Html.div [ Html.Attributes.class "jumbotron" ]
[ Html.h1 [] [ Html.text "Welcome to Dunder Mifflin!" ]
, Html.p []
[ Html.text "Dunder Mifflin Inc. (stock symbol "
, Html.strong [] [ Html.text "DMI" ]
, Html.text <|
"""
) is a micro-cap regional paper and office
supply distributor with an emphasis on servicing
small-business clients.
"""
]
]
main : Html.Html msg
main =
view "dummy model"
If you do decide to use the unqualified style, I recommend limiting it to one exposing (..)
per file. If there is a family of modules that often get used together such as Html
and Html.Attributes
where the chances of name clashes are minimal and the function names are self-documenting then it’s safe to use more than one exposing (..)
.
- Module Names with Prefix
- Notice how the
Html.Attributes
module has a prefix calledHtml
in its name? That’s because it resides in a package called Html which contains multiple modules: -
- Html
- Html.Attributes
- Html.Events
- Html.Keyed
- Html.Lazy
-
When we import a module from a package that has multiple modules in it, we have to add the package name as a prefix except when the module name is the same as the package name, for example the
Html
module. In that case, the package name essentially becomes a part of the module name. One exception to this rule is theCore
package. It contains multiple modules, but when we import those modules we don’t have to prefix them withCore
. For example, to import theArray
module all we need to type isimport Array
and notimport Core.Array
.
As Syntax
If a module name is long, writing code in qualified style gets tedious and the final code looks verbose. Let’s say we have a module called PageVisibility
whose name is a bit longer than some of the modules we have seen thus far. Take a look at the following code that uses this module. Don’t worry about what the code actually does, just pay attention to all the places the name PageVisibility
appears.
import PageVisibility
type Msg =
VisibilityChanged PageVisibility.Visibility
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
VisibilityChanged PageVisibility.Visible ->
...
VisibilityChanged PageVisibility.Hidden ->
...
subscriptions : Model -> Sub Msg
subscriptions model =
PageVisibility.visibilityChanges VisibilityChanged
We can reduce the clutter by introducing a shorter alias for PageVisibility
using the as
syntax.
import PageVisibility as PV
type Msg =
VisibilityChanged PV.Visibility
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
VisibilityChanged PV.Visible ->
...
VisibilityChanged PV.Hidden ->
...
subscriptions : Model -> Sub Msg
subscriptions model =
PV.visibilityChanges VisibilityChanged
Looks better, doesn’t it? It also strikes a good balance between conciseness and self-documentation. PV
is much more concise compared to PageVisibility
, but we also know where the visibilityChanges
function comes from. It’s prefixed with PV
which is an alias for PageVisibility
, so it must be from that module.
We can combine the as
syntax with unqualified imports like this:
import PageVisibility as PV exposing (visibilityChanges)
The alias must come before exposing
, otherwise, Elm will throw an error.
-- This will throw an error
import PageVisibility exposing (visibilityChanges) as PV
Making Functions Private
Making functions private is quite easy in Elm. All we have to do is not expose them. Let’s say the isEmpty
function in the MyList
module delegates the task of determining whether a list is empty or not to a different function, and we don’t want the world to know about this little secret. Add a new function definition below isEmpty
in MyList.elm
and delegate the responsibility of checking emptiness to it.
module MyList exposing (MyList(..), isEmpty, sum)
.
.
isEmpty : MyList a -> Bool
isEmpty xs =
isItReallyEmpty xs
isItReallyEmpty : MyList a -> Bool
isItReallyEmpty xs =
case xs of
Empty ->
True
_ ->
False
isItReallyEmpty
is a private function because it’s not added to the list of values that are exposed. Let’s try to access it anyway from Playground.elm
to see what happens. Change the content of the main
function to this:
main : Html msg
main =
MyList.isItReallyEmpty list2
|> Debug.toString
|> Html.text
If you refresh the page at http://localhost:8000/src/Playground.elm
, Elm will point out that it doesn’t recognize what isItReallyEmpty
is.
To get rid of the error, replace isItReallyEmpty
with isEmpty
in the main
function inside Playground.elm
.
main : Html msg
main =
MyList.isEmpty list2
|> Debug.toString
|> Html.text
It’s best practice to keep functions private unless you must expose them. Once they are exposed to the outside world, it can be very risky to change them. If we modify the input parameters or the return value of a function with no backward compatibility, the client code using that function could stop working. But if a function is private, we can refactor it without any fear of breaking some code out there.
If you’re worried about the list of values exposed in a module definition getting too long, don’t be. Most Elm modules expose a lengthy list of values. Here’s how the List
module exposes its values:
module List exposing
( singleton, repeat, range, (::)
, map, indexedMap, foldl, foldr, filter, filterMap
, length, reverse, member, all, any, maximum, minimum, sum, product
, append, concat, concatMap, intersperse, map2, map3, map4, map5
, sort, sortBy, sortWith
, isEmpty, head, tail, take, drop, partition, unzip
)
Unfortunately, as of this writing, elm-format
pushes each exposed item into a new line when you save the file which makes the module declaration hard to read. Hopefully, that’ll change in the future.
module List exposing
( (::)
, all
, any
, append
, concat
.
.
)
Summary
To summarize, Elm makes it very easy to group functions, constants, type definitions, and other values together using modules. The syntax for creating and importing modules is straightforward. We can also share our modules with other programmers first by including them in a package and then publishing them through the online catalog. We’ll see more examples of custom modules in the chapter that covers the Elm Architecture.