Shape Up With Elixir Protocols

Adz
nested.com
Published in
3 min readFeb 1, 2018

--

This picture really has nothing to do with the article, but you know, sunsets.

Here at nested we use Elixir. In Elixir all functions are namespaced. That means when you write a function, they have to be defined inside of a module. This is good because it means we can easily define the same function, but implement it differently for different modules. Here’s an example:

Excellent, no name clashes because when we call it we namespace the function like so:

This means we know we should call Square.area() when we are passing in a square, and Circle.area() when we are passing in a circle.

But what happens if we don’t know what shape we are going to have?

Let’s imagine we want to use our shapes to work out how much it would cost to tile an area the size of the shape. We want a function that takes in a shape and a cost of tile per meter squared, and then returns us a total cost for the given area:

What do we put in the function? Well if we want to be able to handle each shape, we’ll need to do something like this:

Yuck.

Now you can disguise the case statement by using pattern matching:

But it’s still a case statement. It just looks a bit nicer.

The problem here is that if we want to add another shape, a triangle for example, we need to crack open this Project module and add to the case statement a Triangle case. That might be possible, but imagine if the Project module came from a library, and so we didn’t have access to it. What do we do then? The size and shape of our total_cost function is very tightly coupled to the number and types of shapes we have in our program.

What would be really nice is a way to magically call the appropriate function based on the shape being passed in. So if we pass in a square we run square’s area function, but if we pass in a circle, we run circle’s area function. What we want is an interface something like this:

Luckily there’s a way to do this in elixir: what we need is a protocol

A protocol works like this, you say what functions you want your modules to implement. Then you provide implementations for them.

First let’s define our protocol, we’ll call it Shape, and we’ll say we want anyone who implements this protocol to do so by defining an area function.

That was easy. Notice how the function area doesn’t have a do or an end? That’s because it’s not really a function, it’s just a way of saying that in order to implement this protocol, we need to provide a function called area and it must have one argument.

Now let’s implement the Shape protocol for a Square.

What this says is if I now call Shape.area() and pass in a Square as the first argument, it will run the area function defined above. In other words it will run the Square implementation of the Shape protocol. We call it like this:

Now lets define an implementation for a circle.

This. Is. Awesome.

Now we can call Shape.area() and run a completely different function depending on what shape we pass in to it. Without a case statement in sight!

Let’s add a triangle. First, we’ll need a triangle:

Then we’ll define an implementation of the Shape protocol for it:

Look at that, we didn’t even have to touch the original protocol. The protocol could have been defined in a library, and we would be completely fine adding our own implementations for our own shapes.

What’s more, check out how it improves our total_cost function:

Now we (or anyone else) can add as many shapes as we like without touching any of our old code, and know that our total_cost function will work a treat!

Look out for places to use protocols in your code, and if you like what you read, Nested are hiring!

All the code examples are available here:

https://gist.github.com/Adzz/77344ef43a5c1c5c4d818370a6bdff67

--

--