Structs and Protocols

Elixir structs for typed data and protocols for polymorphism

Structs

Structs are compile-time maps with a defined set of fields and a __struct__ key. They provide a contract for data shape.

Defining a struct

defmodule User do
  defstruct name: nil, email: nil, age: 0
end

Creating struct instances

# All fields
u = %User{name: "Alice", email: "[email protected]", age: 30}

# With defaults
u = %User{name: "Alice"}

# Access fields
u.name   # => "Alice"

# Update fields (returns new struct)
%{u | age: 31}  # => %User{name: "Alice", email: nil, age: 31}

# Pattern matching
%User{name: name} = u
name  # => "Alice"

Enforcing keys

Use @enforce_keys to require certain fields at creation time:

defmodule Order do
  @enforce_keys [:id, :total]
  defstruct [:id, :total, status: :pending]
end

%Order{id: 1, total: 99.99}      # OK
%Order{total: 99.99}             # ** (ArgumentError) the following keys must also be given when building struct Order: [:id]

Deriving protocols

defmodule Point do
  @derive [Inspect, {Poison.Encoder, only: [:x, :y]}]
  defstruct [:x, :y]
end

Protocols

Protocols provide polymorphism in Elixir — dispatching behavior based on the type of the first argument.

Defining a protocol

defprotocol Size do
  @doc "Calculates the size of a data structure"
  def size(data)
end

Implementing for types

defimpl Size, for: BitString do
  def size(string), do: String.length(string)
end

defimpl Size, for: List do
  def size(list), do: length(list)
end

defimpl Size, for: Map do
  def size(map), do: map_size(map)
end

# Using it
Size.size("hello")      # => 5
Size.size([1, 2, 3])    # => 3
Size.size(%{a: 1, b: 2}) # => 2

Implementing for a struct

defmodule Box do
  defstruct [:width, :height]
end

defimpl Size, for: Box do
  def size(%Box{width: w, height: h}), do: w * h
end

Size.size(%Box{width: 10, height: 5})  # => 50

Fallback with @fallback_to_any

defprotocol Blank do
  @fallback_to_any true
  def blank?(data)
end

defimpl Blank, for: List do
  def blank?([]), do: true
  def blank?(_), do: false
end

defimpl Blank, for: Any do
  def blank?(_), do: false
end

Blank.blank?([])       # => true
Blank.blank?([1])      # => false
Blank.blank?("hello")   # => false (falls back to Any)

Built-in protocols

Protocol Purpose Example
String.Chars Convert to string to_string/1
Inspect Custom pretty-printing inspect/1
Enumerable Iterate over collections Enum.map/2
Collectable Collect into a container Enum.into/2
Enumerable + Collectable Enables for comprehensions

Deriving built-in protocols

defmodule User do
  @derive {Inspect, only: [:name]}
  defstruct [:name, :email, :password]
end

inspect(%User{name: "Alice", email: "[email protected]", password: "secret"})
# => %User{name: "Alice", ...}