16. Introduction to Mix
Welcome to the final part, Part 4 of Joy of Elixir. You've done well to venture this far. The focus for this part of the book is "Real World Elixir". We're going to gather all of the things that we've learned so far and build a new Elixir project, just as we would do in the real world.
In this last part consisting of three chapters, we're going to work with an Elixir tool called Mix. Mix allows us to create Elixir projects, allowing us to group together modules into a cohesive unit. It's a similar idea to how we used a module to group together functions in the last chapter.
In the other two sections of this chapter, we'll look at how we can use other people's code. We've done something like this already when we've used functions from within Elixir itself, such as the ones from the String
and Map
modules, but we'll be bringing in extra code in the next chapter -- code that does not exist within standard Elixir.
In the final chapter of this part, we'll look at automated testing. Automated testing is a way to write code that ensures our other code is working correctly.
In order to work with dependencies and learn about testing, we'll first need to learn about the tool that helps with those things. That tool is called Mix. In this chapter, we're going to get started with Mix, walk through the structure of a Mix project and then move our code from the last chapter into this project.
Getting started with Mix
Every Mix project has an origin, and that origin is the mix new
command. We're going to run it now:
mix new people
This little command will create a new directory called people
and puts some files in that directory.
Thankfully, it tells us what those files are so that we don't have to go in and find out for ourselves:
* creating README.md
* creating .formatter.exs
* creating .gitignore
* creating mix.exs
* creating lib
* creating lib/people.ex
* creating test
* creating test/test_helper.exs
* creating test/people_test.exs
Your Mix project was created successfully.
You can use "mix" to compile it, test it, and more:
cd people
mix test
Run "mix help" for more commands.
Out of all of these files, the mix.exs
file is the most important one. This file is used to declare
this directory is a Mix project. Think of it like "I claim this land in the name of..." without the crappy
colonialism that follows those kinds of statements historically.
Let's look at that file now.
defmodule People.MixProject do
use Mix.Project
def project do
[
app: :people,
version: "0.1.0",
elixir: "~> 1.10",
start_permanent: Mix.env() == :prod,
deps: deps()
]
end
# Run "mix help compile.app" to learn about applications.
def application do
[
extra_applications: [:logger]
]
end
# Run "mix help deps" to learn about dependencies.
defp deps do
[
# {:dep_from_hexpm, "~> 0.3.0"},
# {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"}
]
end
end
There's a lot of new things in here, but let's not run away screaming just yet.
This file defines a module called People.MixProject
, and that module is used to define how our Mix
project behaves.
The first function, project
, defines the name for our application (app
), the version
number for the project (version
) and the version for Elixir (elixir
).
The remaining two settings within project
are start_permanent
(which we will ignore for
now) and deps
, which we will not ignore.
The deps
option allows us to include other people's Mix projects into our own project. This
is one of the big "killer features" of Mix projects -- we can bring in other people's code! Just like back in
Chapter 8 when we discovered Elixir itself has functions already included, we can also depend on other people's
code too. We'll see later on how to use this feature of Mix.
This
setting within project
calls the deps
function, which only so far includes comments:
# Run "mix help deps" to learn about dependencies.
defp deps do
[
# {:dep_from_hexpm, "~> 0.3.0"},
# {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"}
]
end
This code hints at two possible sources for dependencies: one is called Hex, and you can find it at Hex.pm. The other source for dependencies is from Git repositories, and shows how you can pull in a particular dependency from GitHub here.
We'll look at how to add another dependency to our project a little later on. For now, let's start by importing the code from the previous chapter.
Bringing in the Person
module
Let's copy over the person.ex
file we created in the last chapter into our new Mix project's
directory. We'll put the contents of that file into lib/person.ex
.
defmodule Person do
defstruct first_name: nil,
last_name: nil,
birthday: nil,
location: "home"
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
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
defp set_location(%Person{} = person, location) do
%{person | location: location}
end
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
We're going to need to make a few changes to this file. The first is that we're going to change the module definition to this:
defmodule People.Person do
We've now added the People.
prefix to this module to clearly indicate that this Person
module comes from the People
Mix project. This will make it unique enough so that if any other
dependency of our Mix application also defined a Person
module, it wouldn't conflict with
ours.
Let's try to run our code and see what happens. When we're in a Mix project, we can start an iex
session with this command:
iex -S mix
The -S
option stands for "script", but I like to think of it as the "S" on Superman's chest, because
this command gives iex
superpowers! It not only will start an iex
session, but also load
the code from our Mix project at the same time.
Unfortunately, we'll see this iex
session crash:
Compiling 1 file (.ex)
== Compilation error in file lib/person.ex ==
** (CompileError) lib/person.ex:8: Person.__struct__/0 is undefined, cannot expand struct Person.
Make sure the struct name is correct.
If the struct name exists and is correct but it still cannot be found,
you likely have cyclic module usage in your code
lib/person.ex:8: (module)
This error is occurring because Elixir cannot find a module called Person
anymore. This is because
we've renamed the module to People.Person
, which, according to Elixir, is a completely different
name!
The line that is causing the error is, as the error message says, line 8 of the lib/person.ex
file.
Let's look at that line now:
def full_name(%Person{} = person) do
"#{person.first_name} #{person.last_name}"
end
We can fix this error by changing the module name here on the first line here too:
def full_name(%People.Person{} = person) do
"#{person.first_name} #{person.last_name}"
end
But hold! There is a shorter way of writing this code too, and a way that will be future-proof if we decide to change the name of our module again. Here's the way:
def full_name(%__MODULE__{} = person) do
"#{person.first_name} #{person.last_name}"
end
The __MODULE__
short-hand is not Mix-specific -- it's available in any and every Elixir module, and
it's a short-hand way of writing the current module's name. Imagine if we had a module called
Universe.SolarSystem.Earth.People.Person
. That's a mouthful! We can use __MODULE__
to
avoid such mouthfuls.
Let's change all of the code in this module to use __MODULE__
now:
defmodule People.Person do
defstruct first_name: nil,
last_name: nil,
birthday: nil,
location: "home"
def full_name(%__MODULE__{} = person) do
"#{person.first_name} #{person.last_name}"
end
def age(%__MODULE__{} = person) do
days = Date.diff(Date.utc_today(), person.birthday)
days / 365.25
end
def toggle_location(%__MODULE__{location: "away"} = person) do
person |> set_location("home")
end
def toggle_location(%__MODULE__{location: "home"} = person) do
person |> set_location("away")
end
defp set_location(%__MODULE__{} = person, location) do
%{person | location: location}
end
defimpl Inspect do
def inspect(
%{
first_name: first_name,
last_name: last_name,
location: location
},
_
) do
"Person[#{first_name} #{last_name}, #{location}]"
end
end
end
These changes should make our code now work correctly. Let's try running iex -S mix
again:
Erlang/OTP 23 [erts-11.1.1] [source] ...
Compiling 1 file (.ex)
Generated people app
Interactive Elixir (v1.10.4) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)>
Excellent! We're now able to start up iex
. When we do this, there's a line to show that one file is
being compiled (that would be lib/person.ex
) and another line that says "Generated people app", where
"app" is short here for "application". What's happening here is that Mix is compiling our application's code in a
process that's similar to the c
helper we saw in the last chapter, but automatically. That's one of
the super benefits of using Mix -- it will automatically compile our code for us!
Let's try using our People.Person
code now in iex
:
iex> izzy = %People.Person{first_name: "Izzy", last_name: "Bell"}
Person[Izzy Bell, home]
Everything will work as it has done in the past, but keep in mind that we need to use the
People.Person
module, not Person
:
iex> izzy |> People.Person.toggle_location
Using aliases for modules
Our People.Person
module name is not long, but it's also not short either. What if there was a way to
use Person
still? If we try to use Person
now, we'll get an error:
izzy |> Person.toggle_location
** (UndefinedFunctionError) function Person.toggle_location/1 is undefined (module Person is not available)
Person.toggle_location(Person[Izzy Bell, home])
This is because the module is now called People.Person
, not Person!
. If we were to
absolutely insist on using Person
, we can do that by calling alias
first:
iex> alias People.Person
This is the way that we can use Person
still:
izzy |> Person.toggle_location
This feature of Elixir comes in handy for when we want to use shorter names across our Elixir code.
You can read more about
Elixir's alias
directive on the elixir-lang.org site.
Making things neat and tidy
Mix isn't just about organising all your code into a specific directory. It also comes with some helpful
utilities, that we call "tasks". You can see a big long list of these if you run mix help
, but be
warned: the list is
very, very long. Talk about intimidating!
You're not ever going to be expected to know about all of these. There will be no pop quiz. However, we will be
using a few of these tasks in our journey. The first one I want to introduce is one called
mix format
.
Long through the ages have wars been waged about the right way to write code. Do we put semi-colons at the end of lines, or not? Do we use tabs for indentation (like heathens) or spaces? When is the right time to use the enter key? So many little battles turn into big internet arguments. And there's nothing nerds like more than arguing about how to write code. Well, except maybe Star Trek vs Star Wars.
The mix format
task puts all these arguments to bed and tucks them in real good. The
mix format
task takes any Elixir code and... formats it. It makes it neat and tidy!
Let's go into lib/person.ex
and make a good ol' mess of things:
defmodule People.Person do
defstruct first_name: nil, last_name: nil, birthday: nil, location: "home"
def full_name(%__MODULE__{} = person) do "#{person.first_name} #{person.last_name}" end
def age(%__MODULE__{} = person) do days = Date.diff(Date.utc_today(), person.birthday); days / 365.25 end
def toggle_location(%__MODULE__{
location: "away"
} = person) do
person |> set_location("home")
end
def toggle_location(%__MODULE__{
location: "home"
} = person) do
person |> set_location("away")
end
defp set_location(%__MODULE__{} = person, location) do
%{person | location: location}
end
defimpl Inspect do
def inspect(%{first_name: first_name,last_name: last_name,location: location}, _) do
"Person[#{first_name} #{last_name}, #{location}]"
end
end
end
Wow, what a mess. Lines are all squished together. Most of the lines have additional spaces at the start of them. There's blank lines where there doesn't need to be, and a lot more.
Let's go over into the terminal and run the format
task now:
$ mix format
This command will format our code for us, turning it into:
defmodule People.Person do
defstruct first_name: nil, last_name: nil, birthday: nil, location: "home"
def full_name(%__MODULE__{} = person) do
"#{person.first_name} #{person.last_name}"
end
def age(%__MODULE__{} = person) do
days = Date.diff(Date.utc_today(), person.birthday)
days / 365.25
end
def toggle_location(
%__MODULE__{
location: "away"
} = person
) do
person |> set_location("home")
end
def toggle_location(
%__MODULE__{
location: "home"
} = person
) do
person |> set_location("away")
end
defp set_location(%__MODULE__{} = person, location) do
%{person | location: location}
end
defimpl Inspect do
def inspect(%{first_name: first_name, last_name: last_name, location: location}, _) do
"Person[#{first_name} #{last_name}, #{location}]"
end
end
end
That's a lot nicer! But it could still be a little neater. For example, the defstruct
line at the top of the file is a bit long. Let's change that line to this:
defstruct first_name: nil,
last_name: nil,
birthday: nil,
location: "home"
That's a little easier to read. A few lines of vertical code is easier to read than a long horizontal line of code. But wait! We changed this mix format ran! Does this mean that if we run mix format
again that it would re-format this code back to a single line? Let's find out:
$ mix format
And if we look at our code again...
defstruct first_name: nil,
last_name: nil,
birthday: nil,
location: "home"
It stayed the same? Yes! While mix format
will re-format your code to be inline with sensible defaults, if you want to format it a little "neater", mix format
doesn't mind and will leave that code as-is.
Whenever we're writing Elixir code, we should use mix format
to format our code to ensure that it is as neat and tidy as it can be. It's worth nothing this too: some editors, such as Visual Studio Code with its Elixir extension, will automatically format your Elixir files whenever you save them.
Now that we've seen how to include our own code into our Mix project (and how to format it!), in the next chapter we'll be looking at how to include other people's code.