Lesson 8 / 25
Structs, Protocols and Behaviours
Define structs, implement protocols for polymorphism and declare behaviours.
Structured data and polymorphism
A struct is a map with a fixed set of keys, defaults and a name, defined in a module with defstruct: %Order{id: "o-1", total: 0}. Structs provide compile-time key checking, pattern matching on the type (%Order{} = value) and @enforce_keys for required fields. They do not inherit the Enum or Access behaviour of maps automatically. Protocols provide polymorphism over data types: define a protocol with defprotocol and implement it for specific types with defimpl, so Shop.Describable.describe(value) dispatches on the value's type. Built-in protocols include String.Chars (used by interpolation), Inspect, Enumerable and Jason.Encoder in the popular JSON library. Behaviours define a contract for modules: @callback declarations list the functions a module must implement, and @behaviour plus @impl true mark implementations, with compiler warnings for missing callbacks. GenServer and Plug are behaviours, and behaviours are the usual way to swap implementations such as a real payment gateway and a test double.
A struct, a protocol and a behaviour
Type-specific behaviour without inheritance.
defmodule Shop.Order do
@enforce_keys [:id, :customer_id]
defstruct [:id, :customer_id, items: [], status: :placed]
def total(%__MODULE__{items: items}), do: Enum.sum(Enum.map(items, & &1.price * &1.qty))
end
defprotocol Shop.Describable do
@doc "A short human-readable description."
def describe(value)
end
defimpl Shop.Describable, for: Shop.Order do
def describe(order), do: "Order #{order.id} (#{order.status}), #{Shop.Order.total(order)} paise"
end
defimpl String.Chars, for: Shop.Order do
def to_string(order), do: "Order<#{order.id}>"
end
defmodule Shop.PaymentGateway do
@callback charge(order_id :: String.t(), amount :: pos_integer()) ::
{:ok, String.t()} | {:error, atom()}
end
defmodule Shop.FakeGateway do
@behaviour Shop.PaymentGateway
@impl true
def charge(order_id, amount) when amount > 0, do: {:ok, "pay_#{order_id}"}
def charge(_order_id, _amount), do: {:error, :invalid_amount}
end
order = %Shop.Order{id: "o-1", customer_id: "c-1", items: [%{price: 4_950, qty: 2}]}
IO.puts(Shop.Describable.describe(order))
IO.puts("Created #{order}") # uses String.Chars
IO.inspect(Shop.FakeGateway.charge(order.id, Shop.Order.total(order)))Universal remote buttons
A protocol is a "play" button on a universal remote: each device type (TV, speaker, projector) implements what play means for it. A behaviour is a job description listing the duties any module filling that role must perform.
Quick check: What is the difference between a protocol and a behaviour?
- They are identical
- Behaviours dispatch on data types
- Protocols only work with strings
- Protocols dispatch on the data type of a value; behaviours define functions a module must implement
Answer
Protocols dispatch on the data type of a value; behaviours define functions a module must implement — Protocols give data-type polymorphism; behaviours are module contracts.