Joy of Elixir

14. Modules and Structs

We've now seen lots of examples of functions throughout this book. We've seen how we can define anonymous functions (in Chapter 5):

iex> hello = fn (place) -> "Hello #{place}!"
hello.("World")
"Hello World!"

And we've seen how we can call functions from already defined modules (in Chapter 8):

iex> String.downcase("HELLO BUT QUIETLY")
"hello but quietly"

But what we haven't seen yet is how to define our own modules, or even why we would want to do that. The why is the simple part: we define functions inside of modules to keep them separate from other functions; modules are a convenient way of grouping functions. Here's a refresher that recycles the description used in Chapter 8:

The functions that Elixir provides are separated into something akin to kitchen drawers or toolboxes, called modules. Whereas in your top kitchen drawer you might have forks, knives, sporks, and spoons (like every sensible person's kitchen does), and in another you might have measuring cups, and in another tea towels, in Elixir the functions to work with the different kinds of data are separated into different modules. This makes finding functions to work with particular kinds of data in Elixir very easy.

We haven't yet built a complex enough system to require us to put functions inside of modules, or to even want to write our own functions. That changes in this chapter! A rumbling occurs behind us, and slightly to our left. It's Izzy vibrating with anticipation. It's a weird and unearthly sound, but we'll roll with it.

This chapter will show you how to write new modules for the purpose of grouping together functions, and we'll also cover a special kind of map called a struct. In fact, we'll write TWO modules. Let's go!

Home, Sweet Home

The system that we're going to start writing in this chapter will be modelling a home automation system for lights. When I say "modelling", I mean we will be writing some Elixir code that will simulate a home automation system. It'll be a fictional house, instead of a real one. But if we were working on a real home automation system in Elixir, it might work something like this one. We'll use this same code in the next two chapters as well.

In our fictional house, there are fictional rooms, each with a set of fictional lights. It looks like this:

Image for a house here.

Red circles here indicate lights that are off, where green ones indicate on.

In this house, we want to be able to go into a room and turn on one particular light, or to turn on all the lights in that room. Maybe when we go into the dining room, we want to turn on all the lights not only so we can see what we're eating, but so we have the best lighting for our Instagram photos too when we cook an especially photogenic meal.

Dining room with all lights on.

But maybe we want to go into another room later that evening -- after our photogenic meal is devoured -- perhaps the lounge room and we want to watch TV. We turn the TV on, but then also want the small lamp on in the room to provide some more light. We don't want to turn on all the lights in that room. We only want to turn on the lamp.

Lounge room with small lamp on.

What we will look at in this chapter is how we could write some Elixir code that will be able to turn on or off a whole room's set of lights, or individual lights.

Let there be light

In our Elixir code, we need to create something that will keep a track of a light. Here's what we know so far:

  • Lights can be on, or off
  • Lights have a room, like "dining room" or "lounge room"
  • Lights can have a name, like "small lamp"

But how can we represent this in Elixir? What kind of data type can we use?

Izzy's unearthly vibration stops, deadening the air. We had become so accustomed to the noise that its loss leaves us with a very mild sense of melancholy. Then she pipes up: "A map might work. On or off, room name and name all look like they could be keys and values. Here, let me try an example."

iex> light = %{
  status: "on",
  room: "Lounge Room",
  name: "Small Map"
}

A map does seem to be quite a good type of data to use here. It allows us to keep a track of the light's status, the room that the light is in, as well as a name specific to the light itself.

If we want to change any one of the light's attributes, we can use pipe merging (from Chapter 10) to do so:

iex> %{light | status: "off"}
%{name: "Small Map", room: "Lounge Room", status: "off"}

This code is a little long to write, so we might want to make a function for it:

iex> turn_off = fn (light) -> %{light | status: "off"} end 

Now we can turn lights off by calling this function:

iex> turn_off.(light)
%{name: "Small Map", room: "Lounge Room", status: "off"}

Similarly, we can write another function to turn the lights on:

iex> turn_on = fn (light) -> %{light | status: "on"} end
#Function<6.127694169/1 in :erl_eval.expr/5>
iex> turn_on.(light)
%{name: "Small Map", room: "Lounge Room", status: "on"}

Finally, we can create one more function that will toggle a light's status, depending on whether the "status" is on or off, using pattern matching (as we originally saw in Chapter 6):

iex> toggle = fn
  %{status: "on"} = light -> %{light | status: "off"}
  %{status: "off"} = light -> %{light | status: "on"}
end

We now have these three functions, turn_off, turn_on and toggle. But they aren't connected together, they're all floating out there. If we added further functions, how would we know if those functions were for lights, or for rooms? It might be hard to tell.

What we can do to group these functions together is to move them into a module.

The Light module

We're now going to move out of iex, and write some code in a new file. This will make it easier to define our very first custom module, the Light module. We'll write this code in a new file called light.exs:

defmodule Light do
  def turn_on(light) do
    %{light | status: "on"}
  end

  def turn_off(light) do
    %{light | status: "off"}
  end

  def toggle(%{status: "on"} = light) do
    %{light | status: "off"}
  end

  def toggle(%{status: "off"} = light) do
    %{light | status: "on"}
  end
end

This code is some of the longest Elixir code that we've written and it looks slightly alien from the Elixir we're used to. Izzy asks: "What is that defmodule thing, and how is it different from def"? Let's walk through it bit by bit.

Simple module functions

The first line here starts our module definition, and the do starts a block of code for the module. Inside the module, we can then define functions using def. When we've defined functions in the past, we would define them like this:

iex> turn_off = fn (light) -> %{light | status: "off"} end
iex> turn_on = fn (light) -> %{light | status: "on"} end

These functions from the past are defined outside of a module, and must be defined like this, with a variable on the left hand side of an equals sign to name the function.

But when we're inside a module, we must define them like this:

def turn_on(light) do
  %{light | status: "on"}
end

def turn_off(light) do
  %{light | status: "off"}
end

The arguments stay within the circle brackets, but move to be next to the name of the function. We then use a do / end block here to define the functions' bodies, rather than -> / end.

These two functions work the same way however we define them. The turn_on function sets the status's value to on, and the turn_off function sets it to off.

Pattern matching with module functions

When we get to toggle, something special happens:

def toggle(%{status: "on"} = light) do
  %{light | status: "off"}
end

def toggle(%{status: "off"} = light) do
  %{light | status: "on"}
end

This looks almost like we're defining the same function twice. And technically, we are. Both functions are taking a single argument. Because these toggle functions are inside the Light module and because they both take one argument (a light), we would refer to both of these functions as Light.toggle/1.

These two functions are equivalent to this other syntax that we have seen:

toggle = fn
  %{status: "on"} = light -> %{light | status: "off"}
  %{status: "off"} = light -> %{light | status: "on"}
end

In both cases, we use pattern matching to match the argument passed to the function. If the argument is a map containing a status key that's an atom, and that key has a string value of "on", it will match the first function clause and not the second:

def toggle(%{status: "on"} = light) do
  %{light | status: "off"}
end

def toggle(%{status: "off"} = light) do
  %{light | status: "on"}
end

But if the value of status was "off" instead, then the first function clause wouldn't match, but the 2nd one would:

def toggle(%{status: "on"} = light) do
  %{light | status: "off"}
end

def toggle(%{status: "off"} = light) do
  %{light | status: "on"}
end

As we can see here, pattern matching comes in handy. Rather than us (as humans) knowing whether or not our light is on or off, and then figuring out which function to use, we can rely on toggle (the computer) to figure it out for us, and to take the right actions on our light.

We've now talked at (extreme) length about the Light module, but we haven't actually used it yet. We've written the code in light.exs, and now it's time to use it.

Using the Light module

It's time to use this Light module we've crafted. We must first navigate in the terminal to the directory where light.exs is. On my computer, it is at ~/code/joy-of-elixir and so I type this to get there:

cd ~/code/joy-of-elixir

Once there, we can start iex and load our module with the c helper from iex:

iex> c "light.exs", "."
[Light]

The c helper's name is short for "compile". Using c like this will load the code from light.exs into our iex session, making the Light module and its functions available to us.

When we run c, it outputs a list of the modules that were loaded by compiling the file, which we see here as [Light], since there's only the Light module in that file.

We're passing a second argument to c here which is a path where our compiled module should go. The dot (.) here means "the current directory", so where we ran the iex command from. When we run this command, Elixir will compile our module to a file called Elixir.Light.beam.

We could also compile this file with c "light.exs" -- only a single argument to the c function call. This will still compile the module, but every time we go back into iex, we will need to re-compile the module to use it. By running it with two arguments -- c "light.exs", ".", we can enter and leave iex and always have our Light module available.

With this file compiled, we can start to use to use the Light module. Let's try the Light.turn_on/1 function:

iex> %{status: "off", name: "Lamp"} |> Light.turn_on
%{status: "on", name: "Lamp"}

"Hey, it works!", exclaims Izzy. She of little faith. This Light.turn_on/1 function has changed the :status key within our map to "on", and has left the :name key unchanged. Let's try turning a light off:

iex> %{status: "on", name: "Lamp"} |> Light.turn_off
%{status: "off", name: "Lamp"}

The light's status is now "off". Excellent! We can even pipe chain these functions together to turn the light on and off:

iex> %{status: "off", name: "Lamp"} |> Light.turn_on |> Light.turn_off
%{status: "off", name: "Lamp"}

We can do this because each of the functions takes a map and changes the value associated with the status key to either "on" or "off". All of these functions not only take a map as an argument, but they also return a map, and so we can chain these functions together infinitely if we wanted. We would, in effect, be flicking the lights on and off really quickly -- something our parents told us not to do as children. Let's try to do this with a pipe chain of Light.toggle/1 functions:

iex> %{status: "off", name: "Lamp"} |> Light.toggle |> Light.toggle |> Light.toggle
%{status: "on", name: "Lamp"}
Short diagram showing input and output for each toggle invocation.

This is so much fun! We wrote our very first Elixir module, put some functions in it and it's working perfectly. We can now turn lights on or off, or toggle them. But what if we only wanted to make it so a light could be toggled? Let's take a brief look at what that might look like.

Public and private functions

We currently have three functions within the Light module: Light.turn_on/1, Light.turn_off/1 and Light.toggle/1. We're now going to change this module so that the Light.toggle/1 function is the only function that can be called from outside this module. We will do this by turning the Light.turn_on/1 and Light.turn_off/1 functions intoprivate functions.

Functions defined in Elixir modules with def are public functions. That means that we can call them from anywhere else within Elixir code. When a function is private, it can only be referred to from within the same module. We might want to do this to simplify the interface to our module. We're doing that now: we're simplifying it by bringing down the number of functions from 3 to 1.

A real-world example of a public / private "function" is the inner workings of a light switch. The public example is the little plastic switch or button we can use to turn the light on or off. The private example happens behind the scenes: flicking the switch one way connects two wires together and completes a circuit. Flicking it the other way breaks the connection.

Basic circuit image of wires connecting for a light here.

When we turn on or off a light, we don't think about wires connecting -- although now you might!

We're now going to turn the Light.turn_on/1 and Light.turn_off/1 functions into private functions. We can do this by moving these functions to the bottom of our Light module, and defining them with defp instead of def:

defmodule Light do
  def toggle(%{status: "on"} = light) do
    %{light | status: "off"}
  end

  def toggle(%{status: "off"} = light) do
    %{light | status: "on"}
  end

  defp turn_on(light) do
    %{light | status: "on"}
  end

  defp turn_off(light) do
    %{light | status: "off"}
  end
end

The order of these function definitions are important. At the top of Elixir modules, we must define the public functions. Then when we're done defining the public functions, we then define the private ones. This helps other people who are reading our code: the public ones are the ones they're probably interested in using, and so it makes sense for them to be defined first.

One funny thing with this code is that we've defined turn_on and turn_off as private functions, but we're not using these at all within the module. If we launch iex now and compile our light.exs file, Elixir will warn us about this:

iex> c "light.exs", "."
warning: function turn_off/1 is unused
  light.exs:14

warning: function turn_on/1 is unused
  light.exs:10

[Light]

Elixir is able to determine that these functions are unused by checking our code. Warnings like these can be incredibly helpful for getting rid of code that we no longer rely on. But in this case, the warnings are happening because we've moved these two functions to be private functions, and we're not using them in either of the Light.toggle/1 clauses. Let's fix this up:

defmodule Light do
  def toggle(%{status: "on"} = light) do
    light |> turn_off
  end

  def toggle(%{status: "off"} = light) do
    light |> turn_on
  end

  defp turn_on(light) do
    %{light | status: "on"}
  end

  defp turn_off(light) do
    %{light | status: "off"}
  end
end

If we re-compile the light.exs file with the c helper, we will no longer see the warnings:

iex> c "light.exs", "."

warning: redefining module Light (current version loaded from ./Elixir.Light.beam)
light.exs:1

[Light]

We do see a different warning though. This is because we're re-defining the module Light by re-compiling this file again. It is nothing to particularly worry about here, and we can safely ignore this. We're re-compiling this module so that the newest code is available within our iex prompt. If we did not re-compile this module, iex would only know about the last version that we compiled.

While we're in iex, let's try using our Light.toggle/1 function again:

iex> %{status: "on"} |> Light.toggle
%{status: "off"}
iex> %{status: "off"} |> Light.toggle
%{status: "on"}

Great, that seems to be all in order. But what happens if we try to use the now-private functions, Light.turn_off/1 or Light.turn_on/1?

iex> %{status: "on"} |> Light.turn_off
** (UndefinedFunctionError) function Light.turn_off/1 is undefined or private
  Light.turn_off(%{status: "on"})
iex> %{status: "off"} |> Light.turn_on
** (UndefinedFunctionError) function Light.turn_on/1 is undefined or private
  Light.turn_on(%{status: "off"})

We know for a fact that these functions are defined, as we defined them ourselves. We know that they're defined as private functions, and that's exactly what Elixir is telling us now. We cannot use these functions in iex, because we are outside of the module. Private functions can only be accessed from within the module itself -- never from the outside. We choose to make functions private to simplify what our module exposes.

Let's look at one final feature of modules before we round out this chapter. That feature is called structs.

The Light struct

So far, we have been using maps to represent our lights and then passing these maps through to the functions from the Light module:

iex> %{status: "off", name: "Lamp"} |> Light.toggle
%{status: "on", name: "Lamp"}

We're now going to take the time to look at a data type that is similar to a map, called a struct. The word "struct" is shorted for "structured data"; it's a map, but a particular type of map, where each map of the same type will share the same structure.

A struct has a set of key-value pairs just like a map, but a struct's keys are limited to only a certain set. Let's look at how we would define a struct now within the Light module. At the top of the module, we can use the defstruct keyword to define a struct:

defmodule Light do
  defstruct [status: "off"]
end

Let's start iex and compile our light.exs file:

iex> c "light.exs"

To use this struct, we use a similar syntax to how we would create a new map:

iex> %Light{}
%Light{status: "off"}

The difference is here that we put the name of the module where the struct is defined between the % and the opening curly bracket. In our defstruct call, we have defined a default value for status: "off". So when we've now built this new struct with %Light{}, it will already have a :status key set to "off".

This is one of the advantages of structs over maps: structs can have default values.

Another advantage is that structs are limited to the keys specified within the defstruct definition. If we attempt to add a :name key when we define a new %Light{} struct, we'll see an error occur:

iex> %Light{name: "Lamp"}
** (KeyError) key :name not found
  expanding struct: Light.__struct__/1
  iex:3: (file)

We're seeing this error because the struct is only supposed to have a :status key. Any additional keys will not be allowed within this struct. This can be helpful in order to keep structs relatively simple.

You might also recognise the Light.__struct__/1 part of this error message as being something that looks like a function. You would be correct! When we use %Light{}, that use the Light.__struct__/1 function underneath, which is automatically defined by Elixir when we use defstruct. We can use this function too:

iex> Light.__struct__(status: "on")
%Light{status: "on"}

But this is much longer than writing:

iex> %Light{status: "on"}
%Light{status: "on"}

And so we should stick with the latter syntax instead.

Structs are beneficial because we can specify default values for certain keys, and we can limit the number of keys that a struct can have, but with a map we cannot. There's also one more place that we can use structs: to pattern match on them for function defintions.

Using structs with functions

We now have some functions within our Light module, and we are using defstruct which will let us define structured maps to represent our lights. We can combine these two together.

But first, a little bit about why. Our code for Light.toggle/1 will break if we use it in weird ways -- like if we pass it a map which does not have a :status key:

iex> %{} |> Light.toggle
** (FunctionClauseError) no function clause matching in Light.toggle/1

The following arguments were given to Light.toggle/1:

    # 1
    %{}

    Attempted function clauses (showing 2 out of 2):

    def toggle(%{status: "on"} = light)
    def toggle(%{status: "off"} = light)

light.exs:4: Light.toggle/1

As we can see from the error output, this error is happening because both of the clauses for Light.toggle/1 do not match:

def toggle(%{status: "on"} = light) do
  %{light | status: "off"}
end

def toggle(%{status: "off"} = light) do
  %{light | status: "on"}
end

Neither clause matches because there is no :status key with a value of either "on" or "off" within the map that we are passing to Light.toggle/1. We can fix this by making our functions match on the Light structs, rather than simple maps. The Light struct always includes a :status key. To make our code do this, we'll change our light.exs code to this:

defmodule Light do
  defstruct [:name, status: "off"]

  def toggle(%Light{status: "on"} = light) do
    light |> turn_off
  end

  def toggle(%Light{status: "off"} = light) do
    light |> turn_on
  end

  defp turn_on(%Light{} = light) do
    %{light | status: "on"}
  end

  defp turn_off(%Light{} = light) do
    %{light | status: "off"}
  end
end

With the module changed, we can re-compile it:

iex> c "light.exs", "."
warning: redefining module Light (current version loaded from Elixir.Light.beam)
  light.exs:1

[Light]
    

Once the module is re-compiled, we can try calling Light.toggle/1 with an empty map again:

iex> Light.toggle(%{})
** (FunctionClauseError) no function clause matching in Light.toggle/1

The following arguments were given to Light.toggle/1:

    # 1
    %{}

Attempted function clauses (showing 2 out of 2):

    def toggle(%Light{status: "on"} = light)
    def toggle(%Light{status: "off"} = light)

light.exs:4: Light.toggle/1

This is a bit better! The error message shows us that we're passing through an empty map, but the function takes a Light struct instead. Let's try passing through an empty Light struct instead:

iex> Light.toggle(%Light{})
%Light{name: nil, status: "on"}

This code works! It works because the Light struct is configured to have a default value of "off" for the :status key within light.exs:

defmodule Light do
  defstruct [:name, status: "off"]

This means that when we write %Light{}, we're actually going to get back something else:

iex> %Light{}
%Light{name: nil, status: "off"}

This is certainly a big advantage of using structs over maps: we can have keys with default values and then we can match on those default values in functions such as Light.toggle/1. This makes mistakes like leaving out the :status key avoidable.

Structs are maps at heart

There's one last thing to cover on the topic of structs. Structs are maps when you get to the bottom of things. Structs can be passed as the argument to any Map function, like Map.get/2 for instance:

iex> %Light{} |> Map.get(:status)

This is because the underlying implementation of structs is based on maps. We can see this in action if we call the Map.keys/1 function on a struct:

iex> %Light{} |> Map.keys
[:__struct__, :name, :status]

The Map.keys/1 function returns not just the two keys that we've defined with defstruct, but a third key called :__struct__. This key contains the module name of the struct. We can see this by asking the struct for its __struct__ key's value:

iex> %Light{}.__struct__
Light

Structs are, simply put, maps with one extra key: a :__struct__ key. We can even define a map ourselves with such a key to prove this:

iex> %{__struct__: Light, status: "off", name: "Lamp"}
%Light{name: "Lamp", status: "off"}

Structs are maps at heart!