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.
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.
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.
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.
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 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:
| 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 |
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.
| 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.