mirror of
https://github.com/livebook-dev/livebook.git
synced 2025-10-10 05:25:57 +08:00
218 lines
6.1 KiB
Markdown
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> + <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!
|