Elixir intro
Mix.install([:kino])
Elixir intro
github.com/membraneframework-labs/rtcon-elixir-membrane
Agenda
- Some background about Elixir, Erlang and the BEAM
- Elixir by example
- Some background about Membrane
- Play with Membrane
Elixir, Erlang & the BEAM
- Erlang is a programming language created in ‘80s for use in telecom industry
- BEAM is the virtual machine that Erlang runs on
- Elixir is a modern language, that also runs on BEAM
Erlang back then: handling thousands of parallel phone calls
- High concurrecy
- Distribution
- Fault tolerance
- Observability
- Hot code reloading
Processes - the light threads
Actor model
flowchart LR
wait_for_message --> process_message --> update_state --> wait_for_message
Message passing
graph LR
process1_mailbox --> process1 --> process2_mailbox --> process2 --> process1_mailbox
Supervision trees
graph TD
top_level_supervisor --- worker1
top_level_supervisor --- worker2
top_level_supervisor --- another_supervisor
another_supervisor --- worker3
another_supervisor --- worker4
Scheduling
- Reductions
- Per-process garbage collector
- Erlang as an access to BEAM
Erlang & Elixir today
- Still handling these phone calls, but video calls too
- Handling millions of parallel websockets (Phoenix framework)
- Focus on developer experience
- Broad ecosystem: web, machine learning, IOT, multimedia
- Covers the full stack
Basics
The snippets below contain Elixir code. You can edit and run each snippet by clicking on it and by hitting CTRL
/CMD
+ Enter
.
Here’s a cheat sheet that covers most of the Elixir syntax: devhints.io/elixir, and the Elixir documentation is available at hexdocs.pm/elixir. Feel free to use them during the workshop.
There’s also a good getting started tutorial on the Elixir website: elixir-lang.org/getting-started, it’s a step-by-step introduction to the language.
x = 3
y = 4
# calculate x^2 + 2y + 1
x ** 2 + 2 * y + 1.5
"abc" <> "def"
defmodule User do
def new(full_name, age) do
[name, surname] = String.split(full_name, " ", parts: 2)
%{name: name, surname: surname, is_adult: age >= 18}
end
end
user = User.new("Joe Armstrong", 19)
IO.inspect(user)
IO.puts(user.name)
IO.puts(user.is_adult)
defmodule User2 do
def new(full_name, age) do
case String.split(full_name, " ", parts: 2) do
[name, surname] ->
user = %{name: name, surname: surname, is_adult: age >= 18}
{:ok, user}
_other ->
{:error, :invalid_full_name}
end
end
end
User2.new("Joe", 19)
{:ok, user} = User2.new("Joe Armstrong", 19)
IO.puts(user.name)
defmodule User3 do
defstruct [:name, :surname, :is_adult]
def new(full_name, age) do
case String.split(full_name, " ", parts: 2) do
[name, surname] ->
user = %User3{name: name, surname: surname, is_adult: age >= 18}
{:ok, user}
_other ->
{:error, :invalid_full_name}
end
end
end
{:ok, user} = User3.new("Joe Armstrong", 19)
user
Task 1
Let’s change the is_adult
field in the User3
struct to a role
field, that can be either :child
or :adult
, so that the snippets below run successfully:
{:ok, %User3{name: "Joe", role: :adult}} = User3.new("Joe Armstrong", 19)
{:ok, %User3{name: "Joe", role: :child}} = User3.new("Joe Armstrong", 13)
Collections
The simplest way to operate on collections in Elixir is the Enum
module - see the docs.
Before you run the following snippet, think what it’s going to return 🤔
cnt = 0
fruits = ["orange", "banana", "apple", "melon", "nut", "grape"]
Enum.each(fruits, fn _fruit -> cnt = cnt + 1 end)
cnt
Enum.reduce(fruits, 0, fn _fruit, cnt -> cnt + 1 end)
Enum.count(fruits)
fruits = Enum.map(fruits, fn fruit -> String.capitalize(fruit) end)
fruits = Enum.sort(fruits)
Enum.take(fruits, 2)
fruits = ["orange", "banana", "apple", "melon", "nut", "grape"]
fruits
|> Enum.map(fn fruit -> String.capitalize(fruit) end)
|> Enum.sort()
|> Enum.take(2)
# |> dbg()
Task 2
Let’s change the snippet above to ignore fruits that start with the letter a
. It should return ["Banana", "Grape"]
. The docs for the Enum module will help you 😊
GenServer
Processes - abstraction levels:
graph
subgraph library
subgraph GenServer/Task/Agent
subgraph bare_process
end
end
end
defmodule ShoppingCart do
use GenServer
@impl true
def init(_opts) do
cart_state = %{}
{:ok, cart_state}
end
@impl true
def handle_info({:put, product}, cart_state) do
cart_state =
Map.update(cart_state, product, 1, fn cnt -> cnt + 1 end)
{:noreply, cart_state}
end
@impl true
def handle_call(:get_all_products, _from, cart_state) do
{:reply, cart_state, cart_state}
end
end
{:ok, cart_pid} = GenServer.start_link(ShoppingCart, [])
send(cart_pid, {:put, "milk"})
send(cart_pid, {:put, "bread"})
GenServer.call(cart_pid, :get_all_products)
GenServer.stop(cart_pid)
send(cart_pid, {:put, "beer"})
GenServer.call(cart_pid, :get_all_products)
Task 3
Let’s add a functionality to the ShoppingCart
module to allow checking if a given product is in the cart, so that the snippet below works. The docs for the Map module will be helpful 😊
{:ok, cart_pid} = GenServer.start_link(ShoppingCart, [])
send(cart_pid, {:put, "milk"})
true = GenServer.call(cart_pid, {:is_product_present, "milk"})
Adding proper API
defmodule ShoppingCart2 do
@moduledoc """
A shopping cart.
You can put some products into it and list what's inside.
"""
use GenServer
@type product :: any
@doc """
Starts and links the Shopping Cart
"""
@spec start_link() :: {:ok, pid}
def start_link() do
GenServer.start_link(ShoppingCart2, [])
end
@doc """
Puts a product into the cart
"""
@spec put_product(pid, product) :: :ok
def put_product(shopping_cart, product) do
send(shopping_cart, {:put, product})
:ok
end
@doc """
Returns all products from the cart
"""
@spec get_all_products(pid) :: [product]
def get_all_products(shopping_cart) do
GenServer.call(shopping_cart, :get_all_products)
end
@impl true
def init(_opts) do
cart_state = %{}
{:ok, cart_state}
end
@impl true
def handle_info({:put, product}, cart_state) do
cart_state =
Map.update(cart_state, product, 1, fn cnt -> cnt + 1 end)
{:noreply, cart_state}
end
@impl true
def handle_call(:get_all_products, _from, cart_state) do
{:reply, cart_state, cart_state}
end
end
{:ok, cart_pid} = ShoppingCart2.start_link()
ShoppingCart2.put_product(cart_pid, "milk")
ShoppingCart2.put_product(cart_pid, "bread")
ShoppingCart2.get_all_products(cart_pid)