Why I Love Elixir

2020-03-26

I often talk about the Elixir programming language to friends, families, and colleagues.

While there are TONS of features in elixir that I think are fantastic... off the top of my head:

Note: maybe I'll revisit each of these in a follow up post if there's enough interest

While some of the features above make Elixir a more compelling sell to upper management and clients - one feature not listed stands out to me as one of the most fun to use.

That feature is function head pattern matching.

Function Head Pattern Matching

This feature is simply outstanding.

The beauty of this feature comes out more in examples than it does in explanations.

Here's a brief explanation though before we get started:

For a language to have function head pattern matching it must allow users to define multiple function bodies for a single function. Each of these function bodies is gated behind a pattern and a set of guard clauses. When the function is called the function definitions are tried one at a time in order of definition until one matches. Once a match is found the code in that function body is run with the given parameter set.

Still not clear? Let's look at examples.

First examples

Function head pattern matching is based on the concept that multiple function definitions can represent a single function.

Consider the absolute value function

abs(x) = {
  x > 0, x
  x < 0, -x
  0, 0
}

In Elixir code this looks like this:

def abs(n) when n > 0 do n end
def abs(n) do -n end

abs(1)
> 1

abs(-1)
> 1

The when n > 0 statement represents a guard and the n in the parenthesis represents the pattern. The pattern here does not have any restrictions other than an arity of one. Note: Having an arity of one simply means the function accepts one parameter

The when clause limits the domain of each function clause.
In the example above when n > 0 limits the domain of the first function body to positive numbers.

Function head pattern matching allows the parameters provided in a function declaration to act not only as a set of variable declarations, but also as a pattern restricting the domain of a function.

Here's an example of that.

The following function has a domain of [1, 2] and returns either the atom :one or the atom :two.

Note: you can think of atoms like strings for now

def one_or_two(1), do: :one
def one_or_two(2), do: :two

This function definition consists of two bodies - each of which having a pattern specified by a single integer.

When the function is called the function will attempt to match the given parameters to each function definition clause in order.

one_or_two(1)
# > "one"
one_or_two(3)
# > ** (FunctionClauseError) no function clause matching in one_or_two/1
# The following arguments were given to Test.one_or_two/1:
    # 1
#     3

If no clause matches Elixir will raise a FunctionClauseError.

Fibonacci

Ah Fibonacci, a very common example function used in programming. Let's just use the naive approach to keep things simple and about the code.

Let's examine the mathematical definition:

fib(x) = {
  0: 1,
  1: 1,
  x: fib(x-1) + fib(x-2)
}

Not too ugly, now let's look at a Javascript implementation:

Javascript

function fib(x) {
  if (x == 0) {
    return 1;
  } else if (x == 1) {
    return 1;
  } else {
    return fib(x-1) + fib(x-2);
  }
}

Not too bad either, mainly due to the fact that the function itself is quite trivial.

Let's look at Elixir's

Elixir

def fib(0), do: 1
def fib(1), do: 1
def fib(n), do: fib(n-1) + fib(n-1)

Between Javascript and Elixir - which function do you find more readable? I myself find Elixir's quite elegant - function head pattern matching Elixir makes it exceptionally clear which clauses operate on which values. This reduces cognitive load when attempting to decipher functions.

Let's look at another example.

Heres an example function that determines if a number is single digit.

Java

public static boolean isSingleDigit(int x) {
    if (x > -10 && x < 10) {
        return true;
    } else {
        return false;
    }
}

Elixir

def is_single_digit(x), when: x in [-10..10], do: true
def is_single_digit(_),                       do: false

Which function do you have an easier time deciphering? For the same reason referenced above I find the Elixir version here easier to trace.

Let's consider a real world example.

Find the first item matching a set of conditions

This is an example I personally face all the time in the real world.

Imagine we have a type, User.

User {
  banned: boolean,
  name: String,
  // etc...
}

These users are granted "access" to some sort of thing based on many criteria. The first of which is if the user is banned.

To start - we will have two criteria:

  • the user is not explicitly banned
  • the user is not named "Zack"

Let's check out a java implementation.

Java

@AutoValue
class User {
  abstract boolean banned();
  abstract String name();
  // ... imagine some other values
}

class AccessAllower {
  private boolean accessAllowed(User user) {
      if (user.banned()) {
        return false;
      }
      if (user.name().equals("Zack")) {
        return false;
      }
      return true;
  }
}

This type of function extremely common in most industrial code bases. Let's examine how Elixir handles these sorts of functions. Elixir maps are represented by %{}. Elixir keys and values can be matched on in function head.

Elixir

defmodule User do
  defstruct [:banned, :name]
end

defmodule AccessAllower
  defp access_allowed?(%{banned: true}), do: false
  defp access_allowed?(%{name: "Zack"}), do: false
  defp access_allowed?(_),               do: true
end

This is already much clearer than the java version - but let's imagine requirements keep getting MORE complex (as the case is).

As requirements evolve our code scales extremely well.

Let's imagine we now want to reject users named AnnMarie. In Elixir this is as easy as:

Elixir

defmodule AccessAllower
  defp access_allowed?(%{banned: true}),     do: false
  defp access_allowed?(%{name: "Zack"}),     do: false
  defp access_allowed?(%{name: "AnnMarie"}), do: false
  defp access_allowed?(_),                   do: true
end

or what if we want to ban users whose names are longer than 33 characters?

Elixir

defmodule AccessAllower
  defp access_allowed?(%{banned: true}),                     do: false
  defp access_allowed?(%{name: "Zack"}),                     do: false
  defp access_allowed?(%{name: "AnnMarie"}),                 do: false
  defp access_allowed?(%{name: name}), when: len(name) > 33, do: false
  defp access_allowed?(_),                                   do: true
end

It's trivial to add new criteria - what if we want to whitelist banned users if they are named "Luke" and make them ban proof?

Elixir

defmodule AccessAllower
  # Whitelist
  defp access_allowed?(%{name: "Luke"}),                     do: true
  # False cases
  defp access_allowed?(%{banned: true}),                     do: false
  defp access_allowed?(%{name: "Zack"}),                     do: false
  defp access_allowed?(%{name: "AnnMarie"}),                 do: false
  defp access_allowed?(%{name: name}), when: len(name) > 33, do: false
  # Fallthrough
  defp access_allowed?(_),                                   do: true
end

As you can see - adding these cases is really easy. The code is still super easy to trace and reason about.

Let's check in over in Java land.

Java

class AccessAllower {
  private boolean accessAllowed(User user) {
    if(user.name().equals("Luke")) {
      return true;
    }

    if (user.banned()) {
      return false;
    }
    if (user.name().equals("Zack")) {
      return false;
    }
    if (user.name().equals("AnnMarie")) {
      return false;
    }
    if (user.name().length() > 33) {
      return false;
    }

    return true;
  }
}

To me - this is much harder to follow.

In enterprise software, requirements change constantly. Function head pattern matching allows code to remain readable as the number of special cases handled grows.

Summary

I love function head pattern matching.

It allows us to write code that is:

  • easier to trace mentally
  • more closely represent mathematical function definitions
  • remain readable when requirements grow

While function head pattern matching isn't a compelling argument to make to upper management on picking Elixir as a primary language - it is a nice bonus to using Elixir and is my personal favorite language feature.