Subscribe to access all episodes. View plans →

#106: Intro to Structs

Published November 11, 2019

Elixir 1.8

Elixir Structs


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

© 2024 HEXMONSTER LLC