पाठ 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.

त्वरित जाँच: 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.