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}!" end
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. Let's go!

Functions for the people

Way back in Chapter 4, we had maps that looked like this:

%{name: "Izzy", age: "30ish"}

In this chapter, we're going to be working with maps based off these ones, but they're going to be a little more complicated in shape:

iex> person = %{
  first_name: "Izzy",
  last_name: "Bell",
  birthday: ~D[1987-12-04],
}

With our new map shape, what if we wanted to have a function that joined together the person's first and last name into a single string? Well, we could probably write it like this:

iex> full_name = fn (person) -> "#{person.first_name} #{person.last_name}" end

Then we can call this function with this code:

iex> full_name.(person)
"Izzy Bell"

This is wilfully ignoring the fact that some people have mononyms, as well as the falsehoods programmers believe about names. Ignoring both of those things for now, we have a single function that will let us display a combination of a first name and a last name. That's all well and good to have a function that does that.

But what if we had another function that also operated on these people maps? A function called age:

iex> age = fn (person) ->
...> days =  Date.diff(Date.utc_today, person.birthday)
...> days / 365.25
...> end

This new function uses two new functions from Elixir's built-in Date module: Date.utc_today/0 and Date.diff/2. The Date.utc_today/0 function returns the current date in the UTC time zone. The Date.diff/2 function allows us to figure out the difference (in days) between two dates. So we're using this function to find the number of days between today and the person's birthday. We then take that number of days and divide it by the average number of days in a year: 365.25. This should give us a close-enough approximation of somebody's age.

We can call this function with this code:

iex> age.(person)
31.972621492128678

Note that you'll get a different number to the one shown here because Date.utc_today will return a different day for you. This number was correct at the time of writing, I promise!

Now we have two functions that work on this same map. Wouldn't it be nice to have a place where we could group the functions? Similar to how Date.diff/2 and Date.utc_today/0 functions. Those functions are grouped together inside of a module called Date. Let's look at how we can create our own module for these full_name and age functions.

The Person module

Elixir comes with a bunch of built-in modules: List, Map, Enum and Date are a few that we've seen so far. Creating modules is not just for Elixir itself, but we can create our own modules too. The main reason to create a module is to group together functions, which is exactly what we're going to do in this part of this chapter.

To define modules in Elixir we use defmodule. We could write it out in iex:

iex(1)> defmodule Person do
...> ...
...> end

But we're going to be changing this code a lot and so we should write the code in a file instead. Let's create a new file called person.exs inside a directory called ~/code/joy_of_elixir and we'll define the Person module in this file:

defmodule Person do
end

Our module doesn't do very much at the moment. So let's change that by putting our functions inside it. Functions defined in a module must start with the keyword def:

defmodule Person do
  def full_name(person) do
    "#{person.first_name} #{person.last_name}"
  end

  def age(person) do
    days = Date.diff(Date.utc_today, person.birthday)
    days / 365.25
  end
end

To use this module, we need to make sure that we're in the ~/code/joy_of_elixir directory and then we can run iex. Once we're in iex we can compile the module with:

iex> c "person.exs", "."

The c helper's name is short for "compile". Using c like this will load the code from person.exs into our iex session, making the Person 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 [Person], since there's only the Person 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.Person.beam.

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

iex> person = %{
  first_name: "Izzy",
  last_name: "Bell",
  birthday: ~D[1987-12-04],
}
iex> person |> Person.age
31.975359342915812

Great! That one works. Let's try the full_name function too:

iex> person |> Person.full_name
"Izzy Bell"

Excellent. Both functions work!

One special thing to note here is that if we exit out of iex and re-open it again, our module will still be accessible. We will still be able to run the functions without first having to compile our module:

iex> person = %{
  first_name: "Izzy",
  last_name: "Bell",
  birthday: ~D[1987-12-04],
}
iex> person |> Person.age
31.975359342915812
iex> person |> Person.full_name
"Izzy Bell"

This is because Elixir will load the Elixir.Person.beam file automatically, as it is located in the directory that we're running iex in.

Now that we have got our module running, let's add another two functions to it. These functions will set a person's location to either be "home" or "away". This location value will indicate if the person is at home, or if they're away from home. The functions should go at the bottom of the module definition inside person.exs:

defmodule Person do
  def full_name(person) do
    "#{person.first_name} #{person.last_name}"
  end

  def age(person) do
    days = Date.diff(Date.utc_today, person.birthday)
    days / 365.25
  end

  def home(person) do
    %{person | location: "home"}
  end

  def away(person) do
    %{person | location: "away"}
  end

If we attempt to use these functions right away, they will not work:

iex> person |> Person.away
** (UndefinedFunctionError) function Person.away/1 is undefined or private

This is happening because we have not yet re-compiled the Person module. To do that, we need to use the c helper again:

c "person.exs", "."

With the module now compiled, we will be able to use this function:

iex> person |> Person.away
** (KeyError) key :location not found
person.exs:15: Person.away/1

Well, we thought we could. But this map doesn't have a location key on it, and this means the away function is unable to set that key to a value. We can fix this by providing that key when we define the initial map:

iex> person = %{
  first_name: "Izzy",
  last_name: "Bell",
  birthday: ~D[1987-12-04],
  location: "home",
}
iex> person |> Person.away
%{
  birthday: ~D[1987-12-04],
  first_name: "Izzy",
  last_name: "Bell",
  location: "away"
}

Okay, so what we needed to do here was to provide the location key in the map and then it worked. That's good to see! But how could we prevent this missing key being an issue again? The way to do that is with a struct!

Structs

So far, we have been using maps to represent our people -- well, one person -- and then passing this map through to the functions from the Person module:

iex> person = %{
  first_name: "Izzy",
  last_name: "Bell",
  birthday: ~D[1987-12-04],
  location: "home",
}
iex> person |> Person.away
%{
  birthday: ~D[1987-12-04],
  first_name: "Izzy",
  last_name: "Bell",
  location: "away"
}

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 Person module. At the top of the module, we can use the defstruct keyword to define a struct:

defmodule Person do
  defstruct [
    first_name: nil,
    last_name: nil,
    birthday: nil,
    location: "home"
  ]

  ...
end

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

iex> c "person.exs"

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

person = %Person{}

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 location: "home". So when we've now built this new struct with %Person{}, it will already have a :location key set to "home".

This is one of the advantages of structs over maps: structs can have default values. This will avoid the issue where our away function was failing because there was no location key in our map.

One extra thing that will help here is to match on the struct type in the away and home functions. This will ensure that we always are getting a Person struct before we try to do anything in these functions. To do this, we need to change these functions to this:

def home(%Person{} = person) do
  %{person | location: "home"}
end

def away(%Person{} = person) do
  %{person | location: "away"}
end

This change to these functions will make them always require a Person struct as an argument. They will no longer work with a map. Let's see this in action by re-compiling our module and trying agani:

iex> c "person.exs", "."
iex> person = %{
  first_name: "Izzy",
  last_name: "Bell",
  birthday: ~D[1987-12-04],
  location: "home",
}
iex> person |> Person.away

When we run this code, we'll see this error:

** (FunctionClauseError) no function clause matching in Person.away/1

The following arguments were given to Person.away/1:

    # 1
    %{
      birthday: ~D[1987-12-04],
      first_name: "Izzy",
      last_name: "Bell",
      location: "home"
    }

Attempted function clauses (showing 1 out of 1):

    def away(%Person{} = person)

person.exs:22: Person.away/1

This error is showing us that the function does not match. It shows us that we're passing a plain map to the function as its first argument, but the function is expecting a Person struct instead. So let's pass one of those instead:

person = %Person{
  first_name: "Izzy",
  last_name: "Bell",
  birthday: ~D[1987-12-04],
}
iex> person |> Person.away
%Person{
  birthday: ~D[1987-12-04],
  first_name: "Izzy",
  last_name: "Bell",
  location: "away"
}

That works a lot better! And did you notice that we didn't have to supply a location for our Izzy either? The struct will use the default value if we do not specify it.

By enforcing a Person struct here in this away function, we can be guaranteed that the function will always receive a person argument that is a Person struct, and that means it will always have a location key.

While we're here, we should also make the same changes to the age and full_name functions too, just to make sure that we receive structs for those functions too.

Our module will now look like this:

defmodule Person do
  defstruct [
    first_name: nil,
    last_name: nil,
    birthday: nil,
    location: "home"
  ]

  def full_name(%Person{} = person) do
    "#{person.first_name} #{person.last_name}"
  end

  def age(%Person{} = person) do
    days = Date.diff(Date.utc_today, person.birthday)
    days / 365.25
  end

  def home(%Person{} = person) do
    %{person | location: "home"}
  end

  def away(%Person{} = person) do
    %{person | location: "away"}
  end
end

One final thing to do here is to use pattern matching to pull out the values from the struct that we depend on. Let's change the two functions of full_name and age to this:

def full_name(%Person{first_name: first_name, last_name: last_name} = person) do
  "#{first_name} #{last_name}"
end

def age(%Person{birthday: birthday} = person) do
  days = Date.diff(Date.utc_today, birthday)
  days / 365.25
end

Public and private functions

Before we move on from here, there's one extra concept I would like to share with you. That concept is about public and private functions in modules. Sometimes, we will have functions in modules that we will not want to share with the outside world. Those functions can be kept private so that only other functions inside the module know about it.

Let's say that instead of having two functions called home and a away, we instead wanted to have a function called toggle_location that toggled the person's location between "home" and "away"?

Well, here's how we might write that function:

def toggle_location(%Person{location: "away"} = person) do
  %{person | location: "home"}
end

def toggle_location(%Person{location: "home"} = person) do
  %{person | location: "away"}
end

And now we can compile the module once again, and use this function:

iex> c "person.exs", "."
iex> person = %Person{
  first_name: "Izzy",
  last_name: "Bell",
  birthday: ~D[1987-12-04],
}
iex> person |> Person.toggle_location
%Person{
  birthday: ~D[1987-12-04],
  first_name: "Izzy",
  last_name: "Bell",
  location: "away"
}

This function does exactly what our home and away functions do, and so we can remove those functions.

But this toggle_location function is public -- it's accessible outside of the module still -- and weren't we talking about both public and private functions? You're right! We were. Let's get to that now.

The two function clauses of toggle_location look remarkably similar. They both set a location key to a particular value. This is a clear opportunity for tidying up some of our code, and it's a great opportunity to demonstrate private functions too.

Let's add a new function -- a private function to our module. We add private functions to the bottom of our module, and define them with defp, where the "p" stands for private.

defp set_location(%Person{} = person, location) do
  %{person | location: location}
end

Now back up in toggle_location, we can use this function to set the location:

def toggle_location(%Person{location: "away"} = person) do
  person |> set_location("home")
end

def toggle_location(%Person{location: "home"} = person) do
  person |> set_location("away")
end

This way, the code involved with setting the location can be shared across these toggle_location functions, and any other functions that later on might also set a location. Perhaps there'll come a time where we might want to announce what a particular person's location is each time it changes:

defp set_location(%Person{} = person, location) do
  IO.puts "#{person |> full_name}'s location is now #{location}"
  %{person | location: location}
end

The private function is an ideal place to put that code. It centralises the code in one simple place, and hides internal implementation details about how a location is set.

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> %Person{} |> Map.get(:location)
"home"

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> %Person{} |> Map.keys
[:__struct__, :birthday, :first_name, :last_name, :location]

The Map.keys/1 function returns not just the four keys that we've defined with defstruct, but a fifth 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> %Person{}.__struct__
Person

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__: Person, first_name: "Izzy", last_name: "Bell", birthday: ~D[1987-12-04], location: "home"}
%Person{
  birthday: ~D[1987-12-04],
  first_name: "Izzy",
  last_name: "Bell",
  location: ""
}

Structs are maps at heart!

Structs and Protocols

There's been one other type of struct that we've been using a lot in this chapter, and we haven't even talked about it being a struct! It's this:

~D[1987-12-04]

Izzy exclaims: "That's not a struct! There's no percent sign!". Yup, there's not a single percent sign there. But it's still a struct! How can we tell? We can use Map.keys/1:

iex> ~D[1987-12-04] |> Map.keys
[:__struct__, :calendar, :day, :month, :year]

This function returns a list of keys, and that list contains :__struct__, and that's how we know that dates are structs under the hood.

Izzy is right that structs usually contain a percent sign. When we create a date, we don't use a percent sign to create it. Instead, we use the ~D sigil. Similarly, when a date is displayed (like in some output for our terminal) it is not displayed like this:

%Calendar.Date{calendar: Calendar.ISO, day: 4, month: 12, year: 1987}

Instead, it is shown like this:

~D[1987-12-04]

This is due to some code within Elixir itself. This code sees that a date is about to be output, and instead displays it in this condensed format instead for readability.

This feature of Elixir is called a protocol, and in particular this is the Inspect protocol we're talking about here. When a date is output on the screen, Elixir checks if there is an inspect protocol implemented for dates. There is, and so it gets used instead of the regular struct output.

That description is a little wordy, so let's see some code of our own in action! Let's go into our person.exs file and define an implementation for this Inspect protocol:

defmodule Person do
  # ...
  # functions go here
  # ...

  defimpl Inspect do
    def inspect(%Person{
      first_name: first_name,
      last_name: last_name,
      location: location,
      }, _) do
      "Person[#{first_name} #{last_name}, #{location}]"
    end
  end
end

Then let's re-compile this module back in iex:

iex> c "person.exs", "."
[Inspect.Person, Person]

Note that this has now compiled two modules: Inspect.Person and Person. The Inspect.Person module has been automatically generated and it will be used when a person struct is inspected.

To inspect a person struct, all we need to do is to generate one and get the console to do the actual inspection:

iex> %Person{first_name: "Izzy", last_name: "Bell"}
Person[Izzy Bell, home]

Great! This is now working. This has allowed us to condense the information that is displayed when using a Person struct in the console. This can be useful if you want to limit the amount of data that is shown in the terminal and just something I thought you should know about before we wrap up this chapter on modules and structs!