Powered by AppSignal & Oban Pro
Would you like to see your link here? Contact us

Elixir intro

elixir_intro.livemd

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)