· elixir

Notes on Elixir: Pattern-Matching Maps

Following on from my last post about pattern-matching, maps, the main key-value store in Elixir, have an interesting capability that sets them apart from other data structures with regards to pattern-matching.

A map can actually pattern-match on just a subset of a value. The key(s) in the pattern have to exist in the match, but the two structures don’t have to mirror each other in the same way a list or a tuple has to. For example:

iex(1)> [a, b] = [1, 2, 3]
** (MatchError) no match of right hand side value: [1, 2, 3]

iex(1)> {a, b} = {1, 2, 3}
** (MatchError) no match of right hand side value: {1, 2, 3}

iex(1)> %{:a => one} = %{:a => 1, :b => 2, :c =>3}
%{a: 1, b: 2, c: 3}
iex(2)> one
1
iex(3)> %{:a => one, :c => three} = %{:a => 1, :b => 2, :c =>3}
%{a: 1, b: 2, c: 3}
iex(4)> three
3
iex(5)> one
1
iex(6)> %{} = %{:a => 1, :b => 2, :c =>3}
%{a: 1, b: 2, c: 3}
iex(7)> %{:d => four} = %{:a => 1, :b => 2, :c =>3}
** (MatchError) no match of right hand side value: %{a: 1, b: 2, c: 3}

iex(8)> %{:a => one, :d => four} = %{:a => 1, :b => 2, :c =>3}
** (MatchError) no match of right hand side value: %{a: 1, b: 2, c: 3}

We can see that neither the list nor the tuple matches if the data structure of the pattern is different to the data structure of the match, namely the size. Whereas this is not the case for the map. Because the key is present in both the pattern and the match then the match is successful, regardless of the different sizes of the pattern and the match. An empty map will also match.

However the match will not be successful if the key in the pattern is not in the match. This is also the case even if there are matching keys. So any key used in the pattern has to be present in the match.

You’ll see this used extensively in the Phoenix Framework when dealing with the parameters passed to a function. The function is able to pick out of the parameters only those pieces of data it needs.

Here we have a function that creates a new user:

def create(conn, %{"user" => user_params}) do
  # do stuff with user_params
end

The map that is actually passed in as the second argument to this function is this:

Parameters: %{"_csrf_token" => "UR99GwILHTtzbSBUYRwmBVpdeDY/AAAA3K70jEiO9UhgPVwh+d3WYw==",
              "_utf8" => "✓",
              "user" => %{"name" => "Memphis Minnie",
                          "password" => "[FILTERED]",
                          "username" => "minnie"}}

The create function pattern-matches against only the "user" key, binding the associated value, in this case another map, to the user_params variable for use within the function. This function basically picks out the user info from the given parameters map, discarding the rest of the data as irrelevant to its needs.

It is also possible to match against the whole map at the same time as pattern-matching part of the map. Let’s change the create function to look like this:

def create(conn, parameters = %{"user" => user_params}) do
  IO.inspect user_params["name"]
  IO.inspect parameters["_utf8"]
  # do stuff with user_params
end

And we can see below when we use this function that we’ve matched parameters to the Parameters map that is passed in, and user_params to the the value of the "user" key in the Parameters map:

[info] POST /users
"Skip James"
"✓"

Interestingly, the parameters pattern-match can also be written the other way around:

def create(conn, %{"user" => user_params} = parameters) do
  IO.inspect user_params["name"]
  IO.inspect parameters["_utf8"]
  # do stuff with user_params
end

This is because the pattern here is actually %{"user" => user_params} = parameters and the match is the Parameters map being passed in. And when you’re inside a pattern you can also match different parts of the pattern, binding them to different variables. As far as I can tell, this is the more idiomatic approach, and what you will see most often.

Pattern-matching provides a really nice way to reach in and grab data out of a map key-value store.

Further Reading

  • LinkedIn
  • Tumblr
  • Reddit
  • Google+
  • Pinterest
  • Pocket