May 23, 2016
Table of contents:
Last week we looked at pattern matching, how it differs from assignment, decomposing data structures, and matching against the return value of a function call.
One of the most important uses of Pattern Matching in Elixir is matching the arguments of a function.
By pattern matching the arguments of a function, Elixir allows us to overload function calls and use multiple definitions of the same function based upon the arguments it was called with.
In today’s tutorial we will be looking at multi-clause functions with pattern matching and guards in Elixir.
If you remember back to Working with Functions and Modules in Elixir, a function can be defined like this:
defmodule Speak do
def greet(first_name, last_name) do
IO.puts("Greetings #{first_name} #{last_name}!")
end
end
Here I’ve defined a greet
function that accepts two arguments for first_name
and last_name
.
Behind the scenes, the arguments in the function definition are being pattern matched.
An alternative way of defining this function could be to use a Tuple:
defmodule Speak do
def greet({first_name, last_name}) do
IO.puts("Greetings #{first_name} #{last_name}!")
end
end
In this example the Tuple is decomposed into the two variables and so the output is exactly the same.
If you don’t provide a two element Tuple to the function you will get an error:
Speak.greet({"Philip", "Brown", "Hello"})
# ** (FunctionClauseError) no function clause matching in Speak.greet/1
# speak.ex:2: Speak.greet({"Philip", "Brown", "Hello"})
One of the most interesting characteristics of Elixir is the fact that you can overload a function by specifying different clauses to match against.
When you provide multiple clauses for a function with the same arity, this is known as multi-clause functions. We looked at the importance of function arity in Understanding Function Arity in Elixir.
A clause is basically just the pattern that is matched against when calling the function.
This allows you to define the function with different clauses. The function that is called depends on which matches the argument you have provided.
For example:
defmodule Speak do
def greet({:formal, first_name, last_name}) do
IO.puts("Greetings #{first_name} #{last_name}!")
end
def greet({:informal, first_name, last_name}) do
IO.puts("What's up #{first_name} #{last_name}!")
end
end
In this example I’ve defined two clauses for the Speak.greet/1
function. The function that is called is based upon the argument that is provided:
Speak.greet({:formal, "Philip", "Brown"})
# Greetings Philip Brown!
Speak.greet({:informal, "Philip", "Brown"})
# What's up Philip Brown!
As you can see, when the first definition is matched, the formal greeting is displayed, but when the second definition is matched the informal greeting is displayed.
When Elixir receives the functional call, it will work through the function definitions from top to bottom until a match is found.
If no match is found, a FunctionClauseError
will be thrown.
An important thing to note is that we still only have a single Speak.greet/1
function. You can’t explicitly call any of the specific functions directly. Instead you need to pass a pattern that matches that definition.
Due to the fact that we’re simply doing pattern matching here, you can very easily provide a catch-all definition that will act as a default fallback clause:
defmodule Speak do
def greet({:formal, first_name, last_name}) do
IO.puts("Greetings #{first_name} #{last_name}!")
end
def greet({:informal, first_name, last_name}) do
IO.puts("What's up #{first_name} #{last_name}!")
end
def greet(unknown) do
{:error, {:unknown_greeting_type, unknown}}
end
end
When calling the greet/1
function with a pattern that does not match either of the first two definitions, Elixir will fallback to the final definition because Elixir will always match a variable:
Speak.greet({:evening, "Philip", "Brown"})
# {:error, {:unknown_greeting_type, {:evening, "Philip", "Brown"}}}
When pattern matching the arguments of a function, you will often require a more sophisticated way of defining the expectations of the definition.
To make the clause more specific, you can also provide a when
clause to the definition that must be satisfied for the definition to be used.
For example, lets take a look at this Module:
defmodule Types do
def check(x) when is_atom(x) do
IO.puts("It's an atom!")
end
def check(x) when is_binary(x) do
IO.puts("It's a string!")
end
def check(x) when is_number(x) do
IO.puts("It's a number")
end
end
This will print a message based upon what type was given to the check
function:
Types.check(:atom)
# It's an atom!
The combination of multi-clause functions and pattern matching is a big reason why I found Elixir so compelling when I first started to learn the language.
In my previous programming experience, I had never come across these techniques. When you learn something new that totally changes how can write code, it’s like opening up the Matrix.
We’ve only really scratched the surface of what is possible with these new found techniques. In next week’s tutorial we will be looking further down this rabbit hole!