Structs

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.

maps/defstruct.exs
 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.

maps/defstruct1.exs
 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.