Elixir: The Case for Pattern Matching

Pattern matching is a fundamental feature in Elixir, making your code efficient, readable, and intuitive and easing your cognitive load during development overall. If you’re looking for a way to replace the traditional if statements, pattern matching offers developers a smart and elegant way to do so.

©freepik.com

Pattern matching in Elixir works with diverse data structures. Be it dealing with lists, tuples, or maps … so, let’s take a closer look!

Pattern Matching: What it is and how to use it

Pattern matching in Elixir is the process of comparing a value against a pattern. It is used to destructure data structures and bind variables to their contents. Let’s look at some simple examples:

match?(1, 1)

This is probably the most simple (and boring) example of a pattern match. When executed in iex, the result is true.

match?/2 will „match“ the two parameters given to it, that is, determine if they are „the same“, and return true if that is the case and false otherwise. The interesting question is, of course, what „the same“ means. Let’s try something that surely is not “the same”.

And sure enough, 1 is not „the same“ as 2.
At its core, that is what pattern matching is. Compare two things and do something if they are „the same“.

Let´s continue with more complex examples:

Matching a Simple Tuple:

{a, b} = {1, 2} 
IO.puts(a) # Outputs 
1 IO.puts(b) # Outputs 2

In this example, the tuple {1, 2} is matched against the pattern {a, b}, binding a to 1 and b to 2.

Matching Head and Tail of a List:

[h | t] = [1, 2, 3] 
IO.puts(h) # Outputs 1 
IO.inspect(t) # Outputs [2, 3]

Here, [h | t] matches the head of the list to h and the tail to t.

Matching with Maps:

%{name: name, age: age} = %{name: "Alice", age: 30} 
IO.puts(name) # Outputs "Alice" 
IO.puts(age) # Outputs 30

This example matches the keys :name and :age in the map, binding their corresponding values to name and age, respectively.

With that in mind, a definition of what pattern and pattern matching are can be made:
A pattern describes the structure of data.
Pattern matching is the process of determining if a concrete value is structured as described by a given pattern.

The Power of Pattern Matching

©vectorpocket via freepik.com

Pattern matching is crucial in control flow structures like the case statement, allowing for more readable and maintainable code. The case statement evaluates the expression and matches it against various patterns. Here’s an example:

case {1, 2, 3} do
  {1, x, 3} -> "Matched: #{x}" 
  _ -> "No match" 
end 
# Outputs "Matched: 2"

In this example, the tuple {1, 2, 3} is matched against the pattern {1, x, 3}, binding x to 2 and returning „Matched: 2“. If the pattern doesn’t match, the fallback clause _ handles it, returning „No match“.

Match as Case

Pattern matching in Elixir can be used instead of conventional branching logic, such as if or case. This reduces complexity and enhances readability.

Consider this function, which deduces potential responses based on a user’s status:

def respond_to_user(%{status: :active}) do 
  "Welcome back!" 
end 

def respond_to_user(%{status: :inactive}) do 
  "Please activate your account." 
end 

def respond_to_user(_) do
  "Unknown user status." 
end

Instead of using a series of if-else statements, the function bodies with different patterns handle various cases based on user status.

Variable Binding

As mentioned above, variable binding is another powerful aspect of pattern matching. It binds values extracted from complex data structures directly to variables:

Matching a tuple:

{a, b} = {3, 4} 
IO.puts(a) # Outputs 3 
IO.puts(b) # Outputs 4

Binding elements from a map:

%{name: person_name, age: person_age} = %{name: "Alex", age: 25} 
IO.puts(person_name) # Outputs "Alex"
IO.puts(person_age) # Outputs 25

Pattern matching simplifies conditionally assigning variables and makes the code cleaner and more intuitive.

Monads Made Easy

©pixaflow via freepik.com

What is a Monad?

In programming, a Monad is a design pattern used to handle a variety of computational contexts, such as dealing with nullability, handling state, or consistently managing side effects. While Elixir does not have monads in the same way as some other functional languages (like Haskell), it offers constructs that achieve similar goals, namely, clean chaining of operations and handling of results.

Elixir’s with statement simplifies complex nested operations by allowing you to chain multiple pattern matches and assign operations sequentially. Let’s see an example:

Suppose you need to fetch user data, verify the user’s status, and authorize them – a typical flow in many applications. Here’s how you can do it using with:

with 
  {:ok, user} <- fetch_user(user_id),
  {:ok, verified_user} <- verify_user(user),
  {:ok, authorized_user} <- authorize_user(verified_user) do 
   {:ok, authorized_user} 
else 
error -> {:error, error} 
end

In this example:

  • fetch_user/1 attempts to fetch a user and returns either {:ok, user} or an error tuple.
  • verify_user/1 returns {:ok, verified_user} or an error tuple.
  • authorize_user/1 returns {:ok, authorized_user} or an error tuple.

If any of these functions return an error, the error -> {:error, error} clause in else will handle it. This construct allows you to sequence operations and make your code more linear and easier to follow.

Chaining Operations

Elixir’s pattern matching coupled with the with statement helps in chaining operations seamlessly. This ensures a smooth flow of data transformations without deeply nested code blocks.

Consider a scenario where you perform a series of operations on a list of numbers, such as doubling each number and then summing the results. You can chain these operations like so:

def process_numbers do
  numbers = [1, 2, 3] 

  doubled = Enum.map(numbers, fn x -> x * 2 end) 
  sum = Enum.reduce(doubled, 0, &(&1 + &2))
 sum
end

Alternatively, using the with construct here enhances clarity when dealing with intermediate steps or potential errors:

def process_numbers(numbers) do
  with 
    doubled <- Enum.map(numbers, &(&1 * 2)),
    sum <- Enum.reduce(doubled, 0, &(&1 + &2)) do
   sum 
  else
    _ -> {:error, "Processing failed"}
end

The with construct provides a structured yet flexible way to manage sequences of operations, ensuring explicit error handling and streamlined data flow.

Using with and chaining techniques help to keep Elixir code concise and improve readability, making complex logic more straightforward to manage and debug.

Functions in Elixir

©fabrikasimf via freepik.com

Multiple Function Clauses

One of the strengths of Elixir is its support for multiple function clauses, allowing developers to define clear, concise, and readable functions to handle different scenarios. This is often accomplished using pattern matching directly in function definitions.

Single Function Definition

Typically, functions are written to handle general cases, often resulting in complex nested conditionals. However, with multiple function clauses, each function variant can be as simple or complex as needed, directly addressing specific patterns.

Multiple Function Clauses with Pattern Matching

Elixir allows you to define multiple function clauses that match against different patterns. This feature is particularly useful when handling different cases explicitly without complex conditionals. Here’s an example of a function that greets users based on their status:

def greet(%{status: :active, name: name}) do
"Welcome back, " <> name <> "!" 
end 
def greet(%{status: :inactive, name: name}) do 
  "Please activate your account, " <> name <> "." end def greet(_) do "Hello, guest!" 
end

In this example:

  • The first greet function clause matches a map with the key :status set to :active and extracts the user’s name, returning a personalized welcome message.
  • The second greet function clause matches a map with the key :status set to :inactive and also extracts the user’s name, prompting them to activate their account.
  • The third greet function clause is a default case that matches any other inputs, providing a generic greeting.

Recursive Functions with Multiple Clauses

Elixir’s support for recursion works seamlessly with multiple function clauses. This combination simplifies loops and repetitive tasks. Consider a function that sums all elements in a list:

def sum_list([]) do   0 
end 
def sum_list([head | tail]) do
head + sum_list(tail)
end

In this example:

  • The first sum_list function matches an empty list and returns 0.
  • The second sum_list function matches a non-empty list, decomposes it into head and tail, and recursively sums the elements.

Error Handling with Multiple Function Clauses

Multiple function clauses can also streamline error handling by defining specific behaviors for different error cases. Consider a function that parses configuration options:

def parse_config(%{key: value}) when is_integer(value) do   {:ok, value} 
end 

def parse_config(%{key: value}) when is_binary(value) do 
  {:ok, String.to_integer(value)} 
end 

def parse_config(_) do 
  {:error, "Invalid configuration"}
end

In this example:

  • The first parse_config function clause matches a map with an integer value and returns it as {:ok, value}.
  • The second parse_config function clause matches a map with a string value, converts it to an integer, and then returns it.
  • The third parse_config function clause is a fallback that handles any other cases, returning an error tuple.

You can create efficient, clean, and expressive functions that naturally handle various cases in Elixir by utilising multiple function clauses. This makes code easier to read and maintain while adhering to functional programming principles. So let’s go!

You want to learn more about Elixir and what it has to offer? How about taking a look at Elixir Sigils?

Picture of Ariadne Engelbrecht

Ariadne Engelbrecht

Ari stieß 2022 zu uns und arbeitet seitdem als Software Engineer bei Inspired. Egal ob Theorie oder aktives Programmieren, sie verbringt ihre Zeit am Liebsten in der IT-Welt – z.B. (als Volunteer) auf Kongressen.

Picture of Alptuğ Dingil

Alptuğ Dingil

Alptuğ unterstützt uns bereits seit 2022 im Bereich Softwareentwicklung. Neben den Kundenprojekten sucht er immer wieder nach neuen Herausforderungen und beschäftigte sich in jüngster Zeit mit Kubernetes und der Konfiguration eines DIY-Clusters. Außerdem ließ er sich von Google als Professional Cloud Architect zertifizieren.

Bisherige Posts