Subscribe now

Mix and Modules [02.13.2017]

One of the great things about Elixir is its fantastic tooling support out of the box. The gateway to this tooling is mix, which is a bit like make or rake. Let's get started

Project

We'll start off by looking at the help:

mix help

There's a lot to take in there, but here you're essentially seeing a list of all of the tasks that mix knows about. You can find out more about a particular task. Since we're going to create a new project, let's look at the help for mix new:

mix help new
                                    mix new

Creates a new Elixir project. It expects the path of the project as argument.

    mix new PATH [--sup] [--module MODULE] [--app APP] [--umbrella]

A project at the given PATH will be created. The application name and module
name will be retrieved from the path, unless --module or --app is given.

A --sup option can be given to generate an OTP application skeleton including a
supervision tree. Normally an app is generated without a supervisor and without
the app callback.

An --umbrella option can be given to generate an umbrella project.

An --app option can be given in order to name the OTP application for the
project.

A --module option can be given in order to name the modules in the generated
code skeleton.

## Examples

    mix new hello_world

Is equivalent to:

    mix new hello_world --module HelloWorld

To generate an app with a supervision tree and an application callback:

    mix new hello_world --sup

Location: /Users/jadams/.asdf/installs/elixir/1.4.0/lib/mix/ebin

We don't really need any of the options to start our first project:

mix new hello_world

Now we have a new project. We can cd into it and run the tests, though there won't really be any useful ones:

cd hello_world
mix test
Compiling 1 file (.ex)
Generated hello_world app
..

Finished in 0.03 seconds
2 tests, 0 failures

Randomized with seed 797486

OK, now we can look around a little bit at the project structure:

tree
.
├── README.md
├── _build
│   └── test
│       ├── consolidated
│       │   ├── Elixir.Collectable.beam
│       │   ├── Elixir.Enumerable.beam
│       │   ├── Elixir.IEx.Info.beam
│       │   ├── Elixir.Inspect.beam
│       │   ├── Elixir.List.Chars.beam
│       │   └── Elixir.String.Chars.beam
│       └── lib
│           └── hello_world
│               └── ebin
│                   ├── Elixir.HelloWorld.beam
│                   └── hello_world.app
├── config
│   └── config.exs
├── lib
│   └── hello_world.ex
├── mix.exs
└── test
    ├── hello_world_test.exs
    └── test_helper.exs

Here we have an auto-generated README.md.

Next, there's a _build directory where build artifacts live. You can see beam modules in there, which are what your code gets compiled down to in order to run on the Erlang VM. These won't exist until you compile your code, which happened when we ran the tests.

Next, we see a config directory. You'll use this to configure your apps, but we won't look at it too closely yet.

There there's the lib directory. Your modules live here, and you can see that we had one auto-generated by mix.

Then there's mix.exs which is the file that describes our mix project entirely.

Finally, you can see the tests.

Let's look at the lib/hello_world.ex file to check out our HelloWorld module:

vim lib/hello_world.ex
defmodule HelloWorld do
  @moduledoc """
  Documentation for HelloWorld.
  """

  @doc """
  Hello world.

  ## Examples

      iex> HelloWorld.hello
      :world

  """
  def hello do
    :world
  end
end

Here we can see documentation and a doctest. We'll talk about those more next week. For now, you can see how you define modules and functions: defmodule and def.

Before we move on, let's run the REPL with our project loaded into it. Normally, if you just run iex, you won't have these modules loaded. You can load your project by running:

iex -S mix

Now we have a shell with our module loaded. Let's try running the HelloWorld.hello function:

iex(1)> HelloWorld.hello()
:world

Let's create a division function:

defmodule HelloWorld do
  # ...
  def div(a, b) do
    a / b
  end
end

And we can use it:

iex -S mix
iex(1)> HelloWorld.div(1, 2)
0.5

Of course, this suffers from a bit of a problem:

iex(2)> HelloWorld.div(1, 0)
** (ArithmeticError) bad argument in arithmetic expression
    (hello_world) lib/hello_world.ex:20: HelloWorld.div/2

We could make a safe division function if we could handle the division by zero case a bit better. Here we'll start to see something sort of unique that Elixir offers - multiple function heads:

defmodule HelloWorld do
  # ...
  def div(a, 0) do
    :no_dice
  end
  def div(a, b) do
    a / b
  end
end
iex -S mix
iex(1)> HelloWorld.div(1, 0)
:no_dice

What just happened here? Well, we used pattern matching to describe our function in the case that the second argument was strictly zero. You can define multiple function heads to define variants of the same function.

Let's change this a bit so we can pattern match on whether or not the function was successful:

defmodule HelloWorld do
  def div(a, 0) do
    {:error, "attempt at division by zero"}
  end
  def div(a, b) do
    {:ok, a / b}
  end
end

Now we can handle this result in a case statement. Let's do it in a test:

vim test/hello_world_test.exs
defmodule HelloWorldTest do
  use ExUnit.Case
  doctest HelloWorld

  test "division" do
    {:ok, result} = HelloWorld.div(2, 1)
    assert result == 2.0
  end
  test "division by zero" do
    {:error, err} = HelloWorld.div(1, 0)
    assert err == "attempt at division by zero"
  end
end

So that's covered the basics of testing, functions and modules. Let's look at one more trick Elixir has up its sleeves: Pipes.

When you use a pipe, which looks like |>, you're really just moving the first argument to a function out of the function call, to the left of the pipe. We'll open an iex session to check it out:

iex -S mix

This:

iex(1)> HelloWorld.div(1, 2)
{:ok, 0.5}

is the same as this:

iex(2)> 1 |> HelloWorld.div(2)
{:ok, 0.5}

That's piping in essence, though it's not amazing when used for a single case. Let's open up our test and see it in action on a longer pipeline:

vim test/hello_world_test.exs
defmodule HelloWorldTest do
  # ...
  test "pipes and strings" do
    # If we import the `String` module we can use its functions without
    # qualifying them fully - so we get `upcase` instead of the more-verbose
    # `String.upcase`
    import String

    val =
      "josh"
        |> reverse
        |> capitalize
        |> reverse

    assert val == "josH"
  end
end

In longer pipelines, it becomes a lot more obvious where the benefit lies. Try to write the same code without pipes - just with nested function calls - and see if you think it's easier to see what's going on. I think you won't.

Summary

In today's episode we saw how to use mix and we saw how to create and test modules. We also saw a use of a 2-tuple return value, how to define a function with multiple function heads, and how to use pipelines. I hope you enjoyed it. See you soon!