Mastering Pattern-Matching in Elixir
Published: 2024-01-08 21:27:54

Elixir is a functional programming language that is great for building scalable applications. Pattern matching is a powerful feature in Elixir.  At its core, pattern matching allows developers to destructure data and make decisions based on its structure. You can use pattern matching in many ways. It's not just for one thing; it's all over the language, making it a big and important part of Elixir programming.

## Prerequisite

To understand this article, you should have a basic knowledge of Elixir syntax.

## Basics of Pattern-Matching

For any Elixir developer, it's essential to understand the way pattern matching works. It's like the language's secret handshake for working with data powerfully and intuitively.

Let’s look at an example of how pattern-matching works:

```bash
iex(1)> user = {"Blessing", "bb@gmail.com", "Male"}
```

If you're used to languages like JavaScript or Python, you might think this is an assignment, but it's not. Here in Elixir, we're matching the variable `user` with the data on the right side. In pattern matching, the left side is like a template, and the right side is the actual data. If the matching works, the variable gets bound to the Elixir data. If not, it throws an error.

Take for example,

```bash
iex(1)> 2 = 1
```

You’d get an error because the Elixir expression, `2` doesn’t match the pattern on the right, `1`

```bash
(MatchError) no match of right hand side value: 1
```

Going forward, we will look at how to pattern match in:

- tuples
- lists
- maps
- structs
- functions
- case
- with

## Tuples

From our first example in this article, we can pattern-match the tuple and create 3 variables that are bound to the elements of the tuple:

```bash
iex(1)> {name, email, gender} = {"Blessing", "bb@gmail.com", "Male"}
{"Blessing", "bb@gmail.com", "Male"}
```

When the right expression is evaluated, the variables `name`, `email`, and `gender` are bound to the elements of the tuple.

```bash
iex(1)> name
"Blessing"
iex(2)> email
"bb@gmail.com"
iex(3)> gender
"Male"
```

This example works because we are matching a tuple that has exactly three elements. What happens if we increase/decrease the number of variables?

```bash
iex> {name, email, gender, unknown} = {"Blessing", "bb@gmail.com", "Male"}
** (MatchError) no match of right hand side value: {"Blessing", "bb@gmail.com", "Male"}
```

The match failed because the left-hand side is expecting a tuple of 4 elements but our Elixir expression only returns a tuple of 3 elements. An error will occur as long as the number of tuple elements on the left-hand side doesn’t match the right-hand side.

It is important to know that you can use underscore(`_`) to ignore values you are not interested in.

```bash
iex(1)> {name, _, _} = {"Blessing", "bb@gmail.com", "Male"}
{"Blessing", "bb@gmail.com", "Male"}
iex(2)> name
"Blessing"
```

## Lists

Pattern matching with lists also works the same way it works with tuples. Take an example:

```bash
iex(1)> [a, b, c, d] = [1, 2, 3, 4]
```

After the list expression is evaluated, the variables will be bound to the list elements.

We can go further by grabbing the first two values and saving the remaining as a list in another variable.

```bash
iex(1)> [a, b | others ] = [1, 2, 3, 4]
[1, 2, 3, 4]
iex(2)> a
1
iex(3)> b
2
iex(4)> others
[3, 4]
```

Just as tuple, an error will be thrown when we try to pattern match lists with different lengths.

```bash
iex(1)> [a, b, c, d, e] = [1, 2, 3, 4]
** (MatchError) no match of right hand side value: [1, 2, 3, 4]
```

An error is also thrown when we try to pattern match an empty list using the cons operator(`|`):

```bash
iex(1)> [a | others] = []
** (MatchError) no match of right hand side value: []
```

This is because we don’t have enough elements on the right-hand side.

## Maps

Pattern matching with maps is also quite straightforward:

```bash
iex(1)> %{name: name, email: email, gender: gender} = %{name: "Blessing", email: "bb@gmail.com", gender: "Male"}
%{name: "Blessing", email: "bb@gmail.com", gender: "Male"}
```

Just like in the previous sections, the variables `name`, `email`, and `gender` will be bound to the map keys. Unlike tuples and lists, the keys on the left side can take any order. The below also produces the same value as the above:

```bash
iex(1)> %{email: email, gender: gender, name: name} = %{name: "Blessing", email: "bb@gmail.com", gender: "Male"}
%{name: "Blessing", email: "bb@gmail.com", gender: "Male"}
```

We can also decide to match the value of a key without specifying other keys.

```bash
iex(1)> %{email: email} = %{name: "Blessing", email: "bb@gmail.com", gender: "Male"}
%{name: "Blessing", email: "bb@gmail.com", gender: "Male"}
iex(2)> email
"bb@gmail.com"
```

An error occurs if the expression doesn’t contain a key you are trying to pattern match:

```bash
iex(1)> %{name: name} = %{email: "bb@gmail.com", gender: "Male"}
** (MatchError) no match of right hand side value: %{email: "bb@gmail.com", gender: "Male"}
```

## Structs

Think of a [**struct**](https://hexdocs.pm/elixir/structs.html) as a specialized map with explicitly defined fields. Structs offer a way to organize and manage data with a predetermined structure. They are similar to maps, they are built on top of maps. Consider a `Person` struct that looks like this:

```bash
iex(1)> defmodule Person do
...(1)>   defstruct [:name, :email, :gender]
...(1)> end
```

When you pattern match a struct, it provides a basic security check. This ensures that you're only matching against the struct you've defined beforehand. Because a struct is an extension of a map, we can pattern-match a struct using a map pattern.

```bash
iex(1)> p_1 = %Person{name: "Blessing", email: "bb@gmail.com", gender: "Male"}
%Person{name: "Blessing", email: "bb@gmail.com", gender: "Male"}
iex(2)> %{name: name} = p_1
%Person{name: "Blessing", email: "bb@gmail.com", gender: "Male"}
iex(3)> name
"Blessing"
```

As you can see, the above didn’t throw an error even tho our pattern was a pure map. Depending on the use case of our application, the above might lead to some bugs. It’ll be better if our pattern is the struct we are expecting:

```bash
iex(1)> %Person{name: name} = p_1
....
iex(2)> name
"Blessing"
```

With this, we are sure that our name variable is coming from a `Person` struct.

Just like map, an error will also be thrown if we try to pattern-match a key that doesn’t exist.

## Functions

Pattern matching in functions helps us to execute a function based on their argument. Let’s look at a simple example:

```bash
iex(1)> test = fn
...(1)>   "Blessing" -> "My developer"
...(1)>   "Bikky" -> "Nurse"
...(1)>   _ -> "Unknown"
...(1)> end
```

In the above example, different results will be returned based on the argument passed to the function. The last pattern, underscore(`_`) will match any other value apart from `"Blessing"` and `"Bikky"`.

```bash
iex(1)> test.("Blessing")
"My developer"
iex(2)> test.("Bikky")
"Nurse"
iex(3)> test.("Olaleye")
"Unknown"
```

Pattern matching in functions also comes in handy when dealing with structs:

```bash
iex(1)> test = fn
...(1)>   %Person{} -> "known"
...(1)>   %{} -> "maybe"
...(1)>   _ -> "unknown"
...(1)> end
```

The above is basically saying if the argument:

- is created from our `Person` struct, the return `“known”`. e.g our `p_1` variable from the structs section
- is created from a plain `map`, then return `“maybe”`. e.g `%{alive: true}`
- is any other thing, then return `"unknown`. e.g `2`, `true` or `:thief`

## Case macro

Pattern matching in `case` macro looks like this:

```bash
iex(1)> case expression do
...(1)>   pattern_1 -> do_something()
...(1)>   pattern_2 -> do_something_else()
...(1)>   pattern_3 -> other_thing()
...(1)>   _ -> always_match()
...(1)> end
```

The `expression` gets evaluated to a valid Elixir value. The result is then pattern-matched against each of our patterns. The function of the first pattern that matches gets evaluated.

The last pattern(underscore) is called a catch-all. If none of the first 3 patterns matches, then the last pattern gets evaluated. Without it, an error is raised if none of the first 3 patterns matches.

## With/1

The [**with/1**](https://hexdocs.pm/elixir/Kernel.SpecialForms.html#with/1) special form lets you chain multiple expressions together.  Pattern-matching in `with/1` works almost the same way as others. The expressions provided to `with/1` continue to execute as far as the patterns are matched:

```bash
iex(1)> with {:ok, name} <- expression_1(),
...(1)>	     {:ok, email} <- expression_2(),
...(1)>	     {:ok, age} <- expression_3() do
...(1)>	 IO.inspect({name, email, age})
...(1)> end
```

Unlike others, `with/1` won’t raise an error if a pattern is not matched. It will rather stop executing. For example, if `expression_2()` returns `{:error, "no email"}`, `with/1` will stop execution and `expression_3()` will never run.

It’s common to provide an `else` block to handle error situations. Also, you can pattern-match the errors:

```elixir
iex(1)> with {:ok, name} <- expression_1(),
...(1)>	     {:ok, email} <- expression_2(),
...(1)>	     {:ok, age} <- expression_3() do
...(1)>	  IO.inspect({name, email, age})
...(1)> else
...(1)>	  {:error, "no_email"} -> IO.inspect("No email")
...(1)>   _ -> IO.inspect("Unknown issue")
...(1)> end
```

## Benefits of Pattern-matching

There are so many benefits to pattern-matching but we will be looking at a few:

### Code Readability

Pattern matching enhances code readability by matching patterns within data or control flow. Take for example, our `case/2` section:

```bash
iex(1)> case expression do
...(1)>   pattern_1 -> do_something()
...(1)>   pattern_2 -> do_something_else()
...(1)>   pattern_3 -> other_thing()
...(1)>   _ -> always_match()
...(1)> end
```

It’s clear what is happening when we have a particular result. It would be quite verbose if we had used nested `if`s.

### **Destructuring**

Pattern matching is the cleanest way to extract values from data structure and assign them to variables. It is even more useful when extracting values from nested data structures.

```bash
iex(1)> [%{name: name} | others] = [%{name: "Blessing", age: 12, alive: true}, %{name: "Olaleye", age: 98, alive: false}]
[%{name: "Blessing", age: 12, alive: true}, %{name: "Olaleye", age: 98, alive: false}]
iex(2)> name
"Blessing"
```

In the above, we extracted the name of the first person in the list.

### Error Handling

Pattern matching can be used effectively for error handling. We can define specific patterns for different error scenarios and handle them differently. An example is what we did in our `with/1` section:

```bash
iex(1)> with ...,
...(1)>	     ...,
...(1)>	     ... do
...(1)>	  ...
...(1)> else
...(1)>	  {:error, "no_email"} -> IO.inspect("No email")
...(1)>   _ -> IO.inspect("Unknown issue")
...(1)> end
```

## Practical Examples

### **Recursion**

Pattern matching proves invaluable when implementing recursive functions. Let's explore a `factorial` function example in Elixir.

```elixir
defmodule MathOperations do
  def factorial(0), do: 1

  def factorial(n), do: n * factorial(n - 1)
end
```

Pattern matching shines when we set the base case to stop the recursion, like when the value hits 0. In the following example, `factorial/1` uses pattern matching with two clauses. The initial clause matches when the argument is 0. This serves as the stopping point for the recursion. The second clause matches as far as the argument is not 0.

### Authentication

Let’s look at a real example from one of my [**projects**](https://github.com/Olaleye-Blessing/oja/blob/main/api/lib/api/controllers/accounts.ex#L54):

```elixir
1...def login(conn) do
2...  %{body_params: body_params} = conn

3...  login_params = %{
4...    email: Map.get(body_params, "email"),
5...    password: Map.get(body_params, "password")
6...  }

7...  with {:ok, _} <- validate_login_params(login_params),
8...       {:ok, user} <- Accounts.login(login_params) do
9...    authenticate_user(conn, user)
10..  else
11..    {:error, msg} -> Router.json_resp(:error, conn, msg, 401)
12..  end
13..end

14..defp validate_login_params(%{email: nil, password: nil}), do: {:error, "Please provide email and password"}
15..defp validate_login_params(%{email: nil, password: _}), do: {:error, "Please provide email"}
16..defp validate_login_params(%{email: "", password: _}), do: {:error, "Please provide email"}
17..defp validate_login_params(%{email: _, password: nil}), do: {:error, "Please provide password"}
18..defp validate_login_params(%{email: _, password: ""}), do: {:error, "Please provide password"}
19..defp validate_login_params(_params), do: {:ok, true}
```

In the above code,

- We first collect the credentials of a user (lines 2 to 6).
- Next, we apply **`validate_login_params/1`** to match various credential scenarios. This function uses pattern matching to handle cases like missing or empty email/password. It returns a tuple indicating success or an error message if validation fails.
- We then pattern-match the result of **`validate_login_params/1`** in the **`with`** construct (lines 7 to 12). If the validation succeeds (**`{:ok, *}`**),* we proceed to attempt user login using **`Accounts.login/1`**. If **`Accounts.login/1`** also returns **`{:ok, }`,** we call the **`authenticate_user/2`** function.
- In case of validation failure from any of **`validate_login_params/1`** or **`Accounts.login/1`**, we pattern match the error in the else clause.

This authentication example demonstrates how pattern matching is used to handle different scenarios in user login validation, making the code clear and concise.

## Conclusion

Pattern matching is a powerful tool for writing clean, concise, and efficient code. By mastering pattern matching, you'll unlock the full potential of Elixir and write effective programs with ease.