Subscribe now

Preparing to Learn about OTP [02.13.2017]

In the last episode, you were provided an exercise to create a Reverse Polish Notation Calculator process. Today we'll look at a solution, as well as some readings to prepare to learn more about OTP. Let's get started.

Exercise Solution

It's good to go through the exercise piece by piece and test-drive the solution. When you first pull down the rpn repo, tagged before this episode and run the tests, you'll find that none pass of course. We can focus on the first test:

  test "starts with an empty stack" do
    {:ok, pid} = Rpn.start
    assert Rpn.peek(pid) == []
  end

This says that if we evaluate the peek function with our process id as an argument, we should get back an empty list. Let's implement that, with a public function to send the message and a case statement for the process receive block:

defmodule Rpn do
  def start do
    {:ok, spawn(__MODULE__, :loop, [[]])}
  end

  def loop(stack) do
    receive do
      # We'll receive a message from another process, with a ref, and the atom
      # `peek`. We will then send back a 2-tuple containing the ref they sent
      # and the value of our stack.
      {from, ref, :peek} ->
        send(from, {ref, stack})
        loop(stack)
    end
  end

  # We'll provide a function that makes it easy to send the message the loop is
  # waiting for and then enters a receive loop of its own, awaiting a reply. The
  # reason we make these unique references, by the way, is because anyone could
  # send us a message at any time, and if it matched the pattern we were looking
  # for we would accept it - this way we only match the message we're hoping to
  # get back.
  def peek(pid) do
    ref = make_ref()
    send(pid, {self(), ref, :peek})
    # Once we receive the value, we just return it.
    receive do
      {^ref, val} -> val
    end
  end
  # ...
end

That's enough code to make the first test pass. Let's move on to the second test:

  test "pushing onto the stack" do
    {:ok, pid} = Rpn.start
    Rpn.push(pid, 5)
    assert Rpn.peek(pid) == [5]
    Rpn.push(pid, 1)
    assert Rpn.peek(pid) == [1, 5]
  end

This test says we can push items onto the stack and see them there. It's easy enough to make this work - we just accept a push message that includes a value, and put it into our internal state:

defmodule Rpn do
  # ...
  def loop(stack) do
    receive do
      # ...
      # When someone pushes a value, we just create a new list with that value
      # at the front of it and loop.
      {:push, val} ->
        loop([val | stack])
    end
  end
  # ...
  # And we make it easy to push a new value.
  def push(pid, val) do
    send(pid, {:push, val})
  end
end

Alright, so now we have two tests passing. Next, we need to support adding:

  test "adding" do
    {:ok, pid} = Rpn.start
    Rpn.push(pid, 5)
    Rpn.push(pid, 1)
    Rpn.push(pid, :+)
    assert Rpn.peek(pid) == [6]
  end

When we get to the point that we push the :+ onto the stack, we have two numbers already. We just need to pull them off the stack and push the result of adding them together back onto the stack.

We'll handle the atom + as a value higher up in our receive block, so that it takes precedence. When the value is :+, we pattern match out the top two items from the stack, add them together, and push the result onto the stack in their place. Note that their order is reversed here - second, then first. That's important when you get to an operator that cares about ordering.

  def loop(stack) do
    receive do
      # ...
      {:push, :+} ->
        [second | [first | rest]] = stack
        loop([first + second | rest])

      {:push, val} ->
        loop([val | stack])
    end
  end

From here, implementing subtraction and multiplication is easy:

  def loop(stack) do
    receive do
      # ...
      {:push, :-} ->
        [second | [first | rest]] = stack
        loop([first - second | rest])

      {:push, :x} ->
        [second | [first | rest]] = stack
        loop([first * second | rest])

      {:push, val} ->
        loop([val | stack])
    end
  end

And with that, the whole test suite passes. There are definitely other ways you could have implemented it, but this is how I'd go about it. You can find my full solution here. You'll note that I didn't implement division - that's because no tests called for it! :)

Preparatory Readings

This week we'll be looking at OTP in depth. OTP stands for Open Telecom Platform, which is a collection of middleware and libraries in Erlang. We'll cover the most prominent parts of OTP:

  • GenServer
    • This is a generic server process. It formalizes some of the things we've done til now to talk to processes, as well as adds support for supervision, which is how Elixir and Erlang programs achieve their high availability. It also provides facilities for tracing and error reporting that bare processes don't have.
  • Supervisor
    • A supervisor supervises other processes - its children. Supervisors define your application's hierarchy and they are the backbone of fault-tolerance within OTP.
  • GenStateMachine
    • The standard library from Erlang provides gen_statem, which is a state machine behaviour. We'll use the GenStateMachine package for state machines, as it yields a better experience in Elixir.

Resources