Joy of Elixir

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.