Multi-app Umbrella Projects

It is unfortunate that Erlang chose to call self-contained bundles of code apps. In many ways, they are closer to being shared libraries. And as your projects grow, you may find yourself wanting to split your code into multiple libraries, or apps. Fortunately, mix makes this painless.

To illustrate the process, we’ll create a simple Elixir evaluator. Given a set of input lines, it will return the result of evaluating each. This will be one app.

To test it, we’ll need to pass in lists of lines. We’ve already written a trivial ~l sigil that creates lists of lines for us, so we’ll make that sigil code into a separate application.

Elixir calls these multi-app projects umbrella projects.

Create an Umbrella Project

We use mix new to create an umbrella project, passing it the --umbrella option.

 $ ​​mix​​ ​​new​​ ​​--umbrella​​ ​​eval
 * creating README.md
 * creating mix.exs
 * creating apps

Compared to a normal mix project, the umbrella is pretty lightweight—just a mix file and an apps directory.

Create the Subprojects

Subprojects are stored in the apps directory. There’s nothing special about them—they are simply regular projects created using mix new. Let’s create our two projects now:

 $ ​​cd​​ ​​eval/apps
 $ ​​mix​​ ​​new​​ ​​line_sigil
 * creating README.md
  ...​​ ​​and​​ ​​so​​ ​​on
 $ ​​mix​​ ​​new​​ ​​evaluator
 * creating README.md
  ...​​ ​​and​​ ​​so​​ ​​on
 * creating test/evaluator_test.exs

At this point we can try out our umbrella project. Go back to the overall project directory and try mix compile.

 $ cd ..
 $ mix compile
 ==> evaluator
 Compiled lib/evaluator.ex
 Generated evaluator app
 ==> line_sigil
 Compiled lib/line_sigil.ex
 Generated line_sigil app

Now we have an umbrella project containing two regular projects. Because there’s nothing special about the subprojects, you can use all the regular mix commands in them. At the top level, though, you can build all the subprojects as a unit.

Making the Subproject Decision

The fact that subprojects are just regular mix projects means you don’t have to worry about whether to start a new project using an umbrella. Simply start as a simple project. If you later discover the need for an umbrella project, create it and move your existing simple project into the apps directory.

The LineSigil Project

This project is trivial—just copy the LineSigil module from the previous section into apps/line_sigil/lib/line_sigil.ex. Verify it builds by running mix compile—in either the top-level directory or the line_sigil directory.

The Evaluator Project

The evaluator takes a list of strings containing Elixir expressions and evaluates them. It returns a list containing the expressions intermixed with the value of each. For example, given

 a = 3
 b = 4
 a + b

our code will return

 code> a = 3
 value> 3
 code> b = 4
 value> 4
 code> a + b
 value> 7

We’ll use Code.eval_string to execute the Elixir expressions. To have the values of variables pass from one expression to the next, we’ll also need to explicitly maintain the current binding.

Here’s the code:

odds/eval/apps/evaluator/lib/evaluator.ex
 defmodule​ Evaluator ​do
 
 def​ eval(list_of_expressions) ​do
  { result, _final_binding } =
  Enum.reduce(list_of_expressions,
  {_result = [], _binding = binding()},
  &evaluate_with_binding/2)
  Enum.reverse result
 end
 
 defp​ evaluate_with_binding(expression, { result, binding }) ​do
  { next_result, new_binding } = Code.eval_string(expression, binding)
  { [ ​"​​value> ​​#{​next_result​}​​"​, ​"​​code> ​​#{​expression​}​​"​ | result ], new_binding }
 end
 end

Linking the Subprojects

Now we need to test our evaluator. It makes sense to use our ~l sigil to create lists of expressions, so let’s write our tests that way.

odds/eval/apps/evaluator/test/evaluator_test.exs
 defmodule​ EvaluatorTest ​do
 use​ ExUnit.Case
 
 import​ LineSigil
 
  test ​"​​evaluates a basic expression"​ ​do
  input = ​~​l​"""
  1 + 2
  """
 
  output = ​~​l​"""
  code> 1 + 2
  value> 3
  """
 
  run_test input, output
 end
 
  test ​"​​variables are propagated"​ ​do
  input = ​~​l​"""
  a = 123
  a + 1
  """
  output = ​~​l​"""
  code> a = 123
  value> 123
  code> a + 1
  value> 124
  """
 
  run_test input, output
 end
 
 defp​ run_test(lines, output) ​do
  assert output == Evaluator.eval(lines)
 end
 end

But if we simply run this in the apps/evaluator directort, Elixir won’t be able to find the LineSigil module, as we don’t have a dependency. Instead, just run the tests from the top-level directory (the one containing the overall umbrella project). Mix will automatically load all the child apps for you.

 $ ​​mix​​ ​​test
»==> evaluator
 ..
 
 Finished in 0.02 seconds
 2 tests, 0 failures
 
 Randomized with seed 334706
»==> line_sigil
 ..
 
 Finished in 0.04 seconds
 1 doctest, 1 test, 0 failures
 
 Randomized with seed 334706

The first stanza of test output is for the evaluator tests, and the second is for line_sigil.