Mix.install([
{:jason, "~> 1.4"},
{:kino, "~> 0.9", override: true},
{:youtube, github: "brooklinjazz/youtube"},
{:hidden_cell, github: "brooklinjazz/hidden_cell"}
])
Upon completing this lesson, a student should be able to answer the following questions.
- How do you achieve data-based polymorphism using protocols?
- Why might you use protocols instead of some control flow structure or some alternative means of achieving data-based polymorphism?
In English, a protocol means a set of rules or procedures. In Elixir, a protocol allows us to create a common functionality, with different implementations.
Specifically, protocols enable polymorphic behavior based off of data.
flowchart
A -- data --> B
B --> C
B --> D
B --> E
B --> F
A[Caller]
B[Protocol]
C[Implementation]
D[Implementation]
E[Implementation]
F[Implementation]
For example, the Enum module works with any collection data type. Under the hood, it uses the Enumerable protocol.
flowchart
E[Enum]
EN[Enumerable]
E --> EN
EN --> Map
EN --> List
EN --> k[Keyword List]
It then executes the appropriate instructions depending on which data type the Enum function is called with.
Enum.random(%{one: 1, two: 2})
Enum.random(1..20)
Enum.random(one: 1, two: 2)
Anytime you need a common function for multiple data types or structs, you can consider a protocol.
For example, let's create an Adder
protocol
that's going to add two values together. It will accept integers, strings, and lists
and hide the specifics of which operator is necessary to add different types.
Adder.add(1, 2)
3
Adder.add("hello, ", "world")
"hello, world"
Adder.add([1], [2])
[1, 2]
So if we give the protocol an integer, it will use the implementation for Integer.
If we provide the protocol a string, it will use the implementation for BitString
, and
if we provide the protocol a list, it will use the implementation for List.
flowchart
A -- Integer --> B
B -- Integer --> C
B -- BitString --> D
B -- List --> E
style C color:green
style D color:red
style E color:red
A[Caller]
B[Adder Protocol]
C[Integer Implementation]
D[String Implementation]
E[List Implementation]
List, Integer, and BitString
are all built-in Elixir modules used to define
which data type the protocol implementation is for.
We define a protocol using defprotocol
and define a function clause. We only need the function
head, not the function body.
defprotocol Adder do
def add(value, value)
end
We've defined a protocol above, but we haven't implemented it for any data type yet.
Notice the error for Adder.add/2
called with two integers says
protocol Adder not implemented for 1 of type Integer
Adder.add(1, 2)
To define an implementation for a protocol, we use defimpl
and provide it the name of the protocol.
We also declare what struct or data type the protocol is for:
defimpl Adder, for: Integer do
def add(int1, int2) do
int1 + int2
end
end
Adder.add(1, 2)
We also want the Adder
protocol to handle strings and lists. That means we need to create
an implementation for List and String. In Elixir, the underlying type for strings is BitString
.
Why BitString
? In Elixir, strings are stored as bitstrings.
defimpl Adder, for: BitString do
def add(string1, string2) do
string1 <> string2
end
end
Adder.add("hello, ", "world")
In the Elixir cell below, create an implementation of Adder
for lists.
Example Solution
defimpl Adder, for: List do
def add(list1, list2) do
list1 ++ list2
end
end
We can create implementations for a protocol based on simple data types such as integer
and string
.
In addition to those simple data types, we can also create an implementation for specific structs.
For example, let's say we're making an old school kids toy that says different animal noises.
we'll create a Sound
protocol that prints different sounds depending on
the struct given to it.
defprotocol Sound do
def say(struct)
end
We'll create a Cat
struct.
defmodule Cat do
defstruct [:mood]
end
Then a Cat
implementation for the Sound
protocol.
defimpl Sound, for: Cat do
def say(cat) do
case cat.mood do
:happy -> "Purr"
:angry -> "Hiss!"
end
end
end
Sound.say(%Cat{mood: :happy})
Sound.say(%Cat{mood: :angry})
defmodule Dog do
defstruct []
end
Define a Sound
implementation for the Dog
struct above.
When Sound.say/1
is called with a Dog
struct it should return "Woof!"
Sound.say(%Dog{})
"Woof!"
Example Solution
defimpl Sound, for: Dog do
def say(dog) do
"woof!"
end
end
For more information, there is a talk by Kevin Rockwood.
YouTube.new("https://www.youtube.com/watch?v=sJvfCE6PFxY")
DockYard Academy now recommends you use the latest Release rather than forking or cloning our repository.
Run git status
to ensure there are no undesirable changes.
Then run the following in your command line from the curriculum
folder to commit your progress.
$ git add .
$ git commit -m "finish Protocols reading"
$ git push
We're proud to offer our open-source curriculum free of charge for anyone to learn from at their own pace.
We also offer a paid course where you can learn from an instructor alongside a cohort of your peers. We will accept applications for the June-August 2023 cohort soon.