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.