Pattern Matching in Elixir

Pattern Matching in Elixir

·

5 min read

When I first started with Elixir, I was so used to imperative languages, I would think about how I would code something in one first and convert that to Elixir code. For example, here's a Java switch statement:

public static String bottlesPhrase(int n) {
  switch(n) {
    case 0:
      return "No more bottles";
    case 1:
      return "1 bottle";
    default:
      return n + " bottles";
  }
}

From here, I would convert to Elixir's equivalent - in this case, using Elixir's case statement:

@spec bottle_phrase(integer()) :: String.t()
def bottle_phrase(number) do
  case number do
    0 -> "no more bottles"
    1 -> "1 bottle"
    n when n > 1 -> "#{n} bottles"
  end
end

The 0, 1 and n to the left of the arrows are patterns. The last pattern (n) matches any integer from the argument number but has the guard n > 1 in the when clause. So when number is greater than 1, the function returns the result of interpolating the string #{n} bottles, where n = number. But what about when number is negative? None of them will match, so Elixir throws CaseClauseError.

In function arguments

A better way is to split it into smaller functions with pattern matching. Each clause is turned into one with the patterns as the arguments and focuses on a specific case. If we apply this to the earlier example, we get the following:

@spec bottle_phrase(integer()) :: String.t()
def bottle_phrase(0) do "no more bottles" end
def bottle_phrase(1) do "1 bottle" end
def bottle_phrase(number) when number > 1 do "#{number} bottles" end

Each case has now become its own function overload. The patterns narrow the down the range of values the functions take, so each one deals with a specific case. In doing so, we have broken down the original function into a few simpler ones. When the argument is a negative number, Elixir will throw a FunctionClauseError because none of the patterns will match.

In lists

My first attempt at writing a function for printing the first item in a list looked like this:

def print(contents) do
  List.first(contents)
  |> IO.puts
end

Notice the first step is getting the first item. If the list is empty, List.first returns nil and the function outputs a blank line - if we wanted to output a different string, we need to check its contents (i.e. if contents == "" ...). Turns out, pattern matching makes this easier! We can use it to get the first item from a non-empty list like this:

def print([first | _]) do
  IO.puts(first)
end

The pattern [first | _] means "put the first item of the list into the variable first and the rest of the list in _". _ denotes unused variables or values within the function - the function never uses them! Elixir will throw a CompileError if we try to. What happens when the list is empty? An empty list won't have anything to assign to first, so it won't match. Instead, to match an empty list use the empty list ([]) as the pattern, like this:

def print([]) do
  # List is empty
  IO.puts("Hey, you gave me an empty list!")
end

To print the entire list, we just need to add a recursive call:

def print([item | rest]) do
  # This runs as long as there is something in the list.
  IO.puts(item)
  print(rest)    # Print the rest of the list by recursion.
end

def print([]) do
  # No more items to print.
  IO.puts("No more items!")
end

Matching maps and structs

With maps or structs, one thing I found useful is the ability to match them based on their keys or fields. Imagine not having to say "check for a key" as the first thing in a method (although, you still need to consider the possibility in the overall code)! For example:

@spec get_name(map()) :: String.t()
def get_name(%{"name" => v}) do "Name is #{v}" end
def get_name(%{}) do "Map doesn't have entry for name" end

The first function matches any map with the key name and puts the value into the variable v. It then returns the interpolated string. The second function has an empty map for its parameter. Beware, this doesn't match just the empty map! Instead, it matches any other map that doesn't have the key (which is at odds with the empty list pattern, [], matching an empty list)! Matching an empty map requires a guard clause, like this:

def get_name(m) when m == %{} do "Map is empty" end

Elixir's structs are like Maps. We can match them based on their field. For example, to match a Product struct where its type field is orange:

def match_oranges(%Product{type: "orange", shop: shop}) do
  IO.puts "Shop #{shop} has oranges"
end

Reflections

Coming from an imperative language like Java and Python, using pattern matching to replace if and case with smaller functions seemed so different at first. However, the ability to split them into simpler functions is powerful. It helps to break down the complexity of a function. After spending time with pattern matching, I have even come to prefer it. I still mostly work with imperative languages. But now, I sometimes think how I would write something in the functional style first when I program in an imperative language.