elixir

Pouring Protocols in Elixir

Miguel Palhas

Miguel Palhas on

Pouring Protocols in Elixir

In today's Elixir Alchemy, we will stir into the potion of protocols. Elixir has several mechanisms that allow us to write expressive and intuitive code.

Pattern matching, for instance, is a powerful way of dealing with multiple scenarios without having to go into complicated branching. It allows each of our functions to be clear and concise.

What Are Protocols?

In a way, Protocols are similar to pattern matching, but they allow us to write more meaningful and context-specific code based on the datatype we’re dealing with.

Let’s take the example of a content-delivery website. This website has multiple types of content: audio clips, videos, texts, and whatever else you can think of.

Each of these content types obviously has different attributes and metadata, so it makes sense for them to be represented by independent structs:

Translating this into Elixir, you’d have the following structures:

1defmodule Content.Audio do
2  defstruct [:title, :album, :artist, :duration, :bitrate, :file]
3end
4
5defmodule Content.Video do
6  defstruct [:title, :cast, :release_date, :duration, :resolution, :file]
7end
8
9defmodule Content.Text do
10  defstruct [:title, :author, :word_count, :chapter_count, :format, :file]
11end

Each of these types has a few different fields, most of them unique to the type. We also have a common :file field which will point to the file keeping the actual data.

Now, let’s say you want to make your content as accessible as possible. You may, for instance, want to allow your hearing-impaired users to view the transcripts of both your audio and video. For that, you’ll use your awesome AudioTranscriber and VideoTranscriber modules which provide transcribe_audio/1 and transcribe_video/1 functions, respectively.

The implementation of those functions uses state-of-the-art machine learning and will be delegated to a future blog post. Let’s just assume they work and roll with it.

Both transcriber modules are split up into separate modules. Aside from having different function names for transcribing content, they might be completely different libraries. To allow us to use both in a transparent manner, we'll implement a protocol named Content.Transcribe that has a unified API that can handle both types of content.

Implementing the Protocol

Using protocols, we can easily define what the act of transcribing something means to each of our data types. This is done by first defining a transcribing protocol:

1defprotocol Content.Transcribe do
2  def transcribe(content)
3end

and then implementing it separately for each of our types:

1defimpl Content.Transcribe, for: Content.Video do
2  def transcribe(video), do: VideoTranscriber.transcribe_video(video.file)
3end
4
5defimpl Content.Transcribe, for: Content.Audio do
6  def transcribe(audio), do: AudioTranscriber.transcribe_audio(audio.file)
7end
8
9defimpl Content.Transcribe, for: Content.Text do
10  def transcribe(text), do: File.read(text.file)
11end

We have separately defined implementations of the same function for all 3 content types.

You may note that for text content, the implementation merely reads the corresponding file, as it's already in text format, while for the other two, we call the corresponding machine-learning-magic function on the file.

We’re then able to call transcribe/1 for all the data types we have an implementation for:

1iex> %Content.Video{...} |> Content.Transcribe.transcribe()
2{:ok, "We're no strangers to love\nYou know the rules and so do I..."}
3
4iex> %Content.Audio{...} |> Content.Transcribe.transcribe()
5{:ok, "Imagine there's no heaven\nIt's easy if you try..."}
6
7iex> %Content.Text{...} |> Content.Transcribe.transcribe()
8{:ok, "in a hole in the ground there lived a hobbit..."}

Fallback Implementations

Now, let’s say we add a new type of media to our platform: games (we’re kidding! We are a very ambitious hypothetical startup, and admittedly, success may be getting into our heads).

What happens when we try to transcribe the newly-added content?

1iex> %Content.Game{...} |> Content.Transcribe.transcribe()
2** (Protocol.UndefinedError) protocol Content.Transcribe is not implemented for %Content.Game{...}. This protocol is implemented for: Content.Audio, Content.Text, Content.Video

Whoops! We’ve hit an error. Which makes sense. We didn’t provide any transcription implementation for this type.

But it doesn’t really make sense to do so, does it? Games are supposed to be interactive experiences, and there simply may be no way to make them accessible to everyone.

So we could just provide an implementation that always fails:

1defimpl Content.Transcribe, for: Content.Game do
2  def transcribe(game), do: {:error, "not supported"}
3end

But this doesn’t seem very scalable, does it? If we keep adding new content types, we'll end up having to duplicate this for every single type that we cannot transcribe.

Instead, we can simply add a fallback implementation for any type we don’t specify. This is done precisely by providing an implementation for the Any type, and then stating in our protocol that we want to fall back to it when necessary.

1defimpl Content.Transcribe, for: Any do
2  def transcribe(_), do: {:error, "not supported"}
3end
4
5defprotocol Content.Transcribe do
6  @fallback_to_any true
7  def transcribe(content)
8end

The implementation for Any can usually be used by asking Elixir to automatically derive implementations from it (you can read more about this in the official Elixir Getting Started guide).

But by adding @fallback_to_any true to our protocol, we’re stating that whenever a specific implementation is not found, the Any implementation should be used. This allows us to fail gracefully for any unsupported data type:

1iex> %Content.Game{...} |> Content.Transcribe.transcribe()
2{:error, "not supported"}
3
4iex> %{key: :value} |> Content.Transcribe.transcribe()
5{:error, "not supported"}

Failed Gracefully

Can we close off any better than with a graceful fail? We'll leave you now that we've experimented with protocols and we gracefully haven't broken any alembic today.

If you love experimenting with code, make sure you don't miss an episode of Elixir Alchemy!

This post is written by guest author Miguel Palhas. Miguel is a professional over-engineer @subvisual and organizes @rubyconfpt and @MirrorConf.

Share this article

RSS
Miguel Palhas

Miguel Palhas

Guest author Miguel is a professional over-engineer at Portuguese-based Subvisual. He works mostly with Ruby, Elixir, DevOps, and Rust. He likes building fancy keyboards and playing excessive amounts of online chess.

All articles by Miguel Palhas

AppSignal monitors your apps

AppSignal provides insights for Ruby, Rails, Elixir, Phoenix, Node.js, Express and many other frameworks and libraries. We are located in beautiful Amsterdam. We love stroopwafels. If you do too, let us know. We might send you some!

Discover AppSignal
AppSignal monitors your apps