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.

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:

  1. it tests the patterns going from top to bottom
  2. it breaks or stops on the first pattern that matches
  3. 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!

Thinking Tip: Be Specific!

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

Comments are closed

This is a static version of the site. Comments are not available.

9 Comments

  1. xrsia on May 8, 2020 at 4:32 am

    Excellent !!!

  2. Francisco Quintero on November 13, 2020 at 12:49 pm

    Awesome. I’m understanding more pattern matching.

  3. Maksym Kosenko on October 16, 2021 at 11:26 am

    Outstanding! 🎹 🎷 🎻 🎯

  4. Takudzwa Gwindingwi on October 26, 2021 at 11:08 am

    This is really helpful thanks.

  5. Juan Borrás on August 24, 2023 at 6:58 am

    When executing the following in `iex`:

    “`{iex}
    f = fn a -> case a do
    %{_a, _b, _c} -> “A tuple of size 3, so what?”
    _ -> “Uhh…. what’s that?”
    end
    end
    “`

    I get the following error: `** (CompileError) iex:24: expected key-value pairs in a map, got: _a`
    Am I running a too old Elixir-Erland/OTP combo? Elixir v1.12..2 on Erlang 24 erts 12.2.1

    • Mark Ericksen on August 24, 2023 at 7:12 am

      I think the issue is you put a “%” on the front, making the datastructure a Map, but it’s and invalid structure for a map. Remove the “%” for it to be a tuple.

      • Juan Borrás on August 24, 2023 at 8:20 am

        *insert-huge-facepalm-image-here*
        You were right on target. Working like a charm now.
        Thanks for the course and for your time.

  6. Marcus West on June 25, 2024 at 6:31 am

    Erm, I read that case do is not proper functional programming, and that we should use functional clauses.

    • Mark Ericksen on June 25, 2024 at 6:41 am

      Case statements are used a lot in Elixir. They are one of the easiest ways to go a pattern match whenever you need it. Multiple function heads that use pattern matching actually turn into case statements when compiled. There are mixed feelings in the community about piping directly into a |> case do. I think that’s what you may have been seeing.

Comments are closed on this static version of the site.