Skip to content

7ML7W Elixir Day 3 Spawning and Respawning

Andy McVitty edited this page Jan 20, 2020 · 2 revisions

The Meeting

We opened the meeting with much bread and dip, as has become tradition, @adzz agrees to drive, @Tom reads us our rights, and we're off!

The Chapter - Spawning and Respawning

The chapter opens with an interview with Jose Valim (creator of elixir) that we largely skip over. One notable quote is this exchange:

Us: What’s your favorite feature?

Valim: That’s a very hard question... But if we are considering only what Elixir brings to the game, I would definitely pick protocols.

And protocols are never mentioned again*.

*(If you do want to know more, introduction, documentation, further reading)

Gen Servers

The book jumps right in to gen_server without much explanation of what they are, so we decide to start the meeting by diving in to the basic units of concurrency in Elixir, and a look at how they relate to gen_servers.

Dr. GenServer, or: How I Learned To Stop Worrying and Love the Process.

Elixir uses the actor model as its model for concurrency. This is nice because having actors that receive messages is not a million miles away from having objects that receive messages (like we may be used to in OOP).

The 'actors' in Elixir are processes. Processes are not OS processes, they are very lightweight and production apps may have thousands of processes running.

We create a process like this:

spawn(fn -> "Hello" end)

The spawn function takes a 0 arity function, the body of which is what the process will do before it dies. spawn returns a PID, or a process identifier. These are super important because identify the process so that you can send messages to them.

The work inside of spawn is non blocking, meaning you can carry on executing while it works:

pid = spawn(fn -> :timer.sleep(900); IO.inspect("done") end); IO.inspect("Starting")

In the code above, you will see "Starting" print to the console, the "done".

Processes die when the work they are spawned with ends:

pid = spawn(fn -> "Hello" end)
Process.alive?(pid) #=> false

That means on their own, they aren't very useful. How do we know if the work actually happened? How do we retry, or get the result of the thing the process did?

To improve on this we can use recursion!

all: [GASP]

When processes are alive they can receive messages, which means they can wait for messages to appear in their inbox. This is a blocking action. That means we can spawn a process initialising it with a simple loop:

defmodule Start do
  def loop() do
    receive do
      message -> IO.inspect(message)
    end

    loop()
  end
end

pid = spawn(fn -> Start.loop() end)
Process.alive?(pid) #=> true

We can then send a message:

send(pid, "Hello")

We will see "Hello" printed twice (once is the return value from the send function, the other is the IO.inspect inside the receive).

This idea becomes more powerful if we add state. We do that by starting the process with state, then allowing messages to alter that state in specific ways. Lets build out a calculator. For now it will only add numbers you send it:

defmodule Calculator do
  def loop(number) do
    result = receive do
      message -> (message + number) |> IO.inspect
    end

    loop(result)
  end
end

pid = spawn(fn -> Calculator.loop(20) end)
send(process, 10) #=> we will see 10 printed to the console (the return value from send) then 30.

Nice! Now lets add a way to read the state without having to print to the console. To do that we need to be able to distinguish between the different kinds of messages our process will receive - the read message and the add message. We can do this with pattern matching. Lets first use pattern matching to get the add working:

defmodule Calculator do
  def loop(number) do
    result = receive do
      #left of the arrow is the pattern match, the right side is what we do if we get a pattern that matches
      {:add, new_number} -> new_number + number
    end

    loop(result)
  end
end

pid = spawn(fn -> Calculator.loop(20) end)
send(pid, {:add, 25})

Now for the read. In elixir everything runs in a process, so even if we just spawn in the REPL, that happens in a process. We can get the PID of the process we are currently in by calling self(). When we want to find out the current state of a process that we have spawned the strategy is to get that process to send us a message containing its' current state. We do that by sending it our PID, and starting a receive function where we wait for the return message:

Now we understand all the pieces to get our read function:

defmodule Calculator do
  def equals(pid) do
    send(pid, {:read, self()})
    receive do
      message -> message
    end
  end

  def loop(number) do
    result = receive do
      #left of the arrow is the pattern match, the right side is what we do if we get a pattern that matches
      {:add, new_number} -> new_number + number
      {:read, caller_pid} -> send(caller_pid, number)
    end

    loop(result)
  end
end

pid = spawn(fn -> Calculator.loop(20) end)
send(pid, {:add, 25})

Now we can send a :read message to our caller like this:

Calculator.equals(pid) #=> 55

We can package all this neatly into a module like this:

defmodule Calculator do
  def start(initial_value) do
    spawn(fn -> loop(initial_value) end)
  end

  def add(pid, number) do
    send(pid, {:add, number})
    pid
  end

  def equals(pid) do
    send(pid, {:equals, self()})

    receive do
      value -> value
    end
  end

  defp loop(current_value) do
    new_value =
      receive do
        {:add, value} -> value + current_value
        {:read, caller_pid} -> send(caller_pid, current_value)
      end

    loop(new_value)
  end
end

Calculator.start(0) |> Calculator.add(20) |> Calculator.equals() #=> 20

(the |> operator takes the result of the function on the left of it and passes it as the first argument to the function on the right.)

Wow! We leave it to the reader to implement other calculator functions, and instead try to link all this back to gen_servers

So far we can spawn a process, send it messages, and receive messages back. Now lets imagine we wanted to do the same thing somewhere else. For example, we implement a SimpleLog module like so:

defmodule SimpleLog do
  def start(initial_value) do
    spawn(fn -> loop(initial_value) end)
  end

  def read(pid) do
    send(pid, {:read, self()})

    receive do
      value -> value
    end
  end

  def add(pid, value) do
    send(pid, {:add, value})
    pid
  end

  defp loop(current_log) do
    new_value =
      receive do
        {:add, value} -> current_log <> " " <> value
        {:read, caller_pid} -> send(caller_pid, current_log)
      end

    loop(new_value)
  end
end

SimpleLog.start("hello") |> SimpleLog.add("world") |> SimpleLog.read() #=> "hello world"

If we squint we can see a lot of duplication between the Calculator and the SimpleLog modules. They both have this loop function, and they both want a way to interpret different messages, and do stuff based on them.

Imagine if we were to abstract this duplication! Oh the possibilities!

The good news is, someone already did, and they called it...

a...

gen_server 🎉

Think of it as being short for generic_server, with the server being the process, and the client being us sending messages to it, and waiting for responses.

[ BREAD INTERMISSION ]

If we refactor slightly we can make it more obvious how we might be able to achieve a nice abstraction for this.

defmodule SimpleLog do
  def start(initial_value) do
    spawn(fn -> loop(initial_value) end)
  end

  def read(pid) do
    send(pid, {:read, self()})

    receive do
      value -> value
    end
  end

  def add(pid, value) do
    send(pid, {:add, value})
    pid
  end

  defp loop(current_value) do
    new_value =
      receive do
        message -> process_message(message, current_value)
      end

    loop(new_value)
  end

  def process_message({:add, value}, current_value), do: current_value <> " " <> value
  def process_message({:read, caller_pid}, current_value), do: send(caller_pid, current_value)
end

From this angle, all we need is a way to add a starting state, a way to add implementations of process_message and a way to send the messages we want to pattern match on in the loop.

These are the kinds of abstractions that a gen_server provides!

[INTERMISSION]

We then decide to try and write our own gen_server, backwards engineering the example in the book. We choose fizz_buzz, the full repo can be found here https://github.com/Adzz/fizz-bex

Clone this wiki locally