livebook/lib/livebook/notebook/learn/elixir_and_livebook.livemd
2023-01-08 12:47:43 +01:00

218 lines
6.1 KiB
Markdown

# 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](/learn/notebooks/distributed-portals-with-elixir)
notebook. For a more structured introduction to the language,
see [Elixir's Getting Started guide](https://elixir-lang.org/getting-started/introduction.html)
and [the many learning resources available](https://elixir-lang.org/learning.html).
Let's move forward.
## Autocompletion
Elixir code cells also support autocompletion by
pressing <kbd>ctrl</kbd> + <kbd></kbd>. 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:
```elixir
"Hello world"
```
Now try autocompleting the code below to `System.version()`.
First put the cursor after the `.` below and
press <kbd>ctrl</kbd>&nbsp;+&nbsp;<kbd></kbd>:
```elixir
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 <kbd>tab</kbd> 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:
```elixir
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, your `Mix.install/2` should look
like this:
<!-- livebook:{"force_markdown":true} -->
```elixir
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
(<i class="ri-livebook-runtime"></i>) on the sidebar (or by using the <kbd>s</kbd> <kbd>r</kbd>
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:
```shell
$ 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:
1. Use the Mix project approach outlined in the previous section;
2. 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](https://www.erlang.org/doc/man/erpc.html)
to fetch data from the remote node and execute code.
## More on branches #1
We already mentioned branching sections in
[Welcome to Livebook](/learn/notebooks/intro-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:
```elixir
parent = self()
```
```elixir
Process.put(:info, "deal carefully with process dictionaries")
```
<!-- livebook:{"branch_parent_index":4} -->
## More on branches #2
```elixir
parent
```
```elixir
self()
```
```elixir
Process.get(:info)
```
Since this branch is a separate process, a crash has limited scope:
```elixir
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:
```elixir
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:
```elixir
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:
```elixir
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:
```elixir
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!