Subscribe to access all episodes. View plans →
Published November 11, 2019
Elixir 1.8
In Elixir structs are specialized maps that give you the ability to include default values as well as provide compile-time checks. In this episode we’ll get an introduction to structs.
To start, let’s see how we can define a struct. Let’s give some structure to our empty PhoneNumber
module. We’ll define 3 parts for our phone numbers: the country code, area code, and the number.
To define a struct we use the defstruct
construct with the fields we want. So let’s include “country_code”, “area_code”, and “number”.
lib/phone_number.ex
defmodule Teacher.PhoneNumber do
defstruct [:country_code, :area_code, :number]
end
Now that we’ve defined our struct, let’s go to the command line. And we’ll start an IEx
session with our project.
Let’s test out our struct by creating one. First, we’ll alias our PhoneNumber
module and since our struct was defined in the PhoneNumber
module that’s the name it will use.
To call our struct we’ll do so with a % - the name of the struct - and then then curly braces - and we see an empty struct was returned.
Now let’s create a phone number struct with some data. We’ll give it an “area_code”, “country_code”, and a “number”. Then once we have our struct we can return values from it.
$ iex -S mix
> alias Teacher.PhoneNumber
Teacher.PhoneNumber
> %PhoneNumber{}
%Teacher.PhoneNumber{area_code: nil, country_code: nil, number: nil}
> phone_num = %PhoneNumber{area_code: "212", country_code: "+1", number: "555-1234"}
%Teacher.PhoneNumber{area_code: "212", country_code: "1", number: "555-1234"}
> phone_num.area_code
"212"
One of the nice features of structs is that they allow us to make certain fields required.
Let’s go back to our module and make our “area_code” and “number” fields required to create a struct. To do that we’ll use the @enforce_keys
module attribute giving it the keys we want to require. And that’s it - our keys will now be required to create a struct.
lib/phone_number.ex
defmodule Teacher.PhoneNumber do
@enforce_keys [:area_code, :number]
defstruct [:country_code, :area_code, :number]
end
Let’s go back to our IEx
session and reload our PhoneNumber
module. Now if we try to create a new %PhoneNumber{}
struct without a number - we get an error telling us we’re missing the required “number” key.
> r Teacher.PhoneNumber
warning: redefining module Teacher.PhoneNumber (current version defined in memory)
> phone_num = %PhoneNumber{area_code: "212", country_code: "+1"}
** (ArgumentError) the following keys must also be given when building struct Teacher.PhoneNumber: [:number]
(teacher) expanding struct: Teacher.PhoneNumber.__struct__/1
iex:7: (file)
Structs also allow us to set default values for fields. Back in our module, let’s update our struct so that the “country_code” defaults to “+1”.
lib/phone_number.ex
...
defstruct [country_code: "+1", :area_code, :number]
...
Then if we reload our module in IEx
, we’ll get a syntax error:
> r Teacher.PhoneNumber
** (SyntaxError) lib/phone_number.ex:3: syntax error before: area_code
This can be a gotcha when starting out with structs. When setting default values, the fields that we don’t want to default to nil
need to be included last.
So let’s go back to our module. And update our fields. Let’s also add a simple function that will return our struct for us. We’ll define a new public function named “new” and we’ll pattern match to get the “area_code” and the “number”. Then we’ll return a struct with those values. Let’s also add an alias for the current module, so we can call our struct without the prefix.
lib/phone_number.ex
defmodule Teacher.PhoneNumber do
@enforce_keys [:area_code, :number]
defstruct [:area_code, :number, country_code: "+1"]
alias __MODULE__
def new(area_code: area_code, number: number) do
%PhoneNumber{area_code: area_code, number: number}
end
end
Alright, we’ll go back to our IEx session and reload our module one last time. Then let’s use our new PhoneNumber.new
function to return our %PhoneNumber{}
struct. Great - it works and returns our phone number with the “area_code” and “number” that we provided and the “country_code” was set to our default value of “+1”.
Now at the beginning of the video I said that structs were specialized maps - we can confirm that, by using the Kernal.is_map
passing in our phone number and we see it returns true.
Because structs are maps, we can call Map functions on our struct. Let’s test it out by calling Map.keys
on our phone number struct. We see our expected “areacode”, “countrycode”, and “number” keys are returned. But there’s one we didn’t specify - “__struct ”. This is unique to structs and simply contains the name of the struct. So if we call it here, we see that PhoneNumber is returned.
> r Teacher.PhoneNumber
warning: redefining module Teacher.PhoneNumber (current version defined in memory)
> phone_num = PhoneNumber.new(area_code: "212", number: "555-1234")
%Teacher.PhoneNumber{area_code: "212", country_code: "+1", number: "555-1234"}
> is_map(phone_num)
true
> Map.keys(phone_num)
[:__struct__, :area_code, :country_code, :number]
> phone_num.__struct__
Teacher.PhoneNumber