Elixir Protocol Implementations
An implementation is the code that keeps a protocol's promises for one data type. It turns the protocol's list of functions into real behaviour for that type.
A built-in type gets its own implementation
The string implementation counts characters. The list implementation counts items.
defprotocol Sized do
def size(data)
def empty?(data)
def present?(data)
end
defimpl Sized, for: BitString do
def size(string), do: String.length(string)
def empty?(string), do: string == ""
def present?(string), do: string != ""
end
defimpl Sized, for: List do
def size(list), do: length(list)
def empty?(list), do: list == []
def present?(list), do: list != []
end
IO.puts(Sized.size("hello"))
IO.puts(Sized.size([10, 20]))
IO.puts(Sized.empty?(""))
IO.puts(Sized.present?([10]))A custom struct gets its own implementation
The Shelf struct holds books in a list. Its implementation counts those books.
defmodule Shelf do
defstruct books: []
end
defprotocol Sized do
def size(data)
def empty?(data)
def present?(data)
end
defimpl Sized, for: Shelf do
def size(%Shelf{books: books}), do: length(books)
def empty?(%Shelf{books: books}), do: books == []
def present?(%Shelf{books: books}), do: books != []
end
shelf = struct(Shelf, books: ["a", "b", "c"])
IO.puts(Sized.size(shelf))
IO.puts(Sized.empty?(shelf))
IO.puts(Sized.present?(shelf))One protocol covers built-in types and a struct
The same Sized calls work for a string, a list, and a Shelf. Dispatch picks the right code for each value.
defmodule Shelf do
defstruct books: []
end
defprotocol Sized do
def size(data)
def empty?(data)
def present?(data)
end
defimpl Sized, for: BitString do
def size(string), do: String.length(string)
def empty?(string), do: string == ""
def present?(string), do: string != ""
end
defimpl Sized, for: List do
def size(list), do: length(list)
def empty?(list), do: list == []
def present?(list), do: list != []
end
defimpl Sized, for: Shelf do
def size(%Shelf{books: books}), do: length(books)
def empty?(%Shelf{books: books}), do: books == []
def present?(%Shelf{books: books}), do: books != []
end
shelf = struct(Shelf, books: ["a", "b", "c"])
IO.puts(Sized.size("hello"))
IO.puts(Sized.size([10, 20]))
IO.puts(Sized.size(shelf))Example
defmodule Shelf do
defstruct books: []
end
defprotocol Sized do
def size(data)
def empty?(data)
def present?(data)
end
defimpl Sized, for: BitString do
def size(string), do: String.length(string)
def empty?(string), do: string == ""
def present?(string), do: string != ""
end
defimpl Sized, for: List do
def size(list), do: length(list)
def empty?(list), do: list == []
def present?(list), do: list != []
end
defimpl Sized, for: Shelf do
def size(%Shelf{books: books}), do: length(books)
def empty?(%Shelf{books: books}), do: books == []
def present?(%Shelf{books: books}), do: books != []
end
shelf = struct(Shelf, books: ["a", "b"])
IO.puts(Sized.size("hello"))
IO.puts(Sized.size([10, 20, 30]))
IO.puts(Sized.size(shelf))
IO.puts(Sized.empty?(shelf))
IO.puts(Sized.present?(""))Key points
- defimpl: defimpl names the protocol and the type it covers.
- Built-in types: One protocol can have a separate implementation for a string and for a list.
- Custom struct: A struct gets its own implementation in the same way as a built-in type.
- Own shape: Each implementation uses its own value shape, so a list counts items and a string counts characters.
- Complete coverage: Each implementation covers every function the protocol promises.
- One call: Callers use one protocol name, and dispatch picks the matching implementation.
In this example, the Sized protocol is defined once. Then a string implementation, a list implementation, and a Shelf implementation each cover all three promised functions. Finally the calls show each type using its own implementation, including a Shelf built at the top level with struct.
Try it Yourself
Edit the code, then press Run.
Running is simulated
Exercise
Which code gives the Shelf struct a Sized implementation?