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
.
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:
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.
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.
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.
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.
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
.
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
.
Now, call the isEmpty
function from main
to check if the list referenced by list1
is empty or not.
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.
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.
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.
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
.
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.
Now we can get rid of the prefix from list1
and list2
in Playground.elm
.
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.
What about MyList.isEmpty
inside main
?
Can we get rid of the prefix there too? Sure. Just add isEmpty
to the list of values being exposed in Playground.elm
.
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
.
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.
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.
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
.
It’s tempting to remove the Debug
prefix too like this:
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:
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
.
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.
- 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.
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:
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.
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.
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.
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.
We can reduce the clutter by introducing a shorter alias for PageVisibility
using the as
syntax.
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:
The alias must come before exposing
, otherwise, Elm will throw an error.
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.
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:
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
.
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:
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.
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.