Pattern Matching a Function Body: Struct
Matching with a struct is just like matching with a map. The main differences with structs are:
- the keys are always atoms
- the compiler tells us if we get a key wrong
- the struct type gives us another thing to match against
In fact, you can write the pattern matching solutions to the tests using only maps! With the test code available, this is a good chance to explore and understand how this works and why there is value in using structs when you can.
The project defines two structs for us to play with here. Feel free to check them out.
lib/pattern_matching/customer.ex
lib/pattern_matching/user.ex
You can also see a description of the structs in IEx. Start the IEx using iex -S mix
to load the project into the IEx session. Using the IEx helper t()
, it can describe a type for us.
$ iex -S mix
Erlang/OTP 21 [erts-10.0.6] [source] [64-bit] [smp:4:4] [ds:4:4:10] [async-threads:1] [hipe]
Interactive Elixir (1.8.1) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> t PatternMatching.Customer
@type t() :: %PatternMatching.Customer{
active: boolean(),
contact_name: nil | String.t(),
contact_number: nil | String.t(),
location: nil | String.t(),
name: String.t(),
orders: list()
}
iex(2)> t PatternMatching.User
@type t() :: %PatternMatching.User{
active: boolean(),
admin: boolean(),
age: nil | integer(),
gender: nil | :male | :female,
hair: nil | String.t(),
name: String.t(),
points: integer()
}
There are some similarities between the Customer and User structs. They both have :name
and :active
keys. All the other fields are different.
Contents
Compile Time Checks
A benefit of using struct types in Elixir is that you get compile-time checks that the keys are correct.
When we use IEx, the Elixir commands we enter are interpreted at runtime, they aren’t compiled at build time. We still get errors for incorrect structs, but they look different. Let’s see an example of both:
Example: IEx – Interpreted. Started using iex -S mix
.
alias PatternMatching.User
user = %User{car: "Toyota"}
#=> ** (KeyError) key :car not found
#=> (pattern_matching) expanding struct: PatternMatching.User.__struct__/1
#=> iex:2: (file)
This is a runtime KeyError
trying to use a key that doesn’t exist on the struct.
Example: Compiled. Assumes you modify the code to be invalid and then try to start the project, which compiles it in the process.
$ iex -S mix
Erlang/OTP 21 [erts-10.0.6] [source] [64-bit] [smp:4:4] [ds:4:4:10] [async-threads:1] [hipe]
Compiling 1 file (.ex)
== Compilation error in file lib/pattern_matching/structs.ex ==
** (CompileError) lib/pattern_matching/structs.ex:9: unknown key :car for struct PatternMatching.User
lib/pattern_matching/structs.ex:9: (module)
This is helpful in catching problems ranging from a simple typo to changing a struct definition where a key was renamed or removed.
However, if I perform the match as a map instead of the struct, I have no protections or guarantees. This alone is a significant benefit. It’s the difference of including the struct name or not.
# This version is compile-time checked for Customer keys.
def do_work(%Customer{name: name}) do
# work
end
# This version cannot be checked.
def do_work(%{name: name}) do
# work
end
Guarantee it is the Struct
When a struct is part of the Pattern Match, it is a guarantee that the data coming in is that struct. Not just a map with similar keys, but it is that struct. All the code inside that function clause can be written confidently knowing it can’t be something else with a similar structure.
It is totally valid to define a function clause that matches the struct just to have that assurance and protection. Here’s an example:
def do_work(%Customer{} = customer) do
# work
end
Inside this function clause I am guaranteed to have a Customer struct. This is opposed to a similar function declaration without it.
def do_work(customer) do
# work
end
This function calls the argument customer
but there is no guarantee it is a customer. The code would likely be written with the assumption it is a Customer struct but it implicitly depends on all callers to always only pass in a customer struct. When this function is passed something other than a customer, it will likely result in runtime errors.
Protects from Misspellings
Explicitly using a struct in a Pattern Match helps protect against misspelled keys. In the following example, I don’t use the struct type but instead use a map. I’ve accidentally misspelled name
as nam
.
def do_work(%{nam: name} = customer) do
# Will never match a %Customer{}!
end
Calling this function and passing in a Customer struct won’t error, it will just never match! I’ve described a pattern that cannot match a Customer. When the Customer struct type is included, I get a compilation error for the invalid key.
Keep in mind that it is completely acceptable to intentionally use a map to match a struct. In this way my code can be somewhat polymorphic. I match on attributes of the data that multiple types can share. However, if I’m expecting the data to be Customer type, then it is preferable to explicitly declare that.
Practice Exercises
The following exercises continue using the Pattern Matching project. We will continue focusing on making a single test pass at a time.
The tests we are focusing on are in test/structs_test.exs
. Running the following command will execute all the tests in this file. Running all the tests now will show they all fail.
Please review the solutions below even if you feel confident with your answer. You may find additional insight in the explanations.
$ mix test test/structs_test.exs
[...]
Finished in 0.05 seconds
9 tests, 9 failures
Randomized with seed 958439
Exercise #1 – Structs.get_name/1
In this exercise you write the function get_name/1
that takes both a User
and Customer
struct.
There are 2 tests for this function. One is the happy-path solution and the other handles when some other data type is given.
mix test test/structs_test.exs:25
mix test test/structs_test.exs:30
Make the tests pass by using pattern matching in the function declaration.
Exercise #2 – Structs.create_greeting/1
In this exercise you write the function create_greeting/1
that handles receiving a User
and Customer
struct differently.
There are 2 tests for this function. One is the happy-path solution that handles creating a customized greeting for a User
or Customer
struct. The other handles when the User
or Customer
being greeted is inactive.
mix test test/structs_test.exs:38
mix test test/structs_test.exs:45
Make the tests pass by using pattern matching in the function declaration.
Exercise #3 – Structs.deactivate_user/1
In this exercise you write the function deactivate_user/1
that handles receiving a User
struct, modifying it and returning the modified struct.
There are 2 tests for this function. One is the happy-path solution that handles creating an updated User
struct. The other handles when something other than a User
is passed in.
mix test test/structs_test.exs:55
mix test test/structs_test.exs:60
Make the tests pass by using pattern matching in the function declaration.
15 Comments
Comments are closed on this static version of the site.
Comments are closed
This is a static version of the site. Comments are not available.