defprotocol Describe do
@doc "Returns a short human description."
def describe(value)
end
A protocol is polymorphism for data: one function
name, a separate implementation per type. It's how
to_string/1, inspect/1 and Enum work on so
many types.
defprotocol Describe do
@doc "Returns a short human description."
def describe(value)
end
Implementations live with defimpl, for built-in
types or your own structs, and can sit in a
different module or library from the protocol.
defimpl Describe, for: Integer do
def describe(n) when n < 0, do: "a negative integer"
def describe(_n), do: "an integer"
end
defimpl Describe, for: BitString do
def describe(s), do: "a string of #{String.length(s)} characters"
end
defimpl Describe, for: List do
def describe(list), do: "a list of #{length(list)} items"
end
IO.puts(Describe.describe(42))
IO.puts(Describe.describe(-1))
IO.puts(Describe.describe("héllo"))
IO.puts(Describe.describe([1, 2, 3]))
an integer
a negative integer
a string of 5 characters
a list of 3 items
Structs get their own implementation, separate from plain maps.
defmodule Temp do
defstruct [:celsius]
end
defimpl Describe, for: Temp do
def describe(%{celsius: c}), do: "a temperature of #{c}°C"
end
IO.puts(Describe.describe(struct(Temp, celsius: 21)))
a temperature of 21°C
Calling a protocol for a type with no
implementation raises Protocol.UndefinedError.
try do
Describe.describe(:an_atom)
rescue
e in Protocol.UndefinedError ->
IO.puts(e |> Exception.message() |> String.split("\n") |> hd())
end
protocol Describe not implemented for Atom
Built-in protocols plug your types into the
language. Implementing String.Chars makes
to_string/1 and interpolation work; @derive
picks a ready-made implementation, here one that
hides a field from inspect.
defmodule Account do
@derive {Inspect, except: [:password]}
defstruct [:login, :password]
end
defimpl String.Chars, for: Account do
def to_string(%{login: login}), do: "account #{login}"
end
account = struct(Account, login: "ada", password: "hunter2")
IO.puts("Logged in as #{account}")
IO.inspect(account)
Logged in as account ada
#Account<login: "ada", ...>
Pitfall: prefer a protocol over a big
case/condon types when other people need to add types later. If every type is yours and known up front, plain pattern matching in function heads is simpler.
Pitfall:
@derive {Inspect, except: [...]}only hides values frominspect, such as logs and crash reports. The data is still in the struct and still serialized by JSON encoders unless you exclude it there too.