Ha! You thought we were done with functions, but there's even more to learn about them! Elixir has a lot of concepts and syntax that surround the function, but I guess that shouldn't be a surpise to me, considering it is a functional language after all.
Today we'll combine the two of the core concepts of Elixir: functions and pattern matching.
Simple Examples
Let's take a look at how functions do pattern matching.
def swap({a, b}), do: {b, a}
This function takes a tuple with two values and returns a tuple with the two values reversed. This function will only be called when the pattern matches. So calling this function while passing a tuple with two elements will pattern match, no matter what those elements are.
iex> SimpleExamples.swap({"Bob", 2})
{2, "Bob"}
iex> SimpleExamples.swap({3, 5})
{5, 3}
iex> SimpleExamples.swap({{1, 2}, :ok})
{:ok, {1, 2}}
If we attempt to call swap/1
without passing a single tuple of two elements, the pattern match will fail and the function cannot be called.
iex> SimpleExamples.swap(1, 2)
** (UndefinedFunctionError) function SimpleExamples.swap/2 is undefined or private. Did you mean one of:
* swap/1
SimpleExamples.swap(1, 2)
iex> SimpleExamples.swap({1, 2}, {3, 4})
** (UndefinedFunctionError) function SimpleExamples.swap/2 is undefined or private. Did you mean one of:
* swap/1
SimpleExamples.swap({1, 2}, {3, 4})
iex> SimpleExamples.swap([1, 2])
** (FunctionClauseError) no function clause matching in SimpleExamples.swap/1
The following arguments were given to SimpleExamples.swap/1:
# 1
[1, 2]
examples/lwm 23/simple_examples.exs:2: SimpleExamples.swap/1
iex> SimpleExamples.swap({1, 2, 3})
** (FunctionClauseError) no function clause matching in SimpleExamples.swap/1
The following arguments were given to SimpleExamples.swap/1:
# 1
{1, 2, 3}
examples/lwm 23/simple_examples.exs:2: SimpleExamples.swap/1
iex> SimpleExamples.swap("Bob")
** (FunctionClauseError) no function clause matching in SimpleExamples.swap/1
The following arguments were given to SimpleExamples.swap/1:
# 1
"Bob"
examples/lwm 23/simple_examples.exs:2: SimpleExamples.swap/1
These all fail because the parameters do not match the arity or pattern in the swap/1
function I defined.
A function that provides a list pattern will only pattern match with a non-empty list.
def first_element([head | _tail]) do
head
end
Note that in the above example we have to put an underscore in front of the tail
parameter to indicate that this parameter is unused. Otherwise, the Elixir compiler will complain about an unused parameter.
We can call this function with a non-empty list because it matches the pattern, which specifies that the list has to have a head and tail.
iex> SimpleExamples.first_element([1, 2, 3])
1
iex> SimpleExamples.first_element(["Bob"])
"Bob"
iex> SimpleExamples.first_element([])
** (FunctionClauseError) no function clause matching in SimpleExamples.first_element/1
The following arguments were given to SimpleExamples.first_element/1:
# 1
[]
examples/lwm 23/simple_examples.exs:4: SimpleExamples.first_element/1
We can also provide a function that will only pattern match to an empty list
def empty_list([]) do
"Yep, it's an empty list"
end
This can be called with an empty list, but not a list with elements: the pattern doesn't match.
iex> SimpleExamples.empty_list([])
"Yep, it's an empty list"
iex> SimpleExamples.empty_list([2, 3])
** (FunctionClauseError) no function clause matching in SimpleExamples.empty_list/1
The following arguments were given to SimpleExamples.empty_list/1:
# 1
[2, 3]
examples/lwm 23/simple_examples.exs:8: SimpleExamples.empty_list/1
Pattern matching can of course be really specific. The following example shows a function that will only match when you pass it a three-element tuple whose second element is the same value as the second parameter.
def do_something({_, item, _}, item) do
"Yep, it matched"
end
Let's try it out.
iex> SimpleExamples.do_something({1, 2, 3}, 1)
** (FunctionClauseError) no function clause matching in SimpleExamples.do_something/2
The following arguments were given to SimpleExamples.do_something/2:
# 1
{1, 2, 3}
# 2
1
examples/lwm 23/simple_examples.exs:12: SimpleExamples.do_something/2
iex> SimpleExamples.do_something({1, "Bob", 3}, "Bob")
"Yep, it matched"
iex> SimpleExamples.do_something({1, "Bob", 3, 4}, "Bob")
** (FunctionClauseError) no function clause matching in SimpleExamples.do_something/2
The following arguments were given to SimpleExamples.do_something/2:
# 1
{1, "Bob", 3, 4}
# 2
"Bob"
examples/lwm 23/simple_examples.exs:12: SimpleExamples.do_something/2
The first example failed to match because element #2 in the tuple did not match the second parameter. The second example succeeded because we used the correct pattern. The third example failed because the tuple was a four-element tuple, which did not match the three-element tuple specified in the pattern.
You can find these functions in a module in the code examples in the Learn With Me: Elixir repository on Github so that you can load them into IEx and use them. They are in the simple_examples.exs
file under the "lwm 23 - Functions and Pattern Matching" folder.
Pattern Matching and Function Clauses
Pattern matching is not just useful when destructuring parameters, but it's also useful in determining which version of a function will be called. This is where pattern matching affects flow control.
Elixir can have multiple versions of the same function that differ in the pattern of parameters. I'm not talking about just functions with the same name, but different arity, which are actually treated as completely separate functions in Elixir. I'm talking different versions of a function with the same arity, where each version has a different parameter pattern. Which version of the function is called depends on what the parameters look like. A version of a function is referred to in Elixir as a "function clause". A single function can have multiple clauses.
For example, let's look at this generic function.
def do_something([head | tail], {_, second}), do: {head, tail, second}
def do_something([], {first, _}), do: first
def do_something([]), do: 3
When the function do_something
is called, a pattern matching will be performed on each of the function clauses, starting at the top and going down.
do_something([3, 4, 5], {6, 7})
will match the first clause ofdo_something/2
do_something([], {"a", "b"})
will match the second clause ofdo_something/2
do_something([])
will match the first (and only) clause ofdo_something/1
, which is a completely different function because it has a different arity.
Calling do_something(4)
will not match any of the definitions, and that call will fail.
iex> MatchingExamples.do_something([3, 4, 5], {6, 7})
{3, [4, 5], 7}
iex> MatchingExamples.do_something([], {"a", "b"})
"a"
iex> MatchingExamples.do_something([])
3
iex> MatchingExamples.do_something(4)
** (FunctionClauseError) no function clause matching in MatchingExamples.do_something/1
The following arguments were given to MatchingExamples.do_something/1:
# 1
4
examples/lwm 23/matching_examples.exs:4: MatchingExamples.do_something/1
Note that Elixir sees two different functions here because it sees the arity of a function as part of its identity. It sees do_something/2
(with two clauses) and do_something/1
with one clause. I tend to see them as different versions of the same function because they have the same name, but I also need to keep in mind that Elixir does not view them that way.
You can find these functions in a module in the code examples in the Learn With Me: Elixir repository on Github. They are in the matching_examples.exs
file under the "lwm 23 - Functions and Pattern Matching" folder.
In the Elixir documentation, each function name and arity will be listed once. There can be multiple function clauses for the same function, but you'll never see the multiple function clauses for a single function listed in the documentation. Functions clauses are an implementation detail, and are irrelevant to the caller.
So the documentation of the above example would document two functions: do_something/2
and do_something/1
.
Generic and Specific Function Clause Matching
When creating multiple clauses of the same function, you want to put the most specific patterns at the top (matching the fewest range of arguments) and the most generic pattern at the bottom (matching the widest range of arguments). This is because that when Elixir looks at which clause to be called, it starts pattern matching on the topmost clause and works its way down until it matches a clause or it fails to match. So if you have the clause with the most generic pattern at the top, it will always match and always get called, and the other clauses will be ignored.
Elixir often produces a warning if it sees that a function clause will never be called, but it can't detect more subtle issues with function clause ordering.
You can create a very generic catch-all function clause using the wildcard character, although the arity will still have to be correct.
def generic_specific([]), do: "specific"
def generic_specific(_), do: "generic"
- generic_specific([]) calls the specific function clause
- generic_specific(3) calls the generic function clause, which accepts 1 parameter (arity of 1)
- generic_specific(3, 4) fails because there are no function clauses with an arity of 2
iex> MatchingExamples.generic_specific([])
"specific"
iex> MatchingExamples.generic_specific(3)
"generic"
iex> MatchingExamples.generic_specific(3, 4)
** (UndefinedFunctionError) function MatchingExamples.generic_specific/2 is undefined or private. Did you mean one of:
* generic_specific/1
MatchingExamples.generic_specific(3, 4)
Generic and Specific Function Clause Match Problems
Putting the generic version at the top will mean that the first clause will always match and the more specific version will be ignored
def generic_specific_wrong_order(_), do: "generic"
def generic_specific_wrong_order([]), do: "specific"
- generic_specific_wrong_order([]) calls the generic function clause
- generic_specific_wrong_order(3) also calls the generic function clause
iex> MatchingExamples.generic_specific_wrong_order([])
"generic"
iex> MatchingExamples.generic_specific_wrong_order(3)
"generic"
The specific function clause will never be called because the generic function clause always matches. In fact, Elixir warns me about this issue when I load the source code into IEx.
warning: this clause cannot match because a previous clause at line 9 always matches
examples/lwm 23/matching_examples.exs:10
Named Parameter Wildcard
The wildcard character isn't required to create a clause that always match every pattern. You can just used a named parameter instead. This will also match everything. The difference is that you specify a named parameter when you want to actually use that parameter.
def generic_specific_named([]), do: "specific"
def generic_specific_named(number_list), do: "generic " <> Integer.to_string(length(number_list))
The second function clause will match any call with a single parameter, just like a clause with the wildcard character "_". Since we actually do something with the parameter, we named it instead. The wildcard character can only be used when you don't need to use the matching data in the function.
generic_specific_named([]) calls the specific function clause
generic_specific_named([1, 2, 3, 4]) calls the generic function clause, which accepts 1 parameter (arity of 1)
generic_specific_named([], 2) fails because there are no function clauses with an arity of 2
iex> MatchingExamples.generic_specific_named([])
"specific"
iex> MatchingExamples.generic_specific_named([1, 2, 3, 4])
"generic 4"
iex> generic_specific_named([], 2)
** (CompileError) iex:18: undefined function generic_specific_named/2
Flow Control With Parameters
We can specify multiple implementations of the same function with the same parameters by adding a third parameter that's an atom. That way we can control which implementation is called. This is just one of many useful patterns you can use.
In the English language, a name is printed out in the form "[First Name] [Last Name]". In Hungarian, it's "[Last Name] [First Name]". We can provide two different implementations of a name printing function with a third atom parameter that diffentiates them. If no atom is provided, a third implementation takes care of that case and defaults the name printing to English.
When we call our print_name
function, there will be a pattern match, and the function with the matching implementation will be called. We use pattern matching to match a name map with :first_name
and :last_name
keys. There can be other keys in the map, but those two are required.
def print_name(name), do: print_name(name, :english)
def print_name(%{first: first_name, last: last_name}, :english) do
IO.puts("#{first_name} #{last_name}")
end
def print_name(%{first: first_name, last: last_name}, :hungarian) do
IO.puts("#{last_name} #{first_name}")
end
If no atom is provided, the function defaults to printing the name using the :english
version. If the second parameter is :english
, the second clause is called. If the second parameter is :hungarian
, the third clause is called. In other programming languages, this would have required an "if" or "switch" statement, which is why pattern matching can serve as flow control.
The first clause is seen by Elixir as a different function entirely because it has an arity of 1.
iex> name = %{first: "Bob", last: "Johansson", age: 32}
%{age: 32, first: "Bob", last: "Johansson"}
iex> MatchingExamples.print_name(name)
Bob Johansson
:ok
iex> MatchingExamples.print_name(name, :english)
Bob Johansson
:ok
iex> MatchingExamples.print_name(name, :hungarian)
Johansson Bob
:ok
iex> name = %{first: "Bob", age: 32}
%{age: 32, first: "Bob"}
iex> MatchingExamples.print_name(name)
** (FunctionClauseError) no function clause matching in MatchingExamples.print_name/2
The following arguments were given to MatchingExamples.print_name/2:
# 1
%{age: 32, first: "Bob"}
# 2
:english
examples/lwm 23/matching_examples.exs:16: MatchingExamples.print_name/2
The last call to print_name
failed because it did not match the pattern, which requires the map to have a :last_name
key. Well, it matched print_name/1
, but that function made a call to print_name/2
, which in turn failed to match.
Another application of this type of flow control is a function that indicates whether a map is a "Bob Map". A "Bob Map" is a map that contains a :name
key with a value of "Bob"
.
def bob_map?(%{name: "Bob"}), do: true
def bob_map?(_), do: false
The first function clause will be called when the map matches and the second function clause will be called when the pattern did not match the first function clause. No "if" statements required.
iex> MatchingExamples.bob_map?(%{name: "Bob"})
true
iex> MatchingExamples.bob_map?(%{name: "Sploog"})
false
iex> MatchingExamples.bob_map?("Bob")
false
The last call fails because "Bob"
is a string, not a map.
Public and Private Clauses
As you may recall, public functions are defined using def
and private functions are defined using defp
. It is not possible to have some function clauses public and other function clauses private for a single function. Either all clauses of a function need to be public or they all need to be private.
However, since functions with the same name but different arity are seen as different functions entirely by Elixir, you can have a function with an arity of 1 that is public, but another function with the same name and an arity of 2 which is private. Elixir doesn't have a problem with that because it sees those as two different functions, not two clauses of the same function.
Binding a Name to a Pattern
You can use an assignment or binding operator in the parameter list to bind a parameter to a pattern. This is useful when you want to do pattern matching and assign parts of the pattern to a parameter name, but you also want to bind the entire pattern to a parameter name.
Let's look at an example.
def decompose_list(list = [ head | tail]) do
{head, tail, length(list)}
end
The decompose_list/1
function returns a tuple containing the head of a list, the tail of a list, and the length of a list. We define a destructuring pattern to get at the head and the tail easily, and we then bind the entire pattern to the list
variable. Otherwise, it would be more cumbersome to compute the length of the list.
iex> MatchingExamples.decompose_list([65, 32, 10, 88, 27])
{65, ' \nX\e', 5}
The tail appears as a character list in IEx because the numbers in [32, 10, 88, 27]
happen to correspond to printable characters. It's still a list of numbers, but IEx is displaying it differently.
This sort of pattern binding is also useful for maps.
def bob_keys(bob_map = %{name: "Bob"}) do
Map.keys(bob_map)
end
The above function returns a list of the keys in a "Bob map".
iex> MatchingExamples.bob_keys(%{name: "Bob"})
[:name]
iex> MatchingExamples.bob_keys(%{name: "Bob", age: 32, location: "Tau Ceti"})
[:age, :location, :name]
iex> MatchingExamples.bob_keys(%{name: "Homer", age: 32, location: "Tau Ceti"})
** (FunctionClauseError) no function clause matching in MatchingExamples.bob_keys/1
The following arguments were given to MatchingExamples.bob_keys/1:
# 1
%{age: 32, location: "Tau Ceti", name: "Homer"}
examples/lwm 23/matching_examples.exs:30: MatchingExamples.bob_keys/1
The last function call fails because the map is not a "Bob map", and does not match the pattern.
Anonymous Function Pattern Matching
Anonymous functions can also have multiple function clauses and make use of pattern matching.
iex> package_data = fn
...> (x, [y]) -> [x | [y]]
...> (x, y = %{}) -> %{y | x: x}
...> (x, y) -> {x, y}
...> end
#Function<12.99386804/2 in :erl_eval.expr/5>
When you call this function, passing a list as the second parameter, it matches the first clause, and the first parameter will be prepended to the list. The second clause matches if the second parameter is a map. In that case, the first parameter is updated in the map. The third clause is more generic and matches any two parameters, packaging them in a tuple.
Since all the clauses in an anonymous function are part of the same function, they must all have the same arity. Elixir will throw an error otherwise. In the above example, all clauses have an arity of 2.
Let's see the anonymous function in action.
iex> package_data.(4, [1])
[4, 1]
iex> package_data.(4, %{name: "Zim", x: 3})
%{name: "Zim", x: 4}
iex> package_data.(4, 5)
{4, 5}
Patterns vs Flow Control Statements
Pattern matching as used in Elixir means that common constructs from imperative languages such as if statements. From what I've seen so far, it appears that Elixir does have if statements and some other flow control statements, but I haven't even seen a mention of any kind of looping constructs so far. So I'm not sure if Elixir has any or not.
Being able to write code without using any flow control constructs has been a bit of a mind bender for me. I haven't started writing any useful code yet, but when I do, so I'll be interested in seeing how much I can do without using those constructs.