Joy of Elixir

14. Modules

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.

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"} |> Light.turn_on
%{status: "on"}

"Hey, it works!", exclaims Izzy. She of little faith. Let's try turning a light off:

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

We can even pipe chain these functions together to turn the light on and off:

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

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 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"} |> Light.toggle |> Light.toggle |> Light.toggle
%{status: "on"}

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.

Public and private functions

move turn on / turn off into private functions, making toggle the only public function

The Light struct

Define a struct within the Light module Structs are limited in the amount of keys they can accept Structs can have default values for keys (status set to "off") Structs can have "enforced" keys Update the Light functions to use the struct Show that the functions do not match on pure maps anymore, explain why Structs are map with a __struct__ key, demonstrate this by defining a map with __struct__ key