String.Chars Protocol for Elixir PIDs

TIL: A Convenience Tip for Your Elixir Development  

I recently did a live coding session on a 3d audio server built in Elixir.

This project involves quite a complicated supervision tree where GenServers are created and destroyed dynamically based on users joining a room and moving around.

As such this requires in-depth logging of processes in the application.

                   [VollumaServer]
                          |
                    -------------
                    |           |
       [ROOMSERVER:1]           [ROOMSERVER:2]
                    |           |
---------------------           ------------------
|                   |           |                |
[MEMBERSHIPS:1]     [CELLS:1]   [MEMBERSHIPS:2]  [CELLS:2]
| | | | | | | |     | | | |     | | | | | |      | | |
U U U U U U U U     1 2 3 4     U U U U U U      1 2 3

Regulary I found myself logging PIDs (process IDs) as I dynamically created GenServers.

The code would look something like this:

def start_link(room = %Room{}) do
  {:ok, pid} = DynamicSupervisor.start_link(
    __MODULE__, room, name: registry_name(room)
  )

  Logger.debug fn ->
    "Created CellsSupervisor for room #{room.id} at #{inspect pid}."
  end

  {:ok, pid}
end

The line I’d like to focus on is this one:

"Created CellsSupervisor for room #{room.id} at #{inspect pid}."

Notice how use the room.id without any processing? The pid however must be inspected.

The reason for this is that number and string types (along with most basic types) in Elixir implement the String.Chars protocol.

For those new to Elixir, protocols are the language’s way of implementing a sort of polymorphism without breaking Elixir’s fabled immutability. They are really fascinating and one of Elixir’s most underrated features.

The String.Chars protocol is the protocol that defines how an Elixir type is converted to a string.

It seems a little bit of an oversight perhaps, considering how essential the PID type is in Elixir development, to not have a native implementation of the String.Chars protocol as part of the language.

Trying to interpolate a String with a bare PID will result in the dreaded error:

iex> IO.puts "This will cause the error: #{self()}"

** (Protocol.UndefinedError) protocol String.Chars
   not implemented for #PID<0.100.0>

    (elixir) lib/string/chars.ex:3: String.Chars.impl_for!/1
    (elixir) lib/string/chars.ex:22: String.Chars.to_string/1

Thankfully, we can easily get around this for any type in Elixir by calling the Kernel.inspect method on it (or just use inspect for short).

iex> IO.puts "Yay! This works: #{inspect self()}"

Yay! This works: #PID<0.100.0>

However, there is a better way. #PID<0.100.0> is an ugly thing to have strewn about our logs, and gives us very little information about what the process behind the PID is doing.

Let’s read that error again.

protocol String.Chars not implemented for #PID<0.100.0>

Let’s implement the String.Chars protocol so that we can use a PID in our logs without having to inspect it, and possibly add some useful information alongside.

Disclaimer: This may have a performance impact on your application if you’re not careful. I’d suggest only using PIDs in debug logs, and to also ensure all calls to Logger.debug and its siblings are given an anonymous function instead of interpolated strings. This allows the Erlang VM to omit expensive iterpolations,

Here is my inital implementation of the String.Chars protocol:

# ./lib/utils/pid.ex

defimpl String.Chars, for: PID do
  def to_string(pid) do
    info = Process.info pid
    name = info[:registered_name]

    "#{name}-#{inspect pid}"
  end
end

Here we retrieve the “registered name” of the module (what you supply as the name when you call start_link on a GenServer). You can retrieve additional information as well here if it suits your needs.

Personally, I have found these few lines of code invaluable in debugging my complicated Elixir supervision trees and logging the connections between different processes in an application.

Hopefully this helps out fellow Elixir enthusiasts in their travels! Take it easy. :)

comments powered by Disqus