Jesse Leite

March 9th, 2026

Clause and Effect

At the end of the last post, I used a multi-clause function to demonstrate the power of pattern matching in Elixir. What are multi-clause functions though, and how is this even possible?

In many languages, a function is defined by name only, and you can only have one function with a given name per class or namespace.

For example, if you redeclare the same greet() function twice in PHP, it will blow up with a FATAL ERROR:

class Message
{
    public function greet(User $user): string
    {
        return "Welcome, {$user->name}!";
    }

    public function greet(Contact $contact): string
    {
        return "Welcome, {$contact->displayName}!";
    }
}

# FATAL ERROR - Cannot redeclare Message::greet()

As we've learned though, Elixir allows for pattern matching across multiple function clauses with the same name, where you can define the various shapes of data that a function can accept:

defmodule Message do
  def greet(%User{} = user) do
    "Welcome, #{user.name}!"
  end

  def greet(%Contact{} = contact) do
    "Welcome, #{contact.display_name}!"
  end
end

This is totally valid Elixir. When calling Message.greet(contact) with a %Contact{} struct as an argument, Elixir will use pattern matching to pass over the first %User{} clause, running the second clause instead.

As long as the shape of the arguments is distinct enough to pattern match without ambiguity, Elixir will treat both of these as valid compilable function clauses.

More than just the shape of the arguments, Elixir also considers the number of arguments in any given function definition. This is called arity, and you'll often see this referenced as function_name/arity:

# This is `greet/1` (because it has one argument)
def greet(%User{} = user) do
  "Welcome, #{user.name}!"
end

# This is `greet/2` (because it has two arguments)
def greet(%User{} = user, %Time{} = time) do
  cond do
    time.hour in 0..11  -> "Good morning, #{user.name}!"
    time.hour in 12..17 -> "Good afternoon, #{user.name}!"
    time.hour in 18..23 -> "Good evening, #{user.name}!"
  end
end

Now if we call greet(user, time) with two arguments, Elixir will know to run the second greet/2 clause because of function arity.

The important thing to note here is that a function's identity is both its name and its arity — so greet/1 and greet/2 are totally separate functions, not overloads of the same function. This mainly matters for control flow, but there are more uses for arity which we'll cover in a future post.

You can see how pattern matching and arity can really clean things up, but what about that nested cond expression inside my greet/2 clause? If we want to remove a layer of nesting here, we can extract to the top level using when guard expressions:

def greet(%User{} = user, %Time{hour: h}) when h in 0..11 do
  "Good morning, #{user.name}!"
end

def greet(%User{} = user, %Time{hour: h}) when h in 12..17 do
  "Good afternoon, #{user.name}!"
end

def greet(%User{} = user, %Time{hour: h}) when h in 18..23 do
  "Good evening, #{user.name}!"
end

Finally, maybe we also want to have a simple fallback greet/0 clause for when we don't have person or time arguments to pass:

def greet do
  "Greetings!"
end

Now why does all of this really matter? 👀

In PHP, if you wanted a greet() function to handle all of the edge cases we've touched on, you would have to get clever with nested conditions and guards inside your function, maybe something like this:

public function greet(User|Contact|null $person = null, ?DateTime $time = null): string
{
    if (! $person) {
        return "Greetings!";
    }

    $hour = $time?->format('G');

    if ($time && $hour >= 0 && $hour <= 11) {
        $greeting = "Good morning";
    } elseif ($time && $hour >= 12 && $hour <= 17) {
        $greeting = "Good afternoon";
    } elseif ($time && $hour >= 18 && $hour <= 23) {
        $greeting = "Good evening";
    } else {
        $greeting = "Welcome";
    }

    if ($person instanceof User) {
        $name = $person->name;
    } elseif ($person instanceof Contact) {
        $name = $person->displayName;
    }

    return "{$greeting}, {$name}!";
}

This honestly isn't so bad! You could even clean this up by extracting smaller getGreeting($time) and getName($person) helpers, but the complexity of logic required within a single greet() definition as an entry point remains.

Elixir, on the other hand, lets you flatten greet() into simpler, hyperfocused function clauses:

def greet(person, %Time{hour: h}) when h in 0..11 do
  "Good morning, #{name(person)}!"
end

def greet(person, %Time{hour: h}) when h in 12..17 do
  "Good afternoon, #{name(person)}!"
end

def greet(person, %Time{hour: h}) when h in 18..23 do
  "Good evening, #{name(person)}!"
end

def greet(person) do
  "Welcome, #{name(person)}!"
end

def greet do
  "Greetings!"
end

defp name(%User{} = user) do
  user.name
end

defp name(%Contact{} = contact) do
  contact.display_name
end

Using all of these multi-clause techniques together (pattern matching, arity, and guard expressions), our clauses are now:

  1. Easier to read
  2. Easier to extend
  3. Easier to test
  4. Easier for AI to reason about
  5. More token-efficient for LLMs

Don't. Sleep. On. Elixir. 😎

Thanks for reading!

For updates, follow me on Twitter / X or subscribe via RSS.

email-icon-emoji feed-icon-emoji linkedin-icon-emoji github-icon-emoji x-icon-emoji

© Jesse Leite