Daniel Perez

CTO@ClaudeTech

tuvistavie

bit.ly/tokyo-ex2

Beta users wanted!
https://eyecatch.io

CI for UI/UX

Today's topic

Concurrency basics

Implementing a stack server


Target audience

  • Getting started with Elixir
  • No Erlang background

Language concurrency builtins

  • spawn
  • send
  • receive
  • monitor
  • link

Spawn a process

pid = spawn fn ->
  IO.puts "Hello from #{inspect(self())}"
end
:timer.sleep(10)
Process.alive?(pid) |> IO.inspect # false

Communicate with a process

pid = spawn fn ->
  receive do
    {:ping, sender} -> send(sender, :pong)
  end
end
:timer.sleep(10)
Process.alive?(pid) # true
send(pid, {:ping, self()})
flush() # :pong
Process.alive?(pid) # false

Building a server (kind of)

defmodule PingServer do
  def start do
    spawn(__MODULE__, :loop, [])
  end

  def loop do
    receive do
      {:ping, sender} ->
        send(sender, :pong)
        loop()
    end
  end
end

We need state!

  • Elixir is immutable
  • For state, use Agents
  • Agent is a wrapper around GenServer
  • We said no GenServer...

Recursive function arguments

def loop(some_int) do
  receive do
    {:add, value} ->
      loop(some_int + value)
  end
end

Our Stack server

defmodule StackServer do
  def loop(state) do
    receive do
      {:push, value} ->
        new_state = [value | state]
        loop(new_state)
      {:pop, sender} ->
        [value | new_state] = state
        send(sender, value)
        loop(new_state)
    end
  end
end

Abstract push

defmodule StackServer do
  def push(pid, value) do
    send(pid, {:push, value})
  end
end

Make pop synchronous

defmodule StackServer do
  def pop(pid) do
    ref = make_ref()
    send(pid, {:pop, self(), ref})
    receive do
      {^ref, value} -> value
    end
  end

  def loop(state) do
    receive do
      {:pop, sender, ref} ->
        [value | new_state] = state
        send(sender, {ref, value})
        loop(new_state)
    end
  end
end

Make it generic

  • A server has a state
  • A server uses a main loop
  • A server can handle sync/async requests

Refactor our loop

defmodule ClumsyGenServer do
  def loop(module, state) do
    receive do
      {:async, message} ->
        {:noreply, new_state} = module.handle_cast(message, state)
        loop(module, new_state)
      {:sync, message, sender, ref} ->
        {:reply, reply, new_state} =
          module.handle_call(message, {sender, ref}, state)
        send(sender, {ref, reply})
        loop(module, new_state)
    end
  end
end

Extract logic of push and pop

defmodule ClumsyGenServer do
  def cast(pid, message) do
    send(pid, {:async, message})
  end

  def call(pid, message) do
    ref = make_ref()
    send(pid, {:sync, message, self(), ref})
    receive do
      {^ref, reply} -> reply
    end
  end
end

Logic to start ClumsyGenServer

defmodule ClumsyGenServer do
  def start(module, state) do
    spawn(__MODULE__, :init, [module, state])
  end

  def init(module, state) do
    {:ok, state} = module.init(state)
    loop(module, state)
  end

  ...
end

Rewrite push and pop

defmodule StackServer do
  def init(state) do
    {:ok, state}
  end
  def pop(pid) do
    ClumsyGenServer.call(pid, :pop)
  end
  def push(pid, value) do
    ClumsyGenServer.cast(pid, {:push, value})
  end
  def handle_cast({:push, value}, state) do
    {:noreply, [value | state]}
  end
  def handle_call(:pop, _from, [head | rest]) do
    {:reply, head, rest}
  end
end

Final words

  • OTP has plenty of useful abstractions
  • It is mostly about abstracting the generic
  • Take a look at what is available
  • Use OTP, don't reinvent the wheel