Matching Complex Data Types
“Complex Data Types” refers to collection data structures. They include tuples, maps, structs and lists. Each type can contain other data structures either simple or complex.
Contents
Matching to Destructure Data
The Match operator is really powerful for pulling data apart or “destructuring” the data. This is what the “binding” step is doing. The BEAM can bind a variable to a location deep in a data structure just because it matched the pattern we described.
We’ll look at some examples of how this works for the different collection data types.
- Map
- Struct
- Tuple
- List
We’ll also look at how to do this with the Strings and binary types.
Case Statement
Part of the magic of pattern matching is that when it doesn’t match our specific pattern, it continues on to try the next pattern. To play with this, we’ll use the case
statement. Pattern matching is everywhere in Elixir and that is true for the case
statement as well.
Let’s look at how a case statement works.
case data_being_examined do
pattern_1 -> code_to_execute_when_matches
pattern_2 -> code_to_execute_when_matches
pattern_3 -> code_to_execute_when_matches
end
The patterns are checked from top to bottom. If it doesn’t match pattern_1
then it checks pattern_2
and so on. This looks a lot like a switch
statement in Javascript.
Three important things to remember:
- it tests the patterns going from top to bottom
- it breaks or stops on the first pattern that matches
- it has all the power of pattern matching (type, shape and binding)
Matching a Map
With a case
statement to play with, let’s start with a Map. Let’s look at an example using code you can paste into IEx.
data = %{name: "Howard", age: 35}
case data do
%{name: "Howard"} -> "Yes sir Mr. Admin!"
%{name: name} -> "Greetings #{name}!"
%{age: age} -> "I don't know who you are, but you're #{inspect age} years old!"
_other -> "Uhh.... what's that?"
end
#=> "Yes sir Mr. Admin!"
After pasting that code in, it returns "Yes sir Mr. Admin!"
. The match was for a map (type), with a :name
key and a value of "Howard"
(shape). That’s a very specific match! Also note, it stopped at the first match.
Let’s try sending a different piece of data though the case
statement and see what happens.
data = %{name: "Jill", age: 30}
case data do
%{name: "Howard"} -> "Yes sir Mr. Admin!"
%{name: name} -> "Greetings #{name}!"
%{age: age} -> "I don't know who you are, but you're #{inspect age} years old!"
_other -> "Uhh.... what's that?"
end
#=> "Greetings Jill!"
After pasting in the case
statement again, you should get a different result. The more specific pattern that included the name "Howard"
in the pattern didn’t match the data for "Jill"
. So the pattern match moved on to the next pattern and that one matched.
The 2nd pattern given is for a map (type) with a :name
key (shape) but we don’t specify what the value must be. We provide a name
variable to bind the value to and magically it is bound to the string "Jill"
and available for use in our code!
Now what will happen if the data being matched is the number 5
?
data = 5
case data do
%{name: "Howard"} -> "Yes sir Mr. Admin!"
%{name: name} -> "Greetings #{name}!"
%{age: age} -> "I don't know who you are, but you're #{inspect age} years old!"
_other -> "Uhh.... what's that?"
end
#=> "Uhh.... what's that?"
The first 3 patterns we specified are all maps. When it doesn’t match any of those, we can specify a “catch all” pattern that will match no matter what. You can think of this like an else
or default
in other switch
-style statements. This pattern doesn’t define a type or a shape, so it matches.
Order is Important!
We’ve already seen that the patterns match from top to bottom. Let’s make that really clear here because it is really important!
Returning to the first data example with Howard, we’ll rearrange the patterns.
data = %{name: "Howard", age: 35}
case data do
%{name: name} -> "Greetings #{name}!"
%{name: "Howard"} -> "Yes sir Mr. Admin!"
%{age: age} -> "I don't know who you are, but you're #{inspect age} years old!"
_other -> "Uhh.... what's that?"
end
#=> "Greetings Howard!"
The less specific pattern that doesn’t match on the value of the name is now higher. The pattern matches and the result is "Greetings Howard!"
even though a more specific and “better” match exists. It stops at the first match!
At the risk of over emphasizing this point, lets rearrange the patterns one more time. Let’s move the other
pattern to the top.
data = %{name: "Howard", age: 35}
case data do
_other -> "Uhh.... what's that?"
%{name: "Howard"} -> "Yes sir Mr. Admin!"
%{name: name} -> "Greetings #{name}!"
%{age: age} -> "I don't know who you are, but you're #{inspect age} years old!"
end
#=> "Uhh.... what's that?"
In this example, we can see that multiple other patterns are “better” matches for the data than the “other
” pattern. It doesn’t matter though, the first pattern to match gets the data!
Try to start with the most explicit or specific pattern you can. These are often the “edge cases”.
Using the underscore to define shape
A significant part of pattern matching is defining the “shape” of the data. Not just the values or attributes, but the very shape itself. To help with this, we can use the underscore character “_
” to help define a shape that lets us ignore the value.
%{name: _}
This defines a pattern for a map that has an atom key of “:name
“. Because of the _
, we skip the “binding” step and we also don’t specify a value it should have. This lets us be more specific about the shape of the data that we care about for this pattern.
Named for Developer Clarity
We can give a name to the ignored space that helps for developer clarity. It doesn’t impact the usage or value, but it does convey meaning and intent to a developer.
%{name: _username} = data
%{name: _company_name} = data
%{name: _pet_name} = data
All 3 of the patterns describe the same shape. The difference is in the named placeholder. This doesn’t change the shape but it is certainly helpful to another developer who comes across the code later!
Tuples and Shape
Tuples are very specific about their shape. A two element tuple is completely different from a three element tuple. Using the _
underscore is very helpful for specifying the shape of the pattern.
{_, _, _}
This defines the pattern for a 3 element tuple. It doesn’t matter about the “values” in the tuple, but it defines the “shape” as being a tuple with 3 elements.
Naming the placeholders for developer clarity is really beneficial here. The following pattern still defines a 3 element tuple. However, it gives much more meaning to the positions!
{_year, _month, _day}
Deeper Data Matches
If you’ve ever dealt with pulling data out of a deeply nested structure, then you’ve felt the pain of deeply nested if
statements. With pattern matching, you can handle pulling out deep data much more elegantly. In this example, our data is a map with two deeper nested maps. The value we want in on the very inside map, but only if the important_flag
value is set to true
.
data = %{
important_flag: true,
level_1: %{
other: "stuff",
level_2: %{
value: 123,
more: "stuff"
}
}
}
case data do
%{important_flag: false} -> {:ok, 0}
%{important_flag: true, level_1: %{level_2: %{value: value}}} -> {:ok, value}
_other -> {:error, "Invalid data"}
end
#=> {:ok, 123}
Try changing the shape of the data and run it against the case
statement to see how it behaves.
Nesting Data Types
Pattern matching works as you nest different data types inside each other. Imagine a function that looks up a user by some search criteria. If it finds the user it is returns an {:ok, user}
tuple.
In this example, we can match the two element tuple (type and shape), the first element is an :ok
atom (shape), the second element is a map (type), the user :email
key (shape) and finally bind the email variable to the value. This is a very common pattern!
user = %{id: 1, name: "John", email: "john@example.com", active: true}
function_call_result = {:ok, user}
case function_call_result do
{:ok, %{email: email}} -> "Sending email to: #{email}"
_other -> "Nothing to do"
end
#=> "Sending email to: john@example.com"
When you start to break it down, there’s a lot happening here. But I don’t have to think about it. I declare the pattern I’m looking for and when it matches, the instructions to perform are executed. It’s declarative and clean. The Pattern Match works both for conditional logic and flow control.
Recap
Here we’ve covered working with maps and tuples in pattern matches. The point worth understanding is the following things apply to all data types, not just maps and tuples.
- The
case
statement can match any data type - Order in pattern matches is very important
- The first pattern to match is the one that’s used
- The
_
can be used as a placeholder to define a pattern’s shape - Matching and binding to deeply nested data works on all types
9 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.