Elixir and Livebook
Introduction
In this notebook, we will explore some unique features when using Elixir and Livebook together, such as inputs, autocompletion, and more.
If you are not familiar with Elixir, there is a fast paced introduction to the language in the Distributed portals with Elixir notebook. For a more structured introduction to the language, see Elixir’s Getting Started guide and the many learning resources available.
Let’s move forward.
Autocompletion
Elixir code cells also support autocompletion by pressing ctrl + ␣. The runtime must have started for autocompletion to work. A simple way to do so is by executing any code, such as the cell below:
"Hello world"
Now try autocompleting the code below to System.version()
.
First put the cursor after the .
below and
press ctrl + ␣:
System.
You should have seen the editor listing many different options,
which you can use to find version
. Executing the code will
return the Elixir version.
Note you can also press tab to cycle across the completion alternatives.
Getting the current directory
You can access the location of the current .livemd
file and
its current directory using __ENV__
and __DIR__
variables:
IO.puts(__ENV__.file)
IO.puts(__DIR__)
Mix projects
Sometimes you may want to run a notebook within the context of an existing
Mix project. This is possible from Elixir v1.14 with the help of Mix.install/2
.
As an example, imagine you have created a notebook inside your current project,
at notebooks/example.livemd
. In order to run within the root Mix project, using
the same configuration and dependencies versions, you can change your notebook
setup cell to invoke Mix.install/2
with the following arguments:
my_app_root = Path.join(__DIR__, "..")
Mix.install(
[
{:my_app, path: my_app_root, env: :dev}
],
config_path: Path.join(my_app_root, "config/config.exs"),
lockfile: Path.join(my_app_root, "mix.lock")
)
Runtimes
Livebook has a concept of runtime, which in practice is an Elixir node responsible for evaluating your code. You can choose the runtime by clicking the “Runtime” icon () on the sidebar (or by using the s r keyboard shortcut).
By default, a new Elixir node is started (similarly to starting iex
). You can click
reconnect whenever you want to discard the current node and start a new one.
You can also manually attach to an existing distributed node by picking the “Attached Node” runtime. To do so, you will need the Erlang Name of the external node and its Erlang Cookie. For example, you can start a Phoenix application as follows:
$ iex --sname phoenix-app --cookie secret -S mix phx.server
Now open up a new notebook and click the “Runtime” icon on the sidebar. Click to “Configure” the runtime and choose “Attached node”. Input the name and cookie as above and you should be ready to connect to it.
Note, however, that you can’t install new dependencies on a connected runtime. If you want to install dependencies, you have two options:
-
Use the Mix project approach outlined in the previous section;
-
Use a regular notebook and use
Node.connect/1
](https://hexdocs.pm/elixir/Node.html#connect/1) to connect to your application. Use the:erpc
module to fetch data from the remote node and execute code.
More on branches #1
We already mentioned branching sections in Welcome to Livebook, but in Elixir terms each branching section:
- runs in a separate process from the main flow
- copies relevant bindings, imports and aliases from the parent
- updates its process dictionary to mirror the parent
Let’s see this in practice:
parent = self()
Process.put(:info, "deal carefully with process dictionaries")
More on branches #2
parent
self()
Process.get(:info)
Since this branch is a separate process, a crash has limited scope:
Process.exit(self(), :kill)
Evaluation vs compilation
Livebook automatically shows the execution time of each Code cell on the bottom-right of the cell. After evaluation, the total time can be seen by hovering the green dot.
However, it is important to remember that all code outside of a module in Elixir is evaluated, and therefore executes much slower than code defined inside modules, which are compiled.
Let’s see an example. Run the cell below:
Enum.reduce(1..1_000_000, 0, fn x, acc -> x + acc end)
We are adding all of the elements in a range by iterating them
one by one. However, executing it likely takes some reasonable
amount of time, as the invocation of the Enum.reduce/3
as well
as the anonymous function argument are evaluated.
However, what if we move the above to inside a function? Let’s do that:
defmodule Bench do
def sum do
Enum.reduce(1..1_000_000, 0, fn x, acc -> x + acc end)
end
end
Now let’s try running it:
Bench.sum()
The latest cell should execute orders of magnitude faster than
the previous Enum.reduce/3
call. While the call Bench.sum()
itself is evaluated, the one million iterations of Enum.reduce/3
happen inside a module, which is compiled.
If a notebook is performing slower than expected, consider moving the bulk of the execution to inside modules.
Running tests
It is also possible to run tests directly from your notebooks.
The key is to disable ExUnit
‘s autorun feature and then explicitly
run the test suite after all test cases have been defined:
ExUnit.start(autorun: false)
defmodule MyTest do
use ExUnit.Case, async: true
test "it works" do
assert true
end
end
ExUnit.run()
This helps you follow best practices and ensure the code you write behaves as expected!