livebook/lib/livebook/notebook/explore/elixir_and_livebook.livemd
2022-07-19 21:53:15 +02:00

6.1 KiB

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.

Using packages

Sometimes you need a dependency or two and notebooks are no exception to this. In Livebook, you can use Mix.install/2 to bring dependencies into your notebook! This approach is especially useful when sharing notebooks because everyone will be able to get the same dependencies.

Installing dependencies is a one-off setup operation and for those we use a special setup cell. Copy the code below, then scroll to the very top of the notebook, double-click on "Notebook dependencies and setup", add and run the code.

Mix.install([
  {:kino, "~> 0.6.1"}
])

Note: compiling dependencies may use a reasonable amount of memory. If you are hosting Livebook, make sure you have enough memory allocated to the Livebook instance, otherwise the installation will fail.

Kino

The package we installed is Kino - a library that allows you to control parts of Livebook directly from the Elixir code. Let's use it to print a data table:

data = [
  %{id: 1, name: "Elixir", website: "https://elixir-lang.org"},
  %{id: 2, name: "Erlang", website: "https://www.erlang.org"}
]

Kino.DataTable.new(data)

There is much more to Kino and we have a series of Kino guides in the Explore section to teach you more.

Note that it is a good idea to specify versions of the installed packages, so that the notebook is easily reproducible later on. The install command goes beyond simply installing dependencies, it also caches them, consolidates protocols, and more. Check its documentation to learn more.

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 choose to run inside a Mix project (as you would with iex -S mix), manually attach to an existing distributed node, or run your Elixir notebook embedded within the Livebook source itself.

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!