Introduction to Pattern Matching

Meet the Match Operator “=”

The “=” sign in most programming languages means “assignment”. You are assigning some value to a variable. In Elixir, we don’t “assign” values to variables, we “bind” a variable to point to a value. That gives us our first hint that things are different here. In Elixir, we call “=” the “Match Operator”. Now you know it’s going to behave differently when it has such a different name!

Similar to operators like “+“, “-“, and “*” there is a left and right side of the Match Operator. The left side of the match operator is the pattern. The right side is the data being matched.

pattern = data

What Happens in a Match?

Three things that can all happen at the same time when a match is performed.

  1. Match the type of data
  2. Match the shape of data
  3. Bind values to variables

What Does “The Shape of Data” Mean?

We can think about the data in our systems having some basic shapes. The type of the data is a kinds of shape. An integer versus a string are different types, but we can also think of that as being different shapes. A map versus a list are also different types and shapes.

The “shape” of the data goes beyond the type. Within the same data type, we can think of our data as having more specific shapes as well.

For instance:

  • A list may be empty, have one item, or have more than one item. We can match on these characteristics and think of them as part of the “shape” of the list.
  • A map may have specific keys that we can use to identify the “shape” of the map.

Maps are commonly used data structures in Elixir, let’s play a bit more with matching a map.

Matching the Shape of a Map

Let’s see an example of doing those three things the “normal” non-pattern matching way. Let’s say the data we want to use is the following map.

data = %{name: "Howard", email: "howard@example.com"}

Now imagine I have a function that takes a given piece of data that may or may not be a map. I want to verify that it is a map, then that the map has the key :name and finally, bind a variable called name to that value in the map. The code might look something like this:

name =                               # result is bound to name
  if is_map(data) do                 # match type
    if Map.has_key?(data, :name) do  # match shape
      Map.get(data, :name)           # get :name value
    end
  end
  
name                      # name variable is bound to value
#=> "Howard"

I systematically “poke” the data blindly trying to feel out it’s shape. Once I know that it is a map and it has a :name key, then I can get that value and bind the name variable.

A Pattern Match match does this all in single statement. Instead of a series of “pokes” at the data, I say, “This is the type and shape of the data I want. If it matches, bind the value to a variable called name“.

%{name: name} = %{name: "Howard", email: "howard@example.com"}

name
#=> "Howard"

This is “declarative”. I state the conditions that must be met for it to match (the pattern) and if it matches, the value is bound!

If the type matches and the data’s shape matches, then it “pulls apart” the data by binding values to variables. This is also “destructuring” the data or “unpacking” it for us.

Thinking Tip: Elegance in communication

The imperative version is a series of instructions that accomplishes something similar to the pattern matching version but it is much less clear. The imperative version tells us how to do it but a developer reading then code is left to figure out what is being done.

Pattern matching gives us an elegance in communicating what is happening to the developer. It is clearer, simpler, and so much better!

Pattern matching helps us create code that avoids nested if statements. Our code becomes flatter, clearer, easier to read and easier to maintain.

The Simplest Match

The simplest version of a match is this:

x = 5
#=> 5

x
#=> 5

It doesn’t look like anything special. It looks like a normal variable assignment you see in other languages. However, the BEAM takes the statement and perform those three pattern matching steps for us:

  1. Does the type match? No type was specified, so “yes”.
  2. Does the shape match? No shape was specified, so “yes”.
  3. Bind the variable to the value.

After this statement, x is bound to the value 5. It skips right to binding because we didn’t provide a pattern to match.

Match Without Binding

You can perform a Pattern Match without binding a variable to a value. This is an example of that:

x
#=> 5

5 = x
#=> 5

If x has the value of 5, and we are matching the statement 5 = x then the BEAM performs the Pattern Match steps for us like this:

  1. Does the type match? The left side is an Integer and x is bound to an Integer, so “yes”.
  2. Does the shape match? The left side has the shape 5 and x is bound to a shape of 5, so “yes”.
  3. Nothing to bind. No variables were given on the left side.

The statement 5 = x is invalid in assignment-based languages but in Elixir, this is a valid match!

You may never have thought of the number 5 as a “shape” for data, but it is! You can think of the number 5 as a specific shape of an Integer.

Match Error

What happens when a match is not made? When we write a Pattern Match like 5 = 3, we are telling the BEAM, “This has to match!”. The expression doesn’t give the BEAM an alternative for what to do when it doesn’t match. We are basically asserting that it matches with the way the expression is written. When the BEAM has no other option, a Match Error is raised.

Here’s an expression that cannot match and results in a Match Error.

5 = 3
#=> ** (MatchError) no match of right hand side value: 3

The BEAM is saying, “You asserted that this matches and it doesn’t. Error!”

We will look at Pattern Match expressions that allow for elegant alternatives to a Match Error. In fact, those other ways are at the heart of what makes pattern matching so awesome!

The Pin Operator: ^

Sometimes we want to use a variable in a match without having the variable become bound to a value. To do this we use the “Pin Operator” ^. If you are familiar with pointer-based languages, I think of it as a “pointer dereference”. It’s simple enough to think of it as saying, “don’t bind me, use the value I point to for the match comparison” and it has a little arrow-like character ^ to help visualize it is pointing back to the value.

Let’s see what that looks like:

x = 5
#=> 5

^x = 6
#=> ** (MatchError) no match of right hand side value: 6

^x = 5
#=> 5

The Pin Operator can be used with every data type. It can be very useful in pattern matching.

Exercises

Here are some commands to try out in an IEx session. Play with what makes a match and what doesn’t. Get familiar with the Match Error message because while you are learning Elixir, you will likely encounter it a lot. As you become more comfortable, you will instinctively know what will match and you will encounter the error a lot less.

a = 1
#=> 1

a = 1 + 5
#=> 6

6 = a
#=> 6

5 = a
#=> ** (MatchError) no match of right hand side value: 6

3 = 2
#=> ** (MatchError) no match of right hand side value: 2

3 = 3
#=> 3

x = 5
#=> 5

^x = 3 + 2
#=> 5

^x = 3 + 5
#=> ** (MatchError) no match of right hand side value: 8

^x = x
#=> 5

:ok = :ok
#=> :ok

:ok = :error
#=> ** (MatchError) no match of right hand side value: :error

[1, 2] = [1, 2]
#=> [1, 2]

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

Limits to Matching

Pattern matching is everywhere in Elixir. It is a powerful and central language feature. There is a limit that is important to understand. You cannot execute a function on the left-hand side of a match.

length([10]) = 1
#=> ** (CompileError) iex:6: cannot invoke remote function :erlang.length/1 inside match

length([10]) == 1
#=> true

Note that the “==” or equality operator works as you’d expect.

Functions can create side effects. Examples of side effects:

  • Making an HTTP call to an external service
  • Modifying records in the database
  • Creating logging output
  • Writing to a file
  • Sending a message to another process that creates a side effect like modifying the database

The BEAM is trying to determine if the data on the left-side matches what is on the right. If it matches, then we may want to intentionally create side effects in our system. The process of performing a match is not allowed to create side effects. Because of this, no function calls are allowed on the left side of a Pattern Match statement.

Recognizing a Common Error

If you are coming from an Object Oriented language, you will likely accidentally see this error a lot in the beginning. A common mistake for developers new to immutable data structures is trying to mutate data. The result will be the error cannot invoke remote function [?] inside match. Let’s see how this happens.

user = %{name: "Jim"}
#=> %{name: "Jim"}

user.name
#=> "Jim"

user.name = "Howard"
#=> ** (CompileError) iex:3: cannot invoke remote function user.name/0 inside match

There are a few things happening with this code.

  1. The developer is trying to use the match operator “=” to perform an “assignment”. In effect, they are trying to “assign” the value “Howard” to the map’s name key. This isn’t how pattern matching binds variables.
  2. The developer is trying to mutate the map. Remember, data in Elixir is immutable. The change could be made with user = Map.put(user, :name, "Howard") or using the abbreviated update syntax of user = %{user | name: "Howard"}.
  3. The left side is actually a function user.name/0. Functions are not allowed on the left side of a match.

Why did the error say cannot invoke remote function user.name/0 inside match? Why did it say we were trying to execute a function on the left-side of the match operator?

In Elixir, the Access behaviour is a convenience that is available to a Map and other data structures. It turns our short-hand access of user.name into a function that effectively calls Map.get(user, :name) for us. Even though we didn’t create or call the name/0 function, the Access behaviour was executed for us, and that is how the function was called. So the left side of the match statement turned into a function call! Couple that with a misuse of the match operator and now you understand the error message.

Hopefully this helps you quickly recognize the error message you’ll see as you occasionally forget and fall into an old code habit and misuse the match operator. It’s okay. It takes time to establish new habits and new ways of thinking about something as common as the equals sign “=“.

Comments are closed

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

7 Comments

  1. Maksym Kosenko on October 14, 2021 at 3:21 pm

    I like the way the subject is explained: step by step, with caution to details! 🎠

  2. Abdulhamid Alsalman on June 14, 2022 at 2:16 am

    This tutorial series is the best tutorial series on Elixir I found on the web.

    • Mark Ericksen on June 14, 2022 at 12:38 pm

      Thanks!

  3. Larry Rix on October 25, 2022 at 6:59 am

    Please define the word “shape”. You never do, so the reader is left to decide for themselves (rightly or wrongly) what that definition is. Thank you! 🙂

    • Larry Rix on October 25, 2022 at 9:30 am

      Pattern Matching BNF-E? Below—I hack together what I think might be a sensible BNF-E Specification to describe pattern matching from the point of view of a parser/compiler. It is fair to note that even this does not “Shape” more than the course prose above. Therefore, I am left to think that the word “Shape” is really talking about the structure of data types like Maps and Lists, which appear to be collections of primitives like integer, float, string, and so on.

      NOTE: Read “≜” as “consists of” or “is defined as”. Also, BNF-E walks a list of general constructs down to terminals, where one can then present “specimen” (examples). See suggested BNF-E below.

      ===========

      Pattern_match¹ ≜ Pattern² Pattern_match_operator Pattern³

      ———–
      ¹ Match Shape and Type, and then Bind, if specified in “left” Pattern. Presume that Shape is defined as Structure—such that—containers like Maps, Lists, and Tuples having matching elements or items by either name (key lookup) or position and then a matching Type. Therefore, Shape is presumed to be determined first, then Type(s) and then Binding (if any).
      ² The “left” Pattern construct cannot be or contain a function.
      ³ However, the “right” Pattern can be or contain a function.
      ———–

      Pattern_match_operator ≜ “=”

      Pattern ≜ {Immutable_data | Function_call¹}+

      ———–
      ¹ See ² for Pattern_match, above.
      ———–

      Immutable_data ≜ {Data_type}+

      Data_type ≜ {Integer
      | Float
      | Map
      | Struct
      | String
      | Tuple
      | Boolean
      | etc}

      Function_call¹ ≜ Function_name [ [“(“] Arguments [“)”] ]

      ———–
      ¹ A Function_call always returns Immutable_data of some Data_type as a result, which makes a Function_call syntactically legal on the “right” side of the Pattern_match_operator.
      ———–

    • Mark Ericksen on October 28, 2022 at 3:42 pm

      Hi Larry! Thanks for the feedback on needing a definition for the “shape” of the data. I added a brief section titled “What Does “The Shape of Data” Mean?” to hopefully give move clarity.

      • Marcus West on June 24, 2024 at 2:38 pm

        The shape & type of the course are great Mark, it matches my present needs. (True, albeit v corny)

Comments are closed on this static version of the site.