From 9eaa98ce56847b33656dd2fa10b403469ac95e9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Tue, 8 Jun 2021 14:53:41 +0200 Subject: [PATCH] Portal guide (#318) --- assets/css/markdown.css | 2 +- lib/livebook/notebook/explore.ex | 14 +- .../distributed_portals_with_elixir.livemd | 823 ++++++++++++++++++ .../explore/elixir_and_livebook.livemd | 81 +- .../notebook/explore/intro_to_elixir.livemd | 3 - .../notebook/explore/intro_to_livebook.livemd | 18 +- priv/static/images/live-elixir.png | Bin 18053 -> 0 bytes 7 files changed, 898 insertions(+), 43 deletions(-) create mode 100644 lib/livebook/notebook/explore/distributed_portals_with_elixir.livemd delete mode 100644 lib/livebook/notebook/explore/intro_to_elixir.livemd delete mode 100644 priv/static/images/live-elixir.png diff --git a/assets/css/markdown.css b/assets/css/markdown.css index d6ba720cc..5e663faf4 100644 --- a/assets/css/markdown.css +++ b/assets/css/markdown.css @@ -106,7 +106,7 @@ } .markdown code { - @apply py-[0.2rem] px-2 rounded-lg text-sm align-middle font-mono bg-gray-200; + @apply py-[0.1rem] px-2 rounded-lg text-sm align-middle font-mono bg-gray-200; } .markdown pre > code { diff --git a/lib/livebook/notebook/explore.ex b/lib/livebook/notebook/explore.ex index 922e7db9f..ac010a0b3 100644 --- a/lib/livebook/notebook/explore.ex +++ b/lib/livebook/notebook/explore.ex @@ -65,13 +65,14 @@ defmodule Livebook.Notebook.Explore do image_url: "/images/logo.png" ) - defnotebook(:elixir_and_livebook, - description: "Learn how to use some of Elixir and Livebook unique features together.", - image_url: "/images/live-elixir.png" + defnotebook(:distributed_portals_with_elixir, + description: + "A fast-paced introduction to the Elixir language by building distributed data-transfer portals.", + image_url: "/images/portals.png" ) - defnotebook(:intro_to_elixir, - description: "New to Elixir? Learn about the language and its core concepts.", + defnotebook(:elixir_and_livebook, + description: "Learn how to use some of Elixir and Livebook's unique features together.", image_url: "/images/elixir.png" ) @@ -109,8 +110,9 @@ defmodule Livebook.Notebook.Explore do def notebook_infos() do [ @intro_to_livebook, + @distributed_portals_with_elixir, @elixir_and_livebook - # @intro_to_elixir, @intro_to_nx, @intro_to_axon, @intro_to_vega_lite + # @intro_to_nx, @intro_to_axon, @intro_to_vega_lite ] end diff --git a/lib/livebook/notebook/explore/distributed_portals_with_elixir.livemd b/lib/livebook/notebook/explore/distributed_portals_with_elixir.livemd new file mode 100644 index 000000000..e5838398f --- /dev/null +++ b/lib/livebook/notebook/explore/distributed_portals_with_elixir.livemd @@ -0,0 +1,823 @@ +# Distributed portals with Elixir + +## Introduction + +This notebook is a fast-paced introduction to the Elixir +programming language. We will explore both basic and advanced +concepts to implement our own version of [the Portal +game](http://en.wikipedia.org/wiki/Portal_(video_game)) to +transfer data across notebooks using Elixir's distribution +capabalities. + +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). + +### The plan ahead + +The Portal game consists of a series of puzzles that must be +solved by teleporting the player's character and simple objects +from one place to another. + +In order to teleport, the player uses the Portal gun to shoot doors +onto flat planes, like a floor or a wall. Entering one of those doors +teleports you to the other: + +![](/posts/elixir/1/images/portal-drop.jpeg) + +Our version of the Portal game will use Elixir to shoot doors of +different colors and transfer data between them! We will even learn how +we can distribute doors across different machines in our network: + +![](/posts/elixir/1/images/portal-list.jpeg) + +Here is what we will learn: + + * Elixir's basic data structures + * Pattern matching + * Using agents for state + * Using structs for custom data structures + * Extending the language with protocols + * Supervision trees and applications + * Distributed Elixir nodes + +At the end of this notebook, we will make the following code work: + + + +```elixir +# Shoot two doors: one orange, another blue +Portal.shoot(:orange) +Portal.shoot(:blue) + +# Start transferring the list [1, 2, 3, 4] from orange to blue +portal = Portal.transfer(:orange, :blue, [1, 2, 3, 4]) + +# This will output: +# +# #Portal< +# :orange <=> :blue +# [1, 2, 3, 4] <=> [] +# > + +# Now every time we call push_right, data goes to blue +Portal.push_right(portal) + +# This will output: +# +# #Portal< +# :orange <=> :blue +# [1, 2, 3] <=> [4] +# > +``` + +Intrigued? Let's get started! + +## Basic data structures + +Elixir has numbers, strings, and variables. Code comments start with `#`: + +```elixir +# Numbers +IO.inspect(40 + 2) + +# Strings +variable = "hello" <> " world" +IO.inspect(variable) +``` + +Executing the cell above prints the number `42` and the string +`"hello world"`. To do so, we called the function `inspect` in +the `IO` module, using the `IO.inspect(...)` notation. This function +prints the given data structure to your terminal - in this case, +our notebook - and returns the value given to it. + +Elixir also has three special values, `true`, `false`, and `nil`. +Everything in Elixir is considered to be a truthy value, except for +`false` and `nil`: + +```elixir +# && is the logical and operator +IO.inspect(true && true) +IO.inspect(13 && 42) + +# || is the logical or operator +IO.inspect(true || false) +IO.inspect(nil || 42) +``` + +For working with collections of data, Elixir has three data types: + +```elixir +# Lists (typically hold a dynamic amount of items) +IO.inspect([1, 2, "three"]) + +# Tuples (typically hold a fixed amount of items) +IO.inspect({:ok, "value"}) + +# Maps (key-value data structures) +IO.inspect(%{"key" => "value"}) +``` + +In the snippet above, we also used a new data structure represented +as `:ok`. All values starting with a leading `:` in Elixir are called +**atoms**. Atoms are used as identifiers across the language. Common +atoms are `:ok` and `:error`. Which brings us to the next topic: pattern +matching. + +## Pattern matching + +The `=` operator in Elixir is a bit different from the ones we see +in other languages: + +```elixir +x = 1 +x +``` + +So far so good, but what happens if we invert the operands? + +```elixir +1 = x +``` + +It worked! That's because Elixir tries to match the right side +against the left side. Since both are set to `1`, it works. Let's +try something else: + +```elixir +2 = x +``` + +Now the sides did not match, so we got an error. We use pattern +matching in Elixir to match on collection too. For example, we +can use `[head | tail]` to extract the head (the first element) +and tail (the remaining ones) from a list: + +```elixir +[head | tail] = [1, 2, 3] +IO.inspect(head) +IO.inspect(tail) +``` + +Matching an empty list against `[head | tail]` causes a match error: + +```elixir +[head | tail] = [] +``` + +Finally, we can also use the `[head | tail]` expression to add +elements to the head of a list: + +```elixir +list = [1, 2, 3] +[0 | list] +``` + +We can also pattern match on tuples. This is often used to match +on the return types of function calls. For example, take the function +`Date.from_iso8601(string)`, which returns `{:ok, date}` if the +string represents a valid date, in the format `YYYY-MM-DD`, otherwise +it returns `{:error, reason}`: + +```elixir +# A valid date +Date.from_iso8601("2020-02-29") +``` + +```elixir +# An invalid date +Date.from_iso8601("2020-02-30") +``` + +Now, what happens if we want our code to behave differently depending +if the date is valid or not? We can use `case` to pattern match on +the different tuples. This is also a good opportunity to use Livebook's +inputs to pass different values to our code: + + + +```elixir +# Read the date input, which returns something like "2020-02-30\n" +input = IO.gets("Date: ") + +# So we trim the newline from the input value +trimmed = String.trim(input) + +# And then match on the return value +case Date.from_iso8601(trimmed) do + {:ok, date} -> + "We got a valid date: #{inspect(date)}" + + {:error, reason} -> + "Oh no, the date is invalid. Reason: #{inspect(reason)}" +end +``` + +We have been using `IO.inspect` to write values out of the notebook +and here we used `IO.gets` to read data in. The string passed to +`IO.gets` must have the same suffix as the input name. The returned +string is always appended with a newline. Then we remove the trailing new +line and use `case` to pattern match on the different outcomes of the +`Date.from_iso8601` function. We say the `case` above has two clauses, +one matching on `{:ok, date}` and another on `{:error, reason}`. +Try changing the input and re-executing the cell to see how the outcome +changes. + +Finally, we can also pattern match on maps. This is used to extract the +values for the given keys: + +```elixir +map = %{:elixir => :functional, :python => :object_oriented} +%{:elixir => type} = map +type +``` + +If the key does not exist on the map, it raises: + +```elixir +%{:c => type} = map +``` + +With pattern matching out of the way, we are ready to start our Portal +implementation! + +## Modeling portal doors with Agents + +Elixir data structures are immutable. In the examples above, we never +mutated the list. We can break a list apart or add new elements to the +head, but the original list is never modified. + +That said, when we need to keep some sort of state, like the data +transfering through a portal, we must use an abstraction that stores +this state for us. One such abstraction in Elixir is called an agent. +Before we use agents, we need to briefly talk about anonymous functions. + +Anonymous functions are a mechanism to represent pieces of code that we +can pass around and execute later on: + +```elixir +adder = fn a, b -> a + b end +``` + +An anonymous function is delimited by the words `fn` and `end` and an +arrow `->` is used to separate the arguments from the anonymous function +body. We can now call the anonymous function above multiple times by +providing two numbers as inputs: + +```elixir +adder.(1, 2) +``` + +```elixir +adder.(3, 5) +``` + +In Elixir, we also use anonymous functions to initialize, get, and update +the agent state: + +```elixir +{:ok, agent} = Agent.start_link(fn -> [] end) +``` + +In the example above, we created a new agent, passing a function that +returns the initial state of an empty list. The agent returns +`{:ok, #PID<...>}`, where PID stands for a process identifier, which +uniquely identifies the agent. Elixir has many abstractions for concurrency, +such as agents, tasks, generic servers, but at the end of the day they all +boil down to processes. When we say processes in Elixir, we don't mean +Operating System processes, but rather Elixir Processes, which are lightweight +and isolated, allowing us to run hundreds of thousands of them on the same +machine. + +We store the agent's PID in the `agent` variable, which allows us to +send messages to get the agent's state: + +```elixir +Agent.get(agent, fn list -> list end) +``` + +As well as update it before reading again: + +```elixir +Agent.update(agent, fn list -> [0 | list] end) +Agent.get(agent, fn list -> list end) +``` + +We will use agents to implement our portal doors. + +Whenever we need to encapsulate logic in Elixir, we create modules, +which are essentially a collection of functions. We define modules +with `defmodule` and functions with `def`. Our functions will +encapsulate the logic to interact with the agent, using the API we +learned in the cells above: + +```elixir +defmodule Portal.Door do + use Agent + + def start_link(color) when is_atom(color) do + Agent.start_link(fn -> [] end, name: color) + end + + def get(door) do + Agent.get(door, fn list -> list end) + end + + def push(door, value) do + Agent.update(door, fn list -> [value | list] end) + end + + def pop(door) do + Agent.get_and_update(door, fn list -> + case list do + [h | t] -> {{:ok, h}, t} + [] -> {:error, []} + end + end) + end + + def stop(door) do + Agent.stop(door) + end +end +``` + +We declare a module by giving it a name, in this case, `Portal.Door`. +At the top of the module, we say `use Agent`, which brings some +`Agent`-related functionality into the module. + +The first function is `start_link`, which we often refer to as +`start_link/1`, where the number 1 is called the "arity" of the +function and it indicates the number of arguments it receives. +Then we check that the argument is an atom and proceed to call +`Agent.start_link/2`, as we did earlier in this section, except we are +now passing `name: color` as an argument. By giving a name to the +Agent, we can refer to it anywhere by its name, instead of using its +PID. + +The next two functions, `get/1` and `push/2` perform simple operation +to the agent, reading its state and adding a new element respectively. +Let's take a look at them: + +```elixir +Portal.Door.start_link(:pink) +Portal.Door.get(:pink) +``` + +Note how we didn't need to store the PID anywhere and we can use the +atom `:pink` to refer to the door and read its state. If the door +already exists, and we try to start another one with the same name, +it returns an `{:error, reason}` tuple instead of `{:ok, pid}`: + +```elixir +Portal.Door.start_link(:pink) +``` + +Next, let's push some events: + +```elixir +Portal.Door.push(:pink, 1) +Portal.Door.push(:pink, 2) +Portal.Door.get(:pink) +``` + +We pushed some events and they show up in our state. Although, note +they appear in reverse order. That's because we are always adding +new entries to the top of the list. + +The fourth function we defined is called `pop/1`. If there is any +item in the agent, it takes the head of the list and returns it +wrapped in a `{:ok, value}` tuple. However, if the list is empty, +it returns `:error`. + +```elixir +IO.inspect(Portal.Door.pop(:pink)) +IO.inspect(Portal.Door.pop(:pink)) +IO.inspect(Portal.Door.pop(:pink)) + +Portal.Door.get(:pink) +``` + +Finally, the last function, `stop/1`, simply terminates the agent, +effectively closing the door. Let's try it: + +```elixir +Portal.Door.stop(:pink) +``` + +Now, if we try to do anything with it, it will raise: + +```elixir +Portal.Door.get(:pink) +``` + +Note the error message points out why the operation did not work, great! + +## Portal transfers + +Our portal doors are ready so it is time to start working on portal +transfers! In order to store the portal data, we are going to create a +struct named `Portal`. Let's first learn what structs are about. + +Structs define data structures with pre-defined keys. The keys are verified +at compilation time, so if you make a typo in the key name, you get an error +early on. Structs are defined inside modules, by calling the `defstruct` with +a list of atom keys. Let's define a `User` struct with the fields `:name` +and `:age`: + +```elixir +defmodule User do + defstruct [:name, :age] +end +``` + +Now, we can create structs using the `%User{...}` notation: + +```elixir +user = %User{name: "john doe", age: 27} +``` + +We can access struct fields using the `struct.field` syntax: + +```elixir +user.name +``` + +We can pattern match on structs too: + +```elixir +%User{age: age} = user +age +``` + +Finally, let's see what happens if we do a typo in a field: + +```elixir +%User{agee: age} = user +``` + +Now we are ready to define our `Portal` struct. It will have +two fields, `:left` and `:right`, which point respectively to +the portal door on the left and the door on the right. Our goal +is to transfer data from the left door to the right one. The +`Portal` module, where we define our struct, will also have +four other functions: + + * `shoot(color)` - shoots a door of the given color. This is + a wrapper around `Portal.Door.start_link/1` + + * `transfer(left_door, right_door, data)` - starts a transfer + by loading the given `data` to `left_door` and returns a + `Portal` struct + + * `push_right(portal)` - receives a portal and continues the + transfer by pushing data from the left to the right + + * `close(portal)` - closes the portal by explicitly stopping + both doors + +Let's implement them: + +```elixir +defmodule Portal do + defstruct [:left, :right] + + def shoot(color) do + Portal.Door.start_link(color) + end + + def transfer(left_door, right_door, data) do + # First add all data to the portal on the left + for item <- data do + Portal.Door.push(left_door, item) + end + + # Returns a portal struct with the doors + %Portal{left: left_door, right: right_door} + end + + def push_right(portal) do + # See if we can pop data from left. If so, push the + # popped data to the right. Otherwise, do nothing. + case Portal.Door.pop(portal.left) do + :error -> :ok + {:ok, h} -> Portal.Door.push(portal.right, h) + end + + # Let's return the portal itself + portal + end + + def close(portal) do + Portal.Door.stop(portal.left) + Portal.Door.stop(portal.right) + :ok + end +end +``` + +The `Portal` modules defines a struct followed by a `shoot/1` function. +The function is just a wrapper around `Portal.Door.start_link/1`. Then +we define the `transfer/3` function, which loads the given data into the +left door and returns a Portal struct. Finally, `push_right/3` gets data +from the door on the left and puts it on the right door. Let's give it a try: + +```elixir +Portal.shoot(:orange) +Portal.shoot(:blue) +portal = Portal.transfer(:orange, :blue, [1, 2, 3]) +``` + +The above returns the `%Portal{}` struct. We can check the data has been +loaded into the left door: + +```elixir +Portal.Door.get(:orange) +``` + +Note the list is reversed - and we knew that! - as we always add items +on the top. But we will use that to our advantage soon. Let's start pushing +data to the right: + +```elixir +Portal.push_right(portal) +Portal.Door.get(:blue) +``` + +Since the list is reversed, we can see that we pushed the number `3` +to the right, which is exactly what we expected. If you reevaluate the +cell above, you will see data moving to the right, as our portal doors +are stateful. + +Our portal transfer seems to work as expected! Now let's clean up and +close the transfer: + +```elixir +Portal.close(portal) +``` + +We have made some good progress in our implementation, so now let's work +a bit on the presentation. Currently the Portal being printed as a struct: +`%Portal{left: :orange, right: :blue}`. It would be nice if we actually +had a printed representation of the portal transfer, allowing us to see +the portal processes as we push data. + +## Inspecting portals with Protocols + +We already know that Elixir data structures data can be printed by Livebook. +After all, when we type `1 + 2`, we get `3` back. However, can we customize +how our own data structures are printed? + +Yes, we can! Elixir provides protocols, which allows behaviour to be extended +and implemented for any data type, like our `Portal` struct, at any time. + +For example, every time something is printed in Livebook, or in Elixir's +terminal, Elixir uses the `Inspect` protocol. Since protocols can be extended +at any time, by any data type, it means we can implement it for `Portal` too. +We do so by calling `defimpl/2`, passing the protocol name and the data +structure we want to implement the protocol for. Let's do it: + +```elixir +defimpl Inspect, for: Portal do + def inspect(%Portal{left: left, right: right}, _) do + left_door = inspect(left) + right_door = inspect(right) + + left_data = inspect(Enum.reverse(Portal.Door.get(left))) + right_data = inspect(Portal.Door.get(right)) + + max = max(String.length(left_door), String.length(left_data)) + + """ + #Portal< + #{String.pad_leading(left_door, max)} <=> #{right_door} + #{String.pad_leading(left_data, max)} <=> #{right_data} + >\ + """ + end +end +``` + +In the snippet above, we have implemented the `Inspect` protocol for the +`Portal` struct. The protocol expects one function named `inspect` to be +implemented. The function expects two arguments, the first is the `Portal` +struct itself and the second is a set of options, which we don't care about +for now. + +Then we call `inspect` multiple times, to get a text representation of both +`left` and `right` doors, as well as to get a representation of the data +inside the doors. Finally, we return a string containing the portal presentation +properly aligned. + +That's all we need! Let's start a new transfer and see how it goes: + +```elixir +Portal.shoot(:red) +Portal.shoot(:purple) +portal = Portal.transfer(:red, :purple, [1, 2, 3]) +``` + +Sweet! Look how Livebook automatically picked up the new representation. +Now feel free to call `push_right` and see what happens: + +```elixir +Portal.push_right(portal) +``` + +Feel free to reevaluate the cell above a couple times. Once you are done, +run the cell below to clean it all up and close the portal: + +```elixir +Portal.close(portal) +``` + +There is just one topic left... + +## Distributed transfers + +With our portals working, we are ready to give distributed transfers a try. +However, before we start, there is one big disclaimer: + +> The feature we are going to implement will allow us to share data across +> two separate notebooks using the Erlang Distribution. This section is a great +> experiment to understand how things work behind the scenes but **sharing data +> across notebooks as done here is a bad idea in practice**. If this topic +> interests you, we recommend picking up one of the many available learning +> resources available for Elixir, which will provide a more solid ground to +> leverage the Erlang VM and its distributed features. + +### Distribution 101 + +When Livebook executes the code in a notebook, it starts a separate Elixir +runtime to do so. Since Livebook itself is also written in Elixir, it uses +the Erlang Distribution to communicate with this Elixir runtime. We can +get the name of the Elixir node our notebook is running on like this: + +```elixir +node() +``` + +By executing the code above, we can see node names are atoms. By default, +Livebook only connects to nodes running on the same machine, but you can +also configure it to connect to runtimes across machines. + +We can also get a list of all nodes our runtime is connected to by calling +`Node.list()`, let's give it a try: + +```elixir +Node.list() +``` + +If you execute the code above, you get... an empty list!? + +The reason why we get an empty list is because, by default, Erlang Distribution +is a fully mesh. This means all nodes can see all nodes in the network. However, +because we want the notebook runtimes to be isolated from each other, we start +each runtime as a _hidden_ node. We can ask Elixir to give us all hidden nodes +instead: + +```elixir +Node.list(:hidden) +``` + +Much better! We see one node, which is the Livebook server itself. + +Now there is one last piece of the puzzle: in order for nodes to connect to each +other, they need to have the same cookie. We can read the cookie of our current +notebook runtime like this: + +```elixir +Node.get_cookie() +``` + +Now we have everything we need to connect across notebooks. + +### Notebook connections + +In order to connect across notebooks, open up a new empty notebook +in a separate tab, copy and paste the code below to this new notebook, +and execute it: + + + +```elixir +IO.puts node() +IO.puts Node.get_cookie() +``` + +Now paste the result of the other node name and its cookie in the inputs below: + + + + + +And now execute the code cell below, which will read the inputs, configure the +cookie, and connect to the other notebook: + +```elixir +other_node = + IO.gets("Other node: ") + |> String.trim() + |> String.to_atom() + +other_cookie = + IO.gets("Other cookie: ") + |> String.trim() + |> String.to_atom() + +Node.set_cookie(other_node, other_cookie) +Node.connect(other_node) +``` + +If it returns true, it means it connected as expected. The code above uses +`IO.gets/1` to read the input values, as we did earlier in this notebook, +then removes the trailing newline, and converts the strings to atoms. We +did so using the pipe operator `|>`, which gets the result of the previous +function and passes it as first argument to the next function. For example, +the first line is equivalent to `String.to_atom(String.trim(IO.gets("Other node: ")))`, +but it is much more readable as a pipeline. + +Notice we also stored the name of the other node in a `other_node` variable +for convenience. + +Now we are ready to start a distributed transfer. The first step is to go +to the other notebook and shoot a door. You can try calling the following +there: + + + +```elixir +Portal.shoot(:blue) +``` + +However, if you try the above, it will fail! This happens because the +Portal code has been defined in this notebook but it is not available +in the other notebook. If we were working on an actual Elixir project, +this issue wouldn't exist, because we would start multiple nodes on top +of the same codebase with the same modules, but we can't do so here. +To work around this, let's simply copy the cell that define the +`Portal.Door` module into the other notebook and execute it. Now we should +be able to shoot a door in the other node: + + + +```elixir +Portal.Door.start_link(:blue) +``` + +### Cross-node references + +Now that we have spawned a door on the other notebook, we can directly read +its content from this notebook. So far, we have been using an atom, such +as `:blue`, to represent the doors, but we can also use the `{name, node}` +notation to refer to a process in another node. Let's give it a try: + +```elixir +blue = {:blue, other_node} +Portal.Door.get(blue) +``` + +It works! We could successlly read something from the other node. Now, +let's shoot a yellow port on this node and start a transfer between them: + +```elixir +Portal.shoot(:yellow) +yellow = {:yellow, node()} +portal = Portal.transfer(yellow, blue, [1, 2, 3, 4]) +``` + +Our distributed transfer was started. Now let's push right: + +```elixir +Portal.push_right(portal) +``` + +If you go back to the other notebook and run `Portal.Door.get(:blue)`, +you should see it has been updated with entries from this notebook! +The best part of all is that we enabled distributed transfers without +changing a single line of code! + +Our distributed portal transfer works because the doors are just processes +and accessing/pushing the data through doors is done by sending messages +to those processes via the Agent API. We say sending a message in Elixir +is location transparent: we can send messages to any PID regardless if it +is in the same node as the sender or in different nodes of the same network. + +## Wrapping up + +So we have reached the end of this notebook with a fast paced introduction +to Elixir! It was a fun ride and we went from manually starting doors to +distributed portal transfers. + +To learn more about Elixir, we welcome you to explore our [website](http://elixir-lang.org), +[read our Getting Started guide](https://elixir-lang.org/getting-started/introduction.html), +and [many of the available learning resources](https://elixir-lang.org/learning.html). + +You can also learn how to use some of Elixir and Livebook's unique features together +in the [Elixir and Livebook](/explore/notebooks/elixir-and-livebook) notebook. + +Finally, huge thanks to [Augie De Blieck Jr.](http://twitter.com/augiedb) for the +drawings in this tutorial. + +See you around! diff --git a/lib/livebook/notebook/explore/elixir_and_livebook.livemd b/lib/livebook/notebook/explore/elixir_and_livebook.livemd index 16b9ab725..c9900804b 100644 --- a/lib/livebook/notebook/explore/elixir_and_livebook.livemd +++ b/lib/livebook/notebook/explore/elixir_and_livebook.livemd @@ -1,60 +1,85 @@ # Elixir and Livebook -## Modules +## Introduction -You can use code cells to execute any Elixir code: +In this notebook, we will explore some unique features when +using Elixir and Livebook together, such as inputs, autocompletion, +and more. To get started, execute the cell below so we can get +the Elixir runtime up and running: ```elixir -IO.puts("hello world!") +"Hello world" ``` -But you can define modules inside cells too! +If you are not familiar with Elixir, there is a fast paced +introduction to the language in the [Distributed portals with +Elixir](/explore/notebooks/distributed-portals-with-elixir) +notebook. + +Let's move on. + +## Inputs + +Livebook supports inputs and you read the input values directly +from your notebook code, using `IO.gets/1`. Let's see an example +that expects a date in the format `YYYY-MM-DD` and returns if the +data is valid or not: + + ```elixir -defmodule Utils do - @doc """ - Generates a random binary id. - """ - @spec random_id() :: binary() - def random_id() do - :crypto.strong_rand_bytes(20) |> Base.encode32(case: :lower) - end +# Read the date input, which returns something like "2020-02-30\n" +input = IO.gets("Date: ") + +# So we trim the newline from the input value +trimmed = String.trim(input) + +# And then match on the return value +case Date.from_iso8601(trimmed) do + {:ok, date} -> + "We got a valid date: #{inspect(date)}" + + {:error, reason} -> + "Oh no, the date is invalid. Reason: #{inspect(reason)}" end ``` -If you're surprised by the above output, keep in mind that -every Elixir expression evaluates to some value and as so -does module compilation! +The string passed to `IO.gets/1` must have the same suffix as the +input name and the returned string is always appended with a newline. +This is built-in on top of Erlang's IO protocol and built in a way +that your notebooks can be exported to Elixir scripts and still work! -Having the module defined, let's take it for a spin. - -```elixir -Utils.random_id() -``` +Create your own inputs to learn more about the available input types +and options. ## Autocompletion -Elixir code cells also support autocompletion by pressing -Ctrl + Spacebar. You can try it out by making sure the -module in the previous section has been defined and then -put the cursor after the `.` below and press Ctrl + Spacebar: +Elixir code cells also support autocompletion by +pressing Ctrl + Spacebar. Try it out by +autocompleting the code below to `System.version()`. First put +the cursor after the `.` below and +press Ctrl + Spacebar: ```elixir -Utils. +System. ``` -You can also press `Tab` to cycle across the different options. +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. ## Imports -You can import modules as normally to make the imported functions visible +You can import Elixir modules to make the imported functions visible to all subsequent cells. Usually you want to keep `import`, `alias`, and `require` in the first section, as part of the notebook setup. For instance, you can import `IEx.Helpers` and bring all of the amazing conveniences in Elixir's shell to your notebook: - ```elixir import IEx.Helpers ``` diff --git a/lib/livebook/notebook/explore/intro_to_elixir.livemd b/lib/livebook/notebook/explore/intro_to_elixir.livemd deleted file mode 100644 index 0db126e46..000000000 --- a/lib/livebook/notebook/explore/intro_to_elixir.livemd +++ /dev/null @@ -1,3 +0,0 @@ -# Introduction to Elixir - -TODO: content 🐈 diff --git a/lib/livebook/notebook/explore/intro_to_livebook.livemd b/lib/livebook/notebook/explore/intro_to_livebook.livemd index 27d74c38a..9fb94412c 100644 --- a/lib/livebook/notebook/explore/intro_to_livebook.livemd +++ b/lib/livebook/notebook/explore/intro_to_livebook.livemd @@ -93,11 +93,19 @@ the sidebar or by typing `?`. ## Next steps -That's our quick your intro to Livebook! Now you are ready to learn -how Elixir integrates with Livebook in the ["Elixir and Livebook"](/explore/notebooks/elixir-and-livebook) notebook. +That's our quick your intro to Livebook! Where to go next? -Finally, remember Livebook is an open source project, so feel free to look -into [the repository](https://github.com/elixir-nx/livebook) -to contribute, report bugs, suggest features or just skim over the codebase. + * If you are not familiar with Elixir, there is a fast paced + introduction to the language in the [Distributed portals + with Elixir](/explore/notebooks/distributed-portals-with-elixir) + notebook; + + * Learn how Elixir integrates with Livebook in the + [Elixir and Livebook](/explore/notebooks/elixir-and-livebook) notebook; + + * Finally, remember Livebook is an open source project, so feel free to + look into [the repository](https://github.com/elixir-nx/livebook) + to contribute, report bugs, suggest features or just skim over the + codebase. Now go ahead and build something cool! 🚢 diff --git a/priv/static/images/live-elixir.png b/priv/static/images/live-elixir.png deleted file mode 100644 index ff8fa2921b46b09b8fd7a415ceed06f9f8693e60..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 18053 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 t000000000000000000000001ffKQ>#tDiYtNooK9002ovPDHLkV1n!969@nR