When Elixir sees %{ … } it knows it is looking at a map. But it doesn’t know much more than that. In particular, it doesn’t know what you intend to do with the map, whether only certain keys are allowed, or whether some keys should have default values.
That’s fine for anonymous maps. But what if we want to create a typed map—a map that has a fixed set of fields and default values for those fields, and that you can pattern-match by type as well as content.
Enter the struct.
A struct is just a module that wraps a limited form of map. It’s limited because the keys must be atoms and because these maps don’t have Dict capabilities. The name of the module becomes the name of the map type.
Inside the module, you use the defstruct macro to define the struct’s members.
| defmodule Subscriber do |
| defstruct name: "", paid: false, over_18: true |
| end |
Let’s play with this in IEx:
| $ iex defstruct.exs |
| iex> s1 = %Subscriber{} |
| %Subscriber{name: "", over_18: true, paid: false} |
| iex> s2 = %Subscriber{ name: "Dave" } |
| %Subscriber{name: "Dave", over_18: true, paid: false} |
| iex> s3 = %Subscriber{ name: "Mary", paid: true } |
| %Subscriber{name: "Mary", over_18: true, paid: true} |
The syntax for creating a struct is the same as the syntax for creating a map—you simply add the module name between the % and the {.
You access the fields in a struct using dot notation or pattern matching:
| iex> s3.name |
| "Mary" |
| iex> %Subscriber{name: a_name} = s3 |
| %Subscriber{name: "Mary", over_18: true, paid: true} |
| iex> a_name |
| "Mary" |
And updates follow suit:
| iex> s4 = %Subscriber{ s3 | name: "Marie"} |
| %Subscriber{name: "Marie", over_18: true, paid: true} |
Why are structs wrapped in a module? The idea is that you are likely to want to add struct-specific behavior.
| defmodule Attendee do |
| defstruct name: "", paid: false, over_18: true |
| |
| def may_attend_after_party(attendee = %Attendee{}) do |
| attendee.paid && attendee.over_18 |
| end |
| |
| def print_vip_badge(%Attendee{name: name}) when name != "" do |
| IO.puts "Very cheap badge for #{name}" |
| end |
| |
| def print_vip_badge(%Attendee{}) do |
| raise "missing name for badge" |
| end |
| end |
| $ iex defstruct1.exs |
| iex> a1 = %Attendee{name: "Dave", over_18: true} |
| %Attendee{name: "Dave", over_18: true, paid: false} |
| iex> Attendee.may_attend_after_party(a1) |
| false |
| iex> a2 = %Attendee{a1 | paid: true} |
| %Attendee{name: "Dave", over_18: true, paid: true} |
| iex> Attendee.may_attend_after_party(a2) |
| true |
| iex> Attendee.print_vip_badge(a2) |
| Very cheap badge for Dave |
| :ok |
| iex> a3 = %Attendee{} |
| %Attendee{name: "", over_18: true, paid: false} |
| iex> Attendee.print_vip_badge(a3) |
| ** (RuntimeError) missing name for badge… |
Notice how we could call the functions in the Attendee module to manipulate the associated struct.
Structs also play a large role when implementing polymorphism, which we’ll see when we look at protocols.