Subscribe now

Intermediate Elixir Prep [02.13.2017]

Last week we learned the basics of Elixir. This week, we're going to dig in a bit more deeply. We'll learn about:

  • managing state in processes
  • for comprehensions
  • testing

I'll provide you with a list of resources you can read up on to prepare for the week's work. First, let's see a solution to last week's exercise.

Exercise Solution

In the last episode, I provided an exercise to build a microservice that will accept requests to upcase strings and respond to them, using processes. This is a very contrived example, as there's no need to do this with a process rather than a function, but it's an opportunity to gain experience with processes.

Here's how I solve this exercise, for a comparison to your own solution.

First, I start a new project:

mix new upcaser
cd upcaser

I then write a test for the behaviour I expect. This helps me work out the user-focused API before I start just pounding out code:

vim test/upcaser_test.exs
defmodule UpcaserTest do
  use ExUnit.Case

  # We'll need the ability to start the service
  test "starting the service" do
    # I want to return a 2-tuple so I can pattern match on whether or not it was
    # started successfully
    assert {:ok, upcaser_pid} = Upcaser.start
    # We can also ensure that the second element in our 2-tuple is a pid:
    assert is_pid(upcaser_pid)
  end
end

Now I run the tests and find no start function on the Upcaser module. Let's add one that spawns a new process that just returns :ok for now:

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

  def loop do
    :ok
  end
end

If we run the tests, they fail - this is because spawn just returns a pid. I think I'd like to be able to handle failure for this case ultimately, so I specified that I'd like it to return the 2-tuple {:ok, pid}. This isn't terribly important, especially since there's no error return value from spawn, but we'll modify our function to return what I'm expecting:

defmodule Upcaser do
  def start do
    pid = spawn(Upcaser, :loop, [])
    {:ok, pid}
  end

  def loop do
    :ok
  end
end

Now the tests should pass. So you can start this process, but it doesn't do anything. Let's talk about how we'd like to handle the upcasing:

defmodule UpcaserTest do
  use ExUnit.Case
  # ...
  test "sending a string to be upcased" do
    # Start the service
    {:ok, upcaser_pid} = Upcaser.start
    # Send a string and get the expected response
    send(upcaser_pid, {self(), {:upcase, "foo"}})
    assert_receive {:ok, "FOO"}
  end
end

This isn't precisely ideal, but it's a start. If we run this it fails because our loop doesn't do anything. We can fix that:

defmodule Upcaser do
  def start do
    pid = spawn(Upcaser, :loop, [])
    {:ok, pid}
  end

  def loop do
    # We'll listen for the messages we care about, and when we get one we'll
    # send the upcased data back.
    receive do
      {from, {:upcase, str}} -> send(from, {:ok, String.upcase(str)})
    end
    # Then we call back into ourselves, just like in the PingPong example.
    loop
  end
end

With that, our tests pass. But we don't want to stop there. It's pretty awful to have to know to send this exact structure to our service, so we'll wrap up a public function that does it for us. Let's tweak the test to both send the data and wait for a response:

defmodule UpcaserTest do
  use ExUnit.Case
  # ...
  test "sending a string to be upcased" do
    # Start the service
    {:ok, upcaser_pid} = Upcaser.start
    # Send a string and get the expected response
    assert {:ok, "FOO"} = Upcaser.upcase(upcaser_pid, "foo")
  end
end

This is a bit nicer to work with. Let's look at how we would implement it:

defmodule Upcaser do
  # ...
  def upcase(server_pid, str) do
    # We'll send the server a request to upcase
    send(server_pid, {self(), {:upcase, str}})
    # Then we'll block, waiting to get a response. Once we do, we'll return it.
    receive do
      {:ok, str} -> {:ok, str}
    end
  end
end

This seems like it should mostly work. One issue with this is that anyone can send us a message that matches that and pattern and we'll assume it was a response to our call to the server. We can get around this by learning a little more about elixir.

There's a function called make_ref/0 that will give you an almost unique reference. We can pass this to the server as well, and if we get a message back with the same reference we can be pretty freaking sure that this response is the one we're looking for. You can play with make_ref in the REPL if you want to see more, but for now we'll make one and send it as part of our call:

defmodule Upcaser do
  # ...
  def upcase(server_pid, str) do
    # We'll make a reference
    ref = make_ref()
    send(server_pid, {self(), ref, {:upcase, str}})
    receive do
      # Here we're 'pinning' the ref variable - we're saying we only match where
      # the second element in the tuple matches a given variable. Without the
      # pin(^), this would rebind the `ref` variable.
      {:ok, ^ref, str} -> {:ok, str}
    end
  end
end

Now our server has to include the ref in its response:

defmodule Upcaser do
  # ...
  def loop do
    receive do
      {from, ref, {:upcase, str}} -> send(from, {:ok, ref, String.upcase(str)})
    end
    loop
  end
  # ...
end

With that, we've built out a little service from processes and references, with a nice public interface wrapper function, that makes it easy to perform our string uppercasing in another process. You could solved the problem a number of other ways, and that would be fine. I just wanted to take the opportunity to introduce make_ref to you as well as talk through building out a nice public API for your processes.

Preparatory Readings

Now that you're familiar with processes in Elixir, think about how you would implement mutable state using a process. We'll do that tomorrow. We'll also look at a basic offering for mutable state management that Elixir provides, in Agent [docs].

We'll also look at for comprehensions . These are like enumeration on steroids.

Finally, we'll look more deeply into Testing. We got a taste of tests today

Resources