Pattern matching in Elixir and why it's so great

One of the things I really like in Elixir is pattern matching. I find it both elegant as well as a very powerful concept. Here I want to show some of the things it can do. I started writing this when I saw someone on the fediverse being surprised that Elixir “has destructuring too”. But the fact is, Elixir doesn’t just have destructuring, it has pattern matching. Destructuring is just one of many powerful properties elegantly emerging from it. Here I want to go over pattern matching in Elixir in a high level way and hope to show why I think it’s so powerful and elegant.

Assignment

Let’s start with taking some simple line of code:

5 = 5

In most languages, the equal operator is considered an assignment. That makes the above code quite wrong since you can’t assign a value to another value. But in Elixir, this isn’t wrong. Why? Because the equal operator isn’t an assignment operator, it’s a pattern matching operator. It checks whether the right hand side “fits” the left hand side. In this example it does, so nothing is wrong.

One cool part of pattern matching, is that you can use it in place of an assignment operator. Let’s change our code a bit:

a = 5

In a typical other language you would now assign the value 5 to the variable a. What happens in Elixir is that it tries to match both sides of the operator. In this case, it can match, but it also implies that the variable a must be equal to the value 5 and thus we can use it as such.

That means that pattern matching isn’t something bolt on to assignments. It’s an intrinsic part of Elixir and assignment is a property emerging from it.

Deconstruction

We can also use pattern matching for deconstruction. Let’s take another example:

[a, b, c] = [1, 2, 3]

Again Elixir will try to match the right side to the left. It matches, but it also implies that a equals the value 1, b equals the value 2 and c equals the value 3. Again, this shows that deconstruction isn’t something bolted on to Elixir, it’s just an emergent property of pattern matching.

This is all very cool and all, but let’s see what else we can do with pattern matching.

Error control

Let’s say we have a function do_something() who either returns {0, some_value} or {1, some_error_message}. Now we can do the following:

{0, value} = do_something()

When it returns something like {0, "hooray, it worked!"}, it will match and the string "hooray, it worked!" will have matched on our variable value, which we can use further in our code.

But it can also return {1, "Oh no, it went wrong"}. Our code will fail to match, and you’ll get a MatchError.

Flow control

Another possibility of pattern matching is flow control. For example in a case statement we can do something like

case do_something() do
  {0, value} -> Log.debug(value)
  {1, error_message} -> Log.error(error_message)
end

And if we have several things we need to do who can all fail, we can do:

with
  {0, value} <- do_something(),
  {0, new_value} <- do_something_else(value),
  {0, final_value} <- do_another_thing(new_value)
do
  Log.debug(final_value)
else
  {1, error_message} -> Log.error(error_message)
end

There are other options as well in Elixir, but this should already give you a good idea of what’s possible here.

Assertion

You can also use pattern matching to assert that a value you get is the type of value you expect. For example, let’s say we defined a User data structure. we get a value from somewhere, but want to assert that the value we have is really this structure.

%User{} = user = get_user()

Note that we use the pattern matching operator multiple times on the same line. This isn’t a problem for Elixir, it will just try to match everything, if something doesn’t match, it will throw a MatchError. This also allows you to do things like

user = %User{username: username, userid: userid} = get_user()

Now there’s a variable user which contains the whole “user”. And there are also the variables username and userid who contain the username and userid of the user in question.

Function overloading

Now a really powerful thing with pattern matching in Elixir, is that you can also use it in your function parameters. Let’s say we have some data in a structure and we want to do something with it. We define a function normalise(object):

def normalise(object) do
  cond do
    %User{} = object -> Log.debug("We normalise a User object")
    %Activity{} = object -> Log.debug("We normalise a Activity object")
  end
end

Here we use pattern matching to do something different based on what type of structure we have. But because you can use pattern matching in your function parameters, we can also use it for function overloading:

def normalise(%User{} = object) do
  Log.debug("We normalise a User object")
end

def normalise(%Activity{} = object) do
  Log.debug("We normalise a Activity object")
end

This looks more compact and readable, doesn’t it. And as with previous properties we looked at, overloading isn’t yet another thing bolted on, it’s just an emergent property of pattern matching.

Putting it all together

And since all of this is really just pattern matching, you can have different properties in one go:

def normalise(%User{username: username, userid: userid} = object) do
  Log.debug("We normalise a User object")
end

def normalise(%Activity{activity_type: activity_type, activity_id: activity_id} = object) do
  Log.debug("We normalise a Activity object")
end

Here we have flow control, function overloading, assignment, destructuring and assertion all in one go, isn’t that just awesome!

Extra

I just went over some basics here in a very high level way. If you like to know more about Elixir and pattern matching, you’re probably better of reading the Elixir getting started guide. There’s more cool things in Elixir than just pattern matching, but I hope this article gave you some insights in why I think pattern matching is so powerful and elegant.

I also like to note that I used {0, value} and {1, error_message} as examples. Although Elixir often uses something similar, you’ll typically use atoms instead of integers. So more correct would be to use {:ok, value} and {:error, error_message}, but then I have to explain atoms in Elixir and that’s out of scope for this article.