Jun 20, 2016
Table of contents:
A couple of weeks ago we looked at working with maps in Elixir.
Something that we didn’t cover, but you will come across are Structs. Structs are an extension of Maps that provide a number of interesting benefits.
In today’s tutorial we will be looking at Structs, how to use them, and what benefits they have over regular Maps.
As I mentioned in the introduction, a Struct is basically a Map, with many of the same semantics. However, Structs offer a number of interesting benefits over Maps for certain situations.
If you aren’t already familiar with Maps, I would go back and read working with maps in Elixir before continuing with this post.
As a quick refresher, here is how you would define a Map:
user = %{first_name: "Philip", last_name: "Brown"}
Here I’m creating a Map with two properties.
You can access the properties using dot notation:
user.first_name
Or access notation:
user[:first_name]
If you try to access a property that doesn’t exist with dot notation you will get an error:
user.age
# ** (KeyError) key :age not found in: %{first_name: "Philip", last_name: "Brown"}
However, if you try to access a property with access notation you will be returned nil
:
user[:age]
# nil
So as you can see, a Map is basically just a dictionary data type where you can specify a bag properties on the fly.
So if Structs are built on top of Maps, what’s a Struct?
A Struct is basically a Map, but you define the properties upfront:
defmodule User do
defstruct first_name: nil, last_name: nil
end
To create a new Struct, you use the defstruct
construct and pass it a keyword list of properties. The name of the Struct is taken from the Module in which it is defined. In this case User
.
The keyword list of properties can be set with default values. In the example above I’ve set the default values to be nil, but you could actually set defaults if you wanted to like this:
defmodule User do
defstruct first_name: nil, last_name: nil, active: false
end
So as you can probably already see, a Struct is basically a more defined Map because we know that certain properties will always be defined through the default values.
Lets take a closer look into working with Structs.
Now that we know how to define a Struct, let’s take a look at working with them.
On thing to note is, if you define the Struct from the last section in a file, you will need to load that file into iex
to continue with these examples.
You will get an error if you copy the “working with” examples into the same file because Elixir expects the Struct to be defined and used in two separate contexts.
However, if you define the Struct in a file, and then load it into iex
everything will work correctly.
To create a new Struct, use the following syntax:
user = %User{first_name: "Philip", last_name: "Brown"}
As you can see, to create a new Struct you simply pass the properties. If you have default values these will be overwritten.
However, you can’t provide extra properties that were not defined in the Struct definition:
user = %User{first_name: "Philip", last_name: "Brown", location: "UK"}
# ** (CompileError) iex:3: unknown key :location for struct User
You can access the properties of a Struct using dot notation:
user.first_name
However, you can’t use access notation like you can with Maps:
user[:first_name]
# ** (UndefinedFunctionError) undefined function: User.fetch/2
We will be looking at why this doesn’t work in more detail next week, but for now you just have to remember you can only access properties via dot notation.
You can access properties using pattern matching (Understanding Pattern Matching in Elixir) like you would with Maps:
%User{first_name: first_name} = user
first_name
# "Philip"
Here I’m using pattern matching to bound the first_name
property to the first_name
variable. Remember, pattern matching Maps does not require that you specify every key and so the same rule applies to Structs.
You can also update the Struct using the same technique as updating a Map:
user = %{user | first_name: "Bob"}
We’ve already covered most of the similarities and differences between Maps and Structs, but let’s just take a look at a couple more examples just so you have the background knowledge on how they are the same and how they are different.
If you pass a Struct into the is_map/1
function it will return true
:
is_map(user)
# true
You can also use the functions of the Map module to interact with a Struct.
user = Map.put(user, :first_name, "Elle")
However as I mentioned earlier, you can’t access properties via access notation:
user[:first_name]
# ** (UndefinedFunctionError) undefined function: User.fetch/
And you can’t use the functions from the Enum
module (Working with Enumerables and Streams in Elixir) because Structs are not enumerable:
Enum.unzip(user)
# ** (Protocol.UndefinedError) protocol Enumerable not implemented for %User{first_name: "Philip", last_name: "Brown"}
We will be looking into why this isn’t possible in more detail next week.
So now that you know what are Structs, how to work with them, and how they are the same and different to Maps, lets now try to understand the benefits of using Structs, and when you should prefer them over Maps.
Firstly Structs provide compile-time guarantees. When you are working with a defined Struct you know that the property you are expecting will be defined because the Struct definition requires it.
This is much different to Maps because a Map is just a bag of properties with no guarantees at all.
Secondly, Structs have strict access. When you use a Map with access notation you could be returned a nil
value. This might be your desired outcome, but often its a silent error that something has went wrong.
Structs are more strict with how you access the properties, so if you try to access a property that doesn’t exist on a Struct you will know about it straight away.
You should use a Struct over a Map whenever the data structure you are working with is a known type with properties that are defined up front.
For example, if you required a Location data type that always had a longitude and a latitude, then this would be a good situation to use a Struct instead of a Map because you know you will always require those two properties.
Structs are a very useful component of Elixir and you will see them used a lot. Although they look a lot like Maps, Structs allow you to define very important data structures.
Something that goes hand-in-hand with Structs is understanding Elixir Protocols, and so that will be the topic of next week’s tutorial.