Pattern Matching a Function Body: Intro

We started playing with pattern matching using the case statement. This is valid and a very common tool. Now we really have fun when we start pattern matching with functions!

Matching with a Function

Pattern matching using a function declaration is really powerful! A big part of pattern matching is what happens when it doesn’t match? It goes on to the next pattern to check. So it is with functions. We can define multiple versions of a function where each one defines a different pattern. These are different clauses. You can think of it like overloaded functions, but each version is to handle a specific data type and shape. It makes more sense when we have some code to look at.

defmodule Testing do

  def do_work(true) do
    {:ok, "I did some work!"}
  end

  def do_work(false) do
    {:ok, "I refuse to work."}
  end

end

Paste that module into an IEx shell. Using tab completion, we can see there is only 1 function declared on our module.

Testing.do_work
#=> do_work/1

Remember the /1 means it takes 1 argument.

When we execute the function passing in true, pattern matching kicks in and the BEAM looks at the different function clauses we defined. It starts from the top and if the pattern matches, that version of the function gets executed.

Testing.do_work(true)
#=> {:ok, "I did some work!"}

Testing.do_work(false)
#=> {:ok, "I refuse to work."}

As defining functions goes, a function definition with types, shapes, and values in the function declaration looks weird! Honestly, until you understand pattern matching, even reading Elixir is confusing. I remember looking at Elixir code and not knowing how to mentally parse what it was doing! No where is that more true than with functions. When function declarations include patterns, they just don’t even look like “normal” code.

Until you understand pattern matching, even reading Elixir code is confusing.

Once I understood what was going on and how pattern matching in functions works, then I thought, “Why isn’t every language doing this?” The problem I had with reading Elixir code was because I had never seen pattern matching in a language before. That won’t be your problem now!

Thinking Tip: Function Overloading?

Is multiple function clauses the same thing as “function overloading“? Not really. Typically function overloading gets evaluated at compile time while pattern matching function clauses happens at runtime. The BEAM is comparing the data against the various function clauses. Beyond just the type of each argument, the BEAM can look deeper into the shape of the data for making the match.

Also, function overloading is often used in other languages to define a function with a different number of arguments. Remember, in Elixir functions are very much “arity” based. So a different number of arguments is a different function.

I don’t think it hurts to think of it as function overloading if that helps you, however, they are different things.

Single Line Function Clauses

A common thing you will see in Elixir code is writing function clauses as a single line when then are simple. This is what that looks like:

defmodule Testing do

  def do_work(true), do: {:ok, "I did some work!"}
  def do_work(false), do: {:ok, "I refuse to work."}

end

This becomes very clear and clean!

When writing a function clause this way, please note the def name(args), do: return_value syntax. There is no closing end for the function. And note the , before the do and that the do: makes it an atom. You will see this code a lot but it has some special little syntax differences to pay attention to.

When a Function Doesn’t Match

With the previous do_work/1 example, what happens if I pass in some data that doesn’t match the patterns I defined?

Testing.do_work("abc")
#=> ** (FunctionClauseError) no function clause matching in Testing.do_work/1    
#=>     
#=>     The following arguments were given to Testing.do_work/1:
#=>     
#=>         # 1
#=>         "abc"
#=>     
#=>     iex:3: Testing.do_work/1

It isn’t a MatchError because we aren’t using the Match Operator. It’s a FunctionClauseError. We defined a do_work function with two clauses. The error tells us that none of the clauses we defined match the data. The error is helpful because it shows us what the data looked like that it couldn’t match against.

Your Flip-the-Lid Clause

Our function clauses couldn’t handle the data "abc" because they were explicitly matching on the values true and false. What if you want the ability to catch everything else? For an if statement, we’re talking about the else clause. In Elixir, it’s the “everything else comes here” function clause.

You already know how to bind to a variable without specifying a type or shape for the pattern. It’s the same thing here! In this case, if we are given some data that we don’t know how to process, we’ll treat it as an error. Let’s see what that looks like:

defmodule Testing do

  def do_work(true), do: {:ok, "I did some work!"}
  def do_work(false), do: {:ok, "I refuse to work."}

  def do_work(other) do
    {:error, "I don't know what to do with #{inspect other}"}
  end

end

The addition of a new function clause that doesn’t define a pattern works perfectly for handling all the other data that can be thrown at our function without causing a FunctionClauseError. Let’s throw some data at it and see what it does!

Testing.do_work("abc")
#=> {:error, "I don't know what to do with "abc""}

Testing.do_work(1)    
#=> {:error, "I don't know what to do with 1"}

Testing.do_work(%{a_map: true})
#=> {:error, "I don't know what to do with %{a_map: true}"}

Testing.do_work([1, 2, 3])     
#=> {:error, "I don't know what to do with [1, 2, 3]"}

Order Matters!

The order of the clauses really matters! The pattern matching stops when the first match is found. The data is “captured” by the function. Even if a better or more exact match exists, it won’t be used if another function clause matches it first.

Let’s see what happens when we put the “flip-the-lid” version first…

defmodule Testing do

  def do_work(other) do
    {:error, "I don't know what to do with #{inspect other}"}
  end

  def do_work(true), do: {:ok, "I did some work!"}
  def do_work(false), do: {:ok, "I refuse to work."}

end

The first thing you’ll notice is we get 2 compiler warnings for the more specific function clauses.

warning: this clause cannot match because a previous clause at line 3 always matches
  iex:7

warning: this clause cannot match because a previous clause at line 3 always matches
  iex:8

The compiler can detect when a function clause will never be reached because the first clause doesn’t define a pattern. Try executing the function passing in true.

Testing.do_work(true)
#=> {:error, "I don't know what to do with true"}

When we call the function with data that has a perfect match, it doesn’t get executed! It is captured by the first match encountered. So order is very important. The order is checked goes top down.

Also note when a function has multiple arguments, it is possible to define function clauses where the compiler can’t tell it isn’t what we actually want. It won’t warn us in every situation. It is up to us to pay attention to the order in which we define our clauses.

Code Without Pattern Matching

You could easily write equivalent code without using pattern matching. That version of it might look like this.

defmodule Testing do

  def do_work(value) do
    if value == true do
      {:ok, "I did some work!"}
    else
      if value == false do
        {:ok, "I refuse to work."}
      else
        {:error, "I don't know what to do with #{inspect value}"}
      end
    end
  end

end

Testing.do_work(true)
#=> {:ok, "I did some work!"}

Testing.do_work(false)
#=> {:ok, "I refuse to work."}

Testing.do_work("abc")
#=> {:error, "I don't know what to do with "abc""}

While this code may feel familiar, I strongly encourage you to avoid the temptation to fall back into this style. The “Elixir way” is to prefer the use of a pattern matching over if conditionals. The code is flatter and easier to read. It values the pattern of the data over the explicit process of “poking” the data to figure out its shape.

Thinking Tip: See IF as an anti-pattern

Saying the Elixir way prefers pattern matching over if conditionals does not mean you can never use an if statement! There are valid cases for their use and they are part of the language! However, consider the use of if statements (especially an excessive use) as an anti-pattern in Elixir.

Compare the if conditional version above code to the pattern matching version. The Elixir-way is declarative, direct, clear, and much easier to reason about. At least it’s easier to reason about now that you understand pattern matching and function clauses!

Ready, Set, Go!

With the basics of pattern matching for the different data types covered and an introduction to using pattern matching with function clauses, you are ready to jump in and have fun!

Comments are closed

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

1 Comments

  1. Francisco Quintero on November 14, 2020 at 1:17 pm

    Thanks! Just what I needed. The person who recommended this course did it because I was so confused doing IF statements and not taking advantage of pattern matching. Now I see how wrong I was.

    Feels good to understand pattern matching better.

Comments are closed on this static version of the site.