ThinkingElixir - Pattern Matching Resource
ThinkingElixir - Pattern Matching Resource
ThinkingElixir.com
Download Reference
by Mark Ericksen
For the best learning experience and access to the accompanying download materials, enroll in the course.
Install Elixir
In order to start learning Elixir, you need the ability to play with Elixir. Please make sure you have Elixir installed on your system
before you continue.
With Elixir installed your machine, from a command terminal, you should be able to execute the -v or “version” command to see
that it is ready to go.
$ elixir -v
Erlang/OTP 22 [erts-10.5.5] [source] [64-bit] [smp:4:4] [ds:4:4:10] [async-threads:1] [hipe]
The exact version isn’t important. Your’s will likely be newer than this one which is awesome!
IEx allows to you write Elixir statements, execute them, and get the results. It has other great features like auto-completing
commands (using the TAB key), displaying help and type information.
We Learn By Doing
The most effective way to learn Elixir is to start doing Elixir. The IEx shell gives you an easy on-ramp to getting started. With Elixir
installed on your system, you are ready to go.
In the coming sections, I really encourage you to play with the code as you read and learn about it. The code examples are
designed to be easy to copy & paste into a terminal to make it that much easier.
Open a terminal on your computer. The command to start IEx is… iex .
$ iex
Erlang/OTP 21 [erts-10.0.6] [source] [64-bit] [smp:4:4] [ds:4:4:10] [async-threads:1] [hipe]
Interactive Elixir (1.8.1) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)>
Before we start to play, you need to know how to get out of your new play area. To exit IEx, hit CTRL+C and you’ll see the BREAK
menu:
iex(1)>
BREAK: (a)bort (c)ontinue (p)roc info (i)nfo (l)oaded
(v)ersion (k)ill (D)b-tables (d)istribution
If you press c , you will remain in IEx. If you press a , it will abort the shell and exit. The most common way to exit is to use
CTRL+C again. Start an IEx shell again, now hit CTRL+C , CTRL+C . That’s right, two times in a row. That’s the easiest way to exit
an IEx shell. Commands like quit and exit don’t exist.
Play Time!
Now that you know how to start and exit an IEx shell, it’s time to start playing. Here are some simple commands you can try.
iex(1)> 1 + 1
2
iex(2)> b = 12
12
iex(3)> b
12
iex(4)> b + 10
22
iex(5)>
Notice that the iex prompt includes a line number. It increments automatically. You don’t have to worry about that.
Line Continuations
When you write an expression and use an opening character that requires a closing one, it can carry across lines.
iex(1)> "abcd
...(1)> efg"
"abcd\nefg"
Notice that it included the \n new-line character inside the string. Also note that the ...(1)> was the continuation line.
Sometimes this happens by accident. You accidentally forget to include a matching closing character. In this example, I “forgot” to
include the closing parenthesis. Eventually I figure it out, add the closing parenthesis and the function is evaluated.
Auto-Complete in IEx
As mentioned, auto-complete is very handy in IEx. Time to play with it. Let’s explore that String module that was just used to
downcase some text. What else can it do? To find out, type Str and hit your TAB key. You should see something like this:
iex(2)> Str
Stream String StringIO
It shows modules that match the str prefix. Add an i and it knows you want “String” over “Stream”. Hit TAB and let it complete.
Now add a . and hit TAB again to see all the modules and functions available under the String module. It should look something
like this:
iex(2)> String.
Break Casing Chars
Tokenizer Unicode at/2
bag_distance/2 capitalize/1 capitalize/2
chunk/2 codepoints/1 contains?/2
downcase/1 downcase/2 duplicate/2
ends_with?/2 equivalent?/2 first/1
graphemes/1 jaro_distance/2 last/1
length/1 match?/2 myers_difference/2
next_codepoint/1 next_grapheme/1 next_grapheme_size/1
normalize/2 pad_leading/2 pad_leading/3
pad_trailing/2 pad_trailing/3 printable?/1
printable?/2 replace/3 replace/4
replace_leading/3 replace_prefix/3 replace_suffix/3
replace_trailing/3 reverse/1 slice/2
slice/3 split/1 split/2
split/3 split_at/2 splitter/2
splitter/3 starts_with?/2 to_atom/1
to_charlist/1 to_existing_atom/1 to_float/1
to_integer/1 to_integer/2 trim/1
trim/2 trim_leading/1 trim_leading/2
trim_trailing/1 trim_trailing/2 upcase/1
upcase/2 valid?/1
iex(2)> String.
I see “length/1”. That seems obvious enough. I expect it will return the length of a string. Let’s try it.
iex(2)> String.length("elixir")
6
It returned the number of characters in the string. Note: the /1 means the function takes 1 argument.
Help in IEx
As mentioned before, IEx has the ability to get “help” on modules, functions and more. Let’s see the help for String.length . The
help is displayed by putting the h command in front of a module name or a module and function name together as in the
following example:
You will notice that this help text is the exact same as what is in the online documentation. String.length/1 documentation. This is
no coincidence. The documentation is generated from the help text included in the code. Likewise, your code always has the
documentation for the exact version of Elixir that you are running. That’s pretty cool! Still works when you are on an airplane.
Command History
Being able to press the UP arrow key and bring back the last command can be really helpful. When you start IEx, you can pass in
a command to make that available like this:
This command is passing an Erlang configuration option through IEx using --erl . The command will enable shell history. Now,
when in an IEx shell, pressing the UP arrow brings back the previous command. Hitting the UP arrow repeatedly lets you keep
going back through the command history.
Entering that command every time you start IEx isn’t fun. You can set the ERL_AFLAGS environment variable on your system
through your shell’s profile file to make it always available.
On Windows:
On Windows 10 / PowerShell:
Don’t worry about doing this right now if it’s not important to you at the moment. Just know that it can be solved and you have the
solution right here when you are ready for it.
atom
boolean
nil
integer
float
string
Atom
An atom is a literal, a constant with name. It is a constant whose name is its value. Atoms are very useful in pattern matching and
are used for more than you might expect in Elixir.
Examples of Atoms:
:my_atom
:testing
:customer_type
An atom begins with a colon and typically is all lowercase letters using an underscore to separate words.
If you need an atom to contain spaces or special characters that otherwise wouldn’t be valid, you can express it as a string with
the colon in front.
However, writing atoms this way is unusual and only done when needed for specific exceptional instances.
An Atom in Elixir is similar to a symbol in Ruby or an enumeration in C/C++. Atoms are stored in the “atom table”. All references
to an atom like “ :ok ” are shared and actually all point to the same atom table entry.
Functions for working with an Atom can be found in the Atom module.
Preventing Denial-of-Service
Atoms are not garbage collected. The important thing to learn from this is you should not allow user provided content to become
an atom at run-time in your system. Converting unchecked user data to an atom can expose your system to a Denial of Service
(DOS) attack. The attack works like this, a malicious actor causes your system to repeatedly create unique atoms until it
consumes all of the available resources on your machine ultimately causing it to crash. It isn’t a security flaw, it isn’t a “break-in”,
but it can be abused to cause your system to crash.
You can allow user input to become an atom using the String.to_existing_atom/1 . If you already have an atom defined in the
system, it accepts the conversion. If you try to convert to something new, it raises an exception blocking the operation.
iex(1)> String.to_existing_atom("ok")
:ok
iex(2)> String.to_existing_atom("whaaaat")
** (ArgumentError) argument error
:erlang.binary_to_existing_atom("whaaaat", :utf8)
Boolean
Examples are: true and false .
false == :false
#=> true
true == :true
#=> true
Nil
Nil represents the absence of a value. It is like null in Javascript and many other languages.
nil == :nil
#=> true
Integer
Represents positive and negative whole numbers. Including 0.
Functions for working with an Integer can be found in the Integer module.
Examples:
-10, 0, 1000
Integers can be arbitrarily large. It uses as much data is needed to contain it.
99999999999999999999999999999999999999999999999999999999
0xFF
#=> 255
Integers can use _ as a grouping separator. This is functionally ignored but helps with readability.
1_000_000
#=> 1000000
Float
Represents floating point numbers.
Examples:
Functions for working with a Float can be found in the Float module.
Floats don’t support a “Decimal” type for explicit levels of precision. As such, it results in typical floating point rounding issues.
0.8 * 3
#=> 2.4000000000000004
This is expected with floating point math. For further explanation, see these resources:
http://0.30000000000000004.com
What Every Programmer Should Know About Floating-Point Arithmetic
If you need to represent fixed decimal values for something like money, using a Float may not be the most appropriate choice. For
example, it may work better to use an integer that represents cents (instead of whole dollars).
Another option is to use a community package called Decimal that provides arbitrary decimal precision.
Scientific Notation
Floats are frequently displayed in their scientific notation. This may not be what you expect or want.
1200.0
#=> 1.2e3
Note that when working in IEx, it converts the data to a string for display in the shell. This is the same display as Float.to_string/1.
Float.to_string(1200.0)
#=> "1.2e3"
If you need to convert a float to a string with explicit decimal precision, use the built-in Erlang function float_to_binary . In Elixir,
you can use any Erlang function. In this example, the function we want is declared in the erlang module. To call it, use an atom as
the module name like this:
:erlang.float_to_binary(1200.0, decimals: 2)
#=> "1200.00"
String
Strings are encoded in UTF-8. A string uses the double quote character " . Single quoted text is not a string, that’s a charlist and
behaves differently.
The String module has a number of functions for working with them.
String.upcase("hello world")
#=> "HELLO WORLD"
Concatenation
Elixir strings can be concatenated using the <> operator.
text = "Hey"
text <> " friends!"
#=> "Hey friends!"
Interpolation
Elixir strings support interpolation using the #{...} characters embedded in a string.
name = "Tom"
"Greetings #{name}!"
#=> "Greetings Tom!"
is_binary("a string")
#=> true
Charlist
Mostly used for interoperability with Erlang. It is just a list of code points and is created with single quotes.
'this is a charlist'
#=> 'this is a charlist'
is_list('testing')
#=> true
A charlist can be converted to a string and vise versa using these functions:
Kernel.to_string/1
Kernel.to_charlist/1
“Kernel” is Elixir’s default environment. It even defines basic math operators like + and - . Because it is so essential to Elixir, the
Kernel module is always included for you. You can just use the functions to_string/1 and to_charlist/1 to convert between the
types without needing to specify the “Kernel” module explicitly.
to_string('hello world')
#=> "hello world"
to_charlist("hello world")
#=> 'hello world'
Modules to Manipulate
All of these types are primitives. They are “data”. They are not objects with functions attached to them. There are no “objects” in
Elixir. There is only “data” and “functions”. Modules are a collection or a container for functions.
By convention, when you want to perform some function on a piece of data, you use the type’s module for doing that. This is
particularly the case for Atom, Integer, Float, and String.
List
How it works
A List uses the [ and ] characters to contain the elements of the list separated by commas. It looks like this:
my_list = [1, 2, 3, 4, 5]
This looks like an Array in other languages. However, a List doesn’t act like an Array.
A list in Elixir (and other functional programming languages) is actually a recursive data structure. Internally implemented as a
linked list.
When you realize an Elixir list is an immutable linked list, it’s behavior makes sense and how you use it becomes clear.
my_list is “bound” to the list. The variable isn’t “assigned” the value of the list, it is said to be “bound”. Variables in Elixir are all
“bound” to their values. You can think of “bound” as meaning “is pointing to”. So my_list is pointing to a specific element in a
linked list. Each element points to the next element in the list and separately points to the value for the element.
In many languages, it is common to add more items to the end of an Array as it is built up. With an immutable List, we can’t add
items to the end as that would be affecting other variables that are bound to some element earlier in the list.
If I really do want to add to the end of the list, I can, but a whole new list is created with that value at the end. When a list is very
large, this becomes an expensive operation. Two lists can be concatenated together using the ++ operator.
my_list ++ [6]
#=> [1, 2, 3, 4, 5, 6]
Adding to the front of the List, or the “head” is very cheap. It doesn’t alter other variables that are already bound to some element
in the List.
This makes it very efficient both in speed and memory consumption. To efficiently add to the front of a list, you use the | pipe
character to join the new element and the existing list together.
[0 | my_list]
#=> [0, 1, 2, 3, 4, 5]
It can be cheaper and faster to build up a large list in reverse, adding to the “head” rather than re-building the whole list with each
new addition. Then, once built, perform a single “reverse” of the whole list using Enum.reverse/1.
Enum.reverse([6, 5, 4, 3, 2, 1])
#=> [1, 2, 3, 4, 5, 6]
Working with immutable data may be a different experience for you. It probably feels
uncomfortable. However, it is what enables concurrency and immutable data removes a whole
category of state related bugs. If it is uncomfortable now, just know that understanding and
embracing it will help make you a better programmer. And you’ll end up loving it!
Below I have 2 one-liner functions that exaggerate the differences so you can really feel it. Don’t worry about understanding the
details of the code. Conceptually they both build a list of 100,000 items. They both add one number at a time to the list starting
with the number 1 and going up to 100,000.
The difference is one function adds the item to the end of the list using list concatenation. That’s the code that says acc + [n] . It
means list_so_far ++ [next_number] .
Running Observer (it’s like an activity monitor built in to the BEAM), you can see the load it put on the machine.
The top chart shows the CPU impact and how much time it took. The bottom left chart shows the RAM impact. The saw-tooth line is
from garbage collection being done.
Why is it so inefficient? With every item added, it re-creates the entire list so no other references to the list are affected.
Let’s try it again with code adds items to the front of the list. The important part of the code is the [n | acc] which means
[next_number | list_so_far] . It adds the single item to the front of the list.
Run this line in IEx and see how long it takes to return.
Did that feel different? Let’s see the system impact of the code.
Notice the chart scale is reduced and that the CPU has one spike that stayed below 30%. The RAM also only slightly bumped.
Why is it so much more efficient? As the list is being built, it never gets re-created. Any other variable references to the list made
during this operation won’t be affected by adding more items to the front. So it can just keep adding items to the front efficiently.
The point here is to understand how lists work in Elixir so you have the right mental model. Lists are internally implemented as
linked list, not an array. You can’t treat them like an array.
List Contents
Lists don’t just contain integers. Lists can contain any data type and can contain different types from one element to the next.
A list’s contents are ordered. They remain in the order they are declared.
Take some time to experiment with lists in IEx. Try concatenating lists together with ++ , you can also remove elements from a
list using -- . Try adding to the front of a list using the | pipe separator. Try tweaking the list function examples to use different
numbers. Experimentation is playing! Have fun playing!
Tuple
Here are a few key points about the tuple data type.
The following example represent common function return values. Each tuple contains 2 pieces of information or data.
{:ok, result}
{:error, reason}
The first piece of information is an atom (ie: :ok or :error ) that tells if the operation succeeded or not. If we got an :ok , we know
it succeeded and the next piece of data is the result of the operation. If we got the atom :error then we know the operation failed
and the reason may tell us why.
Another example of using a tuple to return multiple pieces of information at once might be the result of a function that splits a list
of integers into odd and even sets. Imagine that a function named split_odd_even/1 is available to us. This function leaves the
original list unmodified (immutable) and returns the two different result sets through a tuple. It could look like this:
odd_results
#=> [1, 7, 15]
even_results
#=> [2, 12]
Here is another tuple example that contains multiple pieces of data. In this example the tuple contains a person’s name and age:
{"Howard", 32}
Using the Kernel.elem/2 function, you can extract the value of an element from a tuple.
elem({:foo, :bar}, 2)
#=> ** (ArgumentError) argument error
A cleaner and more natural way to do this is with “pattern matching” which isn’t covered here.
You can replace an element using Kernel.put_elem/3. Remember, Elixir isn’t changing the tuple, internally it is creating a new tuple
where the elements point back to the previous tuple’s values, with the exception of the new change.
You can see that the original tuple variable has not been modified:
tuple
#=> {:foo, :bar, 3}
Tuples are best for a fixed number of elements. If you need a dynamic container that preserves order, use a List instead.
Map
A map is a collection data structure. An entry is made up of a key and a value . As noted in the Map documentation:
The “key” of a map can be anything. However, it is most commonly a string or an atom.
When you receive a web POST with data, it will be represented as a Map with string keys. When you prepare a response structure
for JSON, it will likely end up as Map with atom keys.
String-Key Version
%{"name" => "Howard", "age" => 30}
#=> %{"age" => 30, "name" => "Howard"}
Atom-Key Version
%{name: "Howard", age: 30}
#=> %{age: 30, name: "Howard"}
An atom key can also be written using the arrow syntax. It is shortened for you.
%{
1 => "Integer One",
1.5 => "Float!",
true => "boolean key",
{:name, "Daniel"} => "a tuple",
%{map_as_key: true} => "gracious!",
[1, 2, 3] => "a list"
}
Using any data type as a key may not be very practical, but it can come in handy. Just know that this ability exists.
Nested Values
It is common to have nested values. Take this for example:
order = %{
id: 1,
customer: %{id: 99, name: "Widgets, Inc."},
item: %{
id: "item-11332",
name: "Sprocket #12",
price: 12.70,
quantity: 1
},
discounts: [%{code: "SUMMER19"}],
total: 10.00
}
The customer , item , and discounts keys are atoms. The value is another data structure.
Map.get/3
We can access elements of the map using the Map.get/3 function. We provide it the map and the key we want the value for. The
third argument is the default value to return if the key isn’t found in the map. It defaults to nil .
Map.get(order, :id)
#=> 1
Requesting a missing key from the map, we can give the default value we want back.
Map.get(order, :missing_key)
#=> nil
Access Behaviour
Another way to access the value of a key is to use the access operator [] .
order[:id]
#=> 1
order[:missing_key]
#=> nil
order.id
#=> 1
order.missing_key
#=> ** (KeyError) key :missing_key not found in: ...
Kernel.get_in/2
Another option is Kernel.get_in/2. It supports a handy way of returning data nested deeper in the structure.
You don’t need to worry about the internals though. You can just know that it is efficient memory management and it ensures
“immutable” data.
Map.put/3
The Map.put/3 function can add new keys to a map and change existing ones.
person = %{
name: "Sally Green",
age: 35,
position: "Manager",
division: "Engineering"
}
Say we want to add another key to track on the person . Now we want to track the corporate region she works in.
If we look at the person again, we see it is unchanged! It doesn’t contain the newly added region.
person
#=> %{
#=> age: 35,
#=> division: "Engineering",
#=> name: "Sally Green",
#=> position: "Manager"
#=> }
We didn’t modify the original person map. It added the key to a new map where the new map’s other keys all pointed back to the
original person map.
If we want to keep the change, we can re-bind the person variable to point to the newly updated map. Alternatively we can
create a new variable to bind to the altered map.
person
#=> %{
#=> age: 35,
#=> division: "Engineering",
#=> name: "Sally Green",
#=> position: "Manager",
#=> region: "west-1"
#=> }
Elixir allows us to re-bind a variable. Erlang and other functional programming languages don’t
even allow that. Instead, I’d have to create a new variable for every change. Elixir makes this
easier and it can deceptively feel like how it works in other languages.
Kernel.put_in/3
The Kernel.put_in/3 function lets us add new keys and update existing ones in a deeply nested map .
Updating a value deeper inside a nested map can be hairy when using Map.put/3 because you have to update the map at each
level. Instead Kernel.put_in/3 gives us the syntax we need to make a deeper change.
Remember that we don’t need to use the module Kernel as that is imported for us.
In this example our data is nested 3 levels deep and we want to update the :value at the deepest level.
data = %{
name: "level 1",
value: 100,
data_1: %{
name: "level 2",
value: 200,
data_2: %{
name: "level 3",
value: 300
}
}
}
Using Kernel.put_in/3 works well. The second argument is a list of the keys to follow that get us where we want to go. The last
argument is the new value to set for the last key in our list.
This updated the value at the deepest level and returned a new map with the desired change. Data in Elixir is immutable. Our
original data variable is still bound to the original map with the unchanged value.
Working with immutable data takes some time to get used to, but it is an important foundation for so many benefits that we want!
The benefits are worth the initial discomfort we feel with it.
This can set multiple values at one time. This update recognized Sally’s promotion and that she is now older. Likewise, it doesn’t
update the person unless we explicitly re-bind the person variable to the updated map.
This only works for atom keys that already exist on the map. It will error when attempting to set a key that doesn’t exist.
Try performing some of your own updates in an IEx session. What do you have to do to keep the changes?
More Resources
For more detailed information on the built-in functionality for working with maps, refer to these resources:
Map module
Access module
Kernel.get_in/2
Kernel.pop_in/2
Kernel.put_in/3
Kernel.get_and_update_in/3
In functional programming, it is just “functions” and “data”. Functions are the instructions of how to transform some given data.
Modules are containers for functions that also provide a namespace.
Modules
Modules serve both as a container for functions and as a namespace.
A module is defined using the defmodule command and the name starts with an uppercase letter.
defmodule MyFoo do
def foo do
"Hello!"
end
end
In order to execute the foo function, I need to provide the namespace to the function. You can copy/paste the above module code
into IEx to play with it.
MyFoo.foo
#=> "Hello!"
Parenthesis are optional in Elixir. The above could also be written as MyFoo.foo() . If there is ambiguity about the syntax then the
parenthesis are required by compiler.
Functions in IEx
Functions must be defined inside of a module. This makes playing with them less convenient in IEx. Typing and editing module
code into IEx is awkward. Here are three ways you can play with modules at this stage:
The first one you can do easily enough on your own. Editing the code and pasting it into IEx will “re-write” the module.
Thinking Tip
Read “Running an Elixir File as a Script” for a walk-through of how to do it. It includes
explanations of what’s happening and how to recognize when you’ve grown out of a script.
Simple to create
A mix project is very small
Easily supports organizing your code into multiple files
Starts you off with a testing framework setup
Thinking Tip
Read “Creating Your First Mix Project” for a walk-through of how to do it. It includes tips on
working with your code in a mix project.
For more details on how to do this, please read the post “ Creating Your First Mix Project” which explains it in more detail.
Function “Arity”
In Elixir we talk about functions and their “ arity“. Meaning the number of arguments that a function takes.
Let’s add two functions called greeting to our MyFoo module. You can copy/paste this into IEx.
defmodule MyFoo do
def foo do
"Hello!"
end
def greeting(name) do
"Hello #{name}!"
end
end
In IEx, I can use auto-completion to see the functions available on the module I just defined. After MyFoo. press the TAB key to
see the auto-complete options.
iex> MyFoo.
foo/0 greeting/1 greeting/2
iex> MyFoo.
It shows the two different greeting functions. One with the /1 and the other with a /2 to identify how many arguments they
take.
In the “ MyFoo.foo ” example, the return is the string "Hello!" because it was the last value expressed in the function. There is no
way to not return a value. Given that this is Functional Programming, every function returns a value! You may choose to ignore it,
but it will return something.
In this example, let’s create a function that does nothing and returns nothing. What happens when we call it?
defmodule MyFoo do
def do_nothing do
end
end
MyFoo.do_nothing
#=> nil
No Early Return?
Given that a function always returns something and the last thing the function does is used as the return value, some people ask,
“Why can’t I return explicitly at an earlier point? Why doesn’t Elixir have an explicit return?”
The short answer is, “Erlang doesn’t have explicit returns either and Elixir is built on Erlang.”
The longer answer is, with pattern matching, you have a new tool to solve old problems in a new way. Once you get comfortable
with pattern matching in functions, you won’t miss early returns. Your Elixir code becomes more understandable and readable
without them. If this is a concern of yours, just know for now that it’ll be okay. Promise.
defmodule Testing do
def do_stuff do
# do stuff that can't fail or any errors are handled
:ok
end
end
Testing.do_stuff
#=> :ok
Private Functions
Functions defined with the defp macro are “private”. They are not exported from the module. The can be called from within a
module, but are not available outside the module.
defmodule MyApp do
def public_do_work(input) do
private_work(input)
end
defp private_work(_input) do
IO.puts "working!"
end
end
MyApp.public_do_work(123)
#=> working!
#=> :ok
MyApp.private_work(123)
#=> ** (UndefinedFunctionError) function MyApp.private_work/1 is undefined or private
#=> MyApp.private_work(123)
say_hello = &MyFoo.greeting/1
#=> &MyFoo.greeting/1
I can now execute the function bound to the variable using a . to identify that the variable references a function to execute and
isn’t the name of the function itself.
say_hello.("Mark")
#=> "Hello Mark!"
Using this technique, we can pass a function as a parameter into other functions.
defmodule MyFoo do
def greeting(name) do
"Hello #{name}!"
end
end
Now we can let the process_name/1 function combine a piece of data and a function that we provide.
MyFoo.process_name("Mark", &MyFoo.greeting/1)
#=> "Hello Mark!"
MyFoo.process_name("Mark", &IO.puts/1)
#=> Mark
#=> :ok
MyFoo.process_name("Mark", &String.to_atom/1)
#=> :Mark
Default Arguments
An argument to a function can be given a default value using the \\ operator. It looks like this:
defmodule MyFoo do
end
Let’s create a new greeting function that also gives a compliment. We’ll provide a default compliment.
defmodule MyFoo do
end
When I execute the function with only a name given, the default compliment value is used. I can provide an override compliment
to use for that specific case.
MyFoo.greeting_with_compliment("Tom")
#=> "Greetings Tom! You look nice today!"
iex> MyFoo.greeting_with_compliment
greeting_with_compliment/1 greeting_with_compliment/2
We didn’t explicitly create a greeting_with_compliment/1 function. That one was created for us. If we could see the generated code,
it would essentially look like this.
def greeting_with_compliment(name) do
greeting_with_compliment(name, "You look nice today!")
end
When the /1 function is called, it executes the /2 version passing in the default value for that argument. This is helpful to
understand so as you auto-complete your functions and see functions listed that you didn’t create, you understand where they are
coming from.
is_atom(MyFoo)
#=> true
Atom.to_string(MyFoo)
#=> "Elixir.MyFoo"
String.to_atom("Elixir.MyFoo")
#=> MyFoo
:"Elixir.MyFoo" == MyFoo
#=> true
Behind the scenes, an Elixir module is an atom with “Elixir” as part of the name. This namespaces it and makes it easier to identify
internally.
Aliases
As you organize your code into modules and namespaces, it can become pretty long. You can use an “alias” to create a name
shortcut for your code. Imagine something like the following module.
defmodule MyApp.Customers.Billing.History do
end
In order to execute the function from outside the module, the full namespace is needed. This quickly becomes tedious.
An alias can be declared anywhere. However, it is convention to declare the aliases all together at the top of a module.
defmodule MyApp.Customers do
alias MyApp.Customers.Billing.History
def compute_current_month() do
# get the start/end dates for the current month
History.compute_for_period(month_start, month_end)
end
end
Be default, the alias name is the last piece of the namespace. In this case, “History”. Any references to “History” inside the module
declaring the alias are a shortcut to the full name.
Let’s define some poorly named modules that will create a name collision when we alias them.
defmodule MyApp.Customers.Orders.Process do
def perform(_order) do
IO.puts "performing order work"
:ok
end
end
defmodule MyApp.Customers.Jobs.Process do
def perform(_job_info) do
IO.puts "performing job work"
:ok
end
end
When I have some code that needs to access both of the above modules, declaring the aliases like this won’t work as expected.
No error occurs, but the last alias command wins and the alias to “Process” is overwritten.
alias MyApp.Customers.Orders.Process
#=> MyApp.Customers.Orders.Process
alias MyApp.Customers.Jobs.Process
#=> MyApp.Customers.Jobs.Process
Process
#=> MyApp.Customers.Jobs.Process
An alias that renames the default name might look like this:
OrderProcessor.perform(123)
#=> performing order work
#=> :ok
JobProcessor.perform(123)
#=> performing job work
#=> :ok
I don’t recommend module naming like this, but I’ve seen it enough times now that it’s worth mentioning.
A struct is defined inside a module and the name of the struct is the module itself.
A simple struct:
defmodule Player do
end
This defines a Player struct where the data has 3 named attributes. For a new struct, all the attributes will have the default value
of nil . When you copy/paste that module definition into IEx, you can then auto-complete on “ Player. ” and it completes to
Player.__struct__ . Execute that and you see the struct.
Player.__struct__
#=> %Player{email: nil, score: nil, username: nil}
The struct gets the name of the module. Since we didn’t define any default values for the struct, Elixir assigned nil to each of
the attributes. For a Player , it would make sense that the score should default to 0 instead of nil .
Default Values
When declaring the struct, we can provide a keyword list to provide default values. That looks like this:
defmodule Player do
end
When creating a new Player struct, it defaults the score to 0 for us.
%Player{}
#=> %Player{email: nil, score: 0, username: nil}
%Player{lives: 100}
#=> ** (KeyError) key :lives not found
#=> expanding struct: Player.__struct__/1
A Struct is a Map
A struct is a map and can be accessed using normal map functions.
is_map(gary)
#=> true
Map.get(gary, :score)
#=> 100
gary.score
#=> 100
gary[:username]
#=> ** (UndefinedFunctionError) function Player.fetch/2 is undefined (Player does not implement the Acces
s behaviour)
#=> Player.fetch(%Player{email: nil, score: 100, username: "Gary"}, :username)
#=> (elixir) lib/access.ex:322: Access.get/3
gary.username
#=> "Gary"
Structs have pre-defined atom keys. You can use the key name like gary.username and it returns the value.
This is only an introduction to Elixir structs so we have enough of a foundation to use them in pattern matching. The combination
of structs and pattern matching are powerful and beneficial. They are two features that go great together.
For more information on structs, they are covered in more detail in the Elixir Fundamentals Collection. You can also find
information online.
Similar to operators like “ + “, “ - “, and “ * ” there is a left and right side of the Match Operator. The left side of the match operator
is the pattern. The right side is the data being matched.
pattern = data
Let’s see an example of doing those three things the “normal” non-pattern matching way. Let’s say the data we want to use is the
following map.
Now imagine I have a function that takes a given piece of data that may or may not be a map. I want to verify that it is a map,
then that the map has the key :name and finally, bind a variable called name to that value in the map. The code might look
something like this:
I systematically “poke” the data blindly trying to feel out it’s shape. Once I know that it is a map and it has a :name key, then I
can get that value and bind the name variable.
A Pattern Match match does this all in single statement. Instead of a series of “pokes” at the data, I say, “This is the type and
shape of the data I want. If it matches, bind the value to a variable called name “.
name
#=> "Howard"
This is “declarative”. I state the conditions that must be met for it to match (the pattern) and if it matches, the value is bound!
If the type matches and the data’s shape matches, then it “pulls apart” the data by binding values to variables. This is also
“destructuring” the data or “unpacking” it for us.
Pattern matching helps us create code that avoids nested if statements. Our code becomes
flatter, clearer, easier to read and easier to maintain.
x = 5
#=> 5
x
#=> 5
It doesn’t look like anything special. It looks like a normal variable assignment you see in other languages. However, the BEAM
takes the statement and perform those three pattern matching steps for us:
After this statement, x is bound to the value 5 . It skips right to binding because we didn’t provide a pattern to match.
x
#=> 5
5 = x
#=> 5
If x has the value of 5, and we are matching the statement 5 = x then the BEAM performs the Pattern Match steps for us like
this:
1. Does the type match? The left side is an Integer and x is bound to an Integer, so “yes”.
2. Does the shape match? The left side has the shape 5 and x is bound to a shape of 5 , so “yes”.
3. Nothing to bind. No variables were given on the left side.
The statement 5 = x is invalid in assignment-based languages but in Elixir, this is a valid match!
You may never have thought of the number 5 as a “shape” for data, but it is! You can think of the number 5 as a specific shape of
an Integer.
Match Error
What happens when a match is not made? When we write a Pattern Match like 5 = 3 , we are telling the BEAM, “This has to
match!”. The expression doesn’t give the BEAM an alternative for what to do when it doesn’t match. We are basically asserting
that it matches with the way the expression is written. When the BEAM has no other option, a Match Error is raised.
5 = 3
#=> ** (MatchError) no match of right hand side value: 3
The BEAM is saying, “You asserted that this matches and it doesn’t. Error!”
We will look at Pattern Match expressions that allow for elegant alternatives to a Match Error. In fact, those other ways are at the
heart of what makes pattern matching so awesome!
x = 5
#=> 5
^x = 6
#=> ** (MatchError) no match of right hand side value: 6
^x = 5
#=> 5
The Pin Operator can be used with every data type. It can be very useful in pattern matching.
Exercises
Here are some commands to try out in an IEx session. Play with what makes a match and what doesn’t. Get familiar with the Match
Error message because while you are learning Elixir, you will likely encounter it a lot. As you become more comfortable, you will
instinctively know what will match and you will encounter the error a lot less.
a = 1
#=> 1
a = 1 + 5
#=> 6
6 = a
#=> 6
5 = a
#=> ** (MatchError) no match of right hand side value: 6
3 = 2
#=> ** (MatchError) no match of right hand side value: 2
3 = 3
#=> 3
x = 5
#=> 5
^x = 3 + 2
#=> 5
^x = 3 + 5
#=> ** (MatchError) no match of right hand side value: 8
^x = x
#=> 5
:ok = :ok
#=> :ok
:ok = :error
#=> ** (MatchError) no match of right hand side value: :error
[1, 2] = [1, 2]
#=> [1, 2]
[1, 2] = [3, 4]
#=> ** (MatchError) no match of right hand side value: [3, 4]
Limits to Matching
Pattern matching is everywhere in Elixir. It is a powerful and central language feature. There is a limit that is important to
understand. You cannot execute a function on the left-hand side of a match.
length([10]) = 1
#=> ** (CompileError) iex:6: cannot invoke remote function :erlang.length/1 inside match
length([10]) == 1
#=> true
The BEAM is trying to determine if the data on the left-side matches what is on the right. If it matches, then we may want to
intentionally create side effects in our system. The process of performing a match is not allowed to create side effects.
Because of this, no function calls are allowed on the left side of a Pattern Match statement.
user.name
#=> "Jim"
user.name = "Howard"
#=> ** (CompileError) iex:3: cannot invoke remote function user.name/0 inside match
1. The developer is trying to use the match operator “ = ” to perform an “assignment”. In effect, they are trying to “assign”
the value “Howard” to the map’s name key. This isn’t how pattern matching binds variables.
2. The developer is trying to mutate the map. Remember, data in Elixir is immutable. The change could be made with user =
Map.put(user, :name, "Howard") or using the abbreviated update syntax of user = %{user | name: "Howard"} .
3. The left side is actually a function user.name/0 . Functions are not allowed on the left side of a match.
Why did the error say cannot invoke remote function user.name/0 inside match ? Why did it say we were trying to execute a function
on the left-side of the match operator?
In Elixir, the Access behaviour is a convenience that is available to a Map and other data structures. It turns our short-hand access
of user.name into a function that effectively calls Map.get(user, :name) for us. Even though we didn’t create or call the name/0
function, the Access behaviour was executed for us, and that is how the function was called. So the left side of the match
statement turned into a function call! Couple that with a misuse of the match operator and now you understand the error
message.
Hopefully this helps you quickly recognize the error message you’ll see as you occasionally forget and fall into an old code habit
and misuse the match operator. It’s okay. It takes time to establish new habits and new ways of thinking about something as
common as the equals sign “ = “.
We’ll look at some examples of how this works for the different collection data types.
Map
Struct
Tuple
List
We’ll also look at how to do this with the Strings and binary types.
Case Statement
Part of the magic of pattern matching is that when it doesn’t match our specific pattern, it continues on to try the next pattern. To
play with this, we’ll use the case statement. Pattern matching is everywhere in Elixir and that is true for the case statement as
well.
case data_being_examined do
pattern_1 -> code_to_execute_when_matches
pattern_2 -> code_to_execute_when_matches
pattern_3 -> code_to_execute_when_matches
end
The patterns are checked from top to bottom. If it doesn’t match pattern_1 then it checks pattern_2 and so on. This looks a lot
like a switch statement in Javascript .
Matching a Map
With a case statement to play with, let’s start with a Map. Let’s look at an example using code you can paste into IEx.
case data do
%{name: "Howard"} -> "Yes sir Mr. Admin!"
%{name: name} -> "Greetings #{name}!"
%{age: age} -> "I don't know who you are, but you're #{inspect age} years old!"
_other -> "Uhh.... what's that?"
end
After pasting that code in, it returns "Yes sir Mr. Admin!" . The match was for a map (type), with a :name key and a value of
"Howard" (shape). That’s very specific match! Also note, it stopped at the first match.
Let’s try sending a different piece of data though the case statement and see what happens.
case data do
%{name: "Howard"} -> "Yes sir Mr. Admin!"
%{name: name} -> "Greetings #{name}!"
%{age: age} -> "I don't know who you are, but you're #{inspect age} years old!"
_other -> "Uhh.... what's that?"
end
After pasting in the case statement again, you should get a different result. The more specific pattern that included the name
"Howard" in the pattern didn’t match the data for "Jill" . So the pattern match moved on to the next pattern and that one
matched.
The 2nd pattern given is for a map (type) with a :name key (shape) but we don’t specify what the value must be. We provide a
name variable to bind the value to and magically it is bound to the string "Jill" and available for use in our code!
Now what will happen if the data being matched is the number 5 ?
data = 5
case data do
%{name: "Howard"} -> "Yes sir Mr. Admin!"
%{name: name} -> "Greetings #{name}!"
%{age: age} -> "I don't know who you are, but you're #{inspect age} years old!"
_other -> "Uhh.... what's that?"
end
The first 3 patterns we specified are all maps. When it doesn’t match any of those, we can specify a “catch all” pattern that will
match no matter what. You can think of this like an else or default in other switch -style statements. This pattern doesn’t define
a type or a shape, so it matches.
Order is Important!
We’ve already seen that the patterns match from top to bottom. Let’s make that really clear here because it is really important!
Returning to the first data example with Howard, we’ll rearrange the patterns.
case data do
%{name: name} -> "Greetings #{name}!"
%{name: "Howard"} -> "Yes sir Mr. Admin!"
%{age: age} -> "I don't know who you are, but you're #{inspect age} years old!"
other -> "Uhh.... what's that?"
end
The less specific pattern that doesn’t match on the value of the name is now higher. The pattern matches and the result is
"Greetings Howard!" even though a more specific and “better” match exists. It stops at the first match!
At the risk of over emphasizing this point, lets rearrange the patterns one more time. Let’s move the other pattern to the top.
case data do
other -> "Uhh.... what's that?"
%{name: "Howard"} -> "Yes sir Mr. Admin!"
%{name: name} -> "Greetings #{name}!"
%{age: age} -> "I don't know who you are, but you're #{inspect age} years old!"
end
In this example, we can see that multiple other patterns are “better” matches for the data than the “ other ” pattern. It doesn’t
matter though, the first pattern to match gets the data!
%{name: _}
This defines a pattern for a map that has an atom key of “ :name “. Because of the _ , we skip the “binding” step and we also
don’t specify a value it should have. This lets us be more specific about the shape of the data that we care about for this pattern.
All 3 of the patterns describe the same shape. The difference is in the named placeholder. This doesn’t change the shape but it is
certainly helpful to another developer who comes across the code later!
{_, _, _}
This defines the pattern for a 3 element tuple. It doesn’t matter about the “values” in the tuple, but it defines the “shape” as being
a tuple with 3 elements.
Naming the placeholders for developer clarity is really beneficial here. The following pattern still defines a 3 element tuple.
However, it gives much more meaning to the positions!
data = %{
important_flag: true,
level_1: %{
other: "stuff",
level_2: %{
value: 123,
more: "stuff"
}
}
}
case data do
%{important_flag: false} -> {:ok, 0}
%{important_flag: true, level_1: %{level_2: %{value: value}}} -> {:ok, value}
_other -> {:error, "Invalid data"}
end
Try changing the shape of the data and run it against the case statement to see how it behaves.
In this example, we can match the two element tuple (type and shape), the first element is an :ok atom (shape), the second
element is a map (type), the user :email key (shape) and finally bind the email variable to the value. This is a very common
pattern!
case function_call_result do
{:ok, %{email: email}} -> "Sending email to: #{email}"
_other -> "Nothing to do"
end
When you start to break it down, there’s a lot happening here. But I don’t have to think about it. I declare the pattern I’m looking
for and when it matches, the instructions to perform are executed. It’s declarative and clean. The Pattern Match works both for
conditional logic and flow control.
Recap
Here we’ve covered working with maps and tuples in pattern matches. The point worth understanding is the following things apply
to all data types, not just maps and tuples.
Exercise #1
Write the left hand side of this statement. Your goal is to only match when the map has an atom key of :amount and then binds
the value for that key to the variable named value . What does the left side look like?
Showing Solution
%{amount: value}
The value variable should be bound to the value 142 . You can verify:
value
#=> 142
Exercise #2
Write the left hand side of this statement. Your goal is to only match when the map has a string key of "name" and then binds the
value for that key to the variable named name . What does the left side look like?
Showing Solution
%{"name" => name} = %{"name" => "Your Customer, Inc", "amount" => 142}
The name variable should be bound to the expected value. You can verify:
name
#=> "Your Customer, Inc"
Exercise #3
Write the left hand side of this statement. Your goal is to only match when the map has both string keys of "name" and "amount" .
We don’t care what the "name" value is, but the key should be present. Bind to the amount value. What does the left side look
like?
Showing Solution
%{"name" => _name, "amount" => amount} = %{"name" => "Your Customer, Inc", "amount" => 142}
The amount variable should be bound to the expected value. You can verify:
amount
#=> 142
Exercise #1
Write the left hand side of this statement. Your goal is to only match when the first element of the tuple is :ok and then bind the
value 1500 to a variable named answer . What does the left side look like?
Showing Solution
{:ok, answer}
The answer variable should be bound to the value 1500 . You can verify:
answer
#=> 1500
Exercise #2
Given a tuple that represents a date used in native Erlang functions, how would you bind a variable to the month value? The
format is {year, month, day}
Showing Solution
{_, month, _}
The month variable should be bound to the value 2 . You can verify:
month
#=> 2
It can also be written using placeholder names. The values are still disregarded, but it communicates what kind of value is in that
position.
Matching a List
A list is a common data structure. You’ll have lists of customers, orders, players, etc. You will need to process lists a lot. Because
of how often you’ll be working with them, let’s spend a little more time talking about a different way to think about lists.
There are 2 parts to this snake. The “head” and the “tail”.
Now imagine the snake has consumed a sequence of numbers. The first one eaten is the furthest inside. The last one eaten is still
in the head.
I want to process this list and pull off the “head” value. Using a Pattern Match, I can cut off the head of the snake and break it into
the two parts “head” and “tail”.
I’ve detached the head from the tail. Now the freaky part! The tail grows a new head!
In this way a list is recursive. When you remove the head, the tail is a new list and so has a head of its own!
existing = [2, 3, 4]
[1 | existing]
#=> [1, 2, 3, 4]
The same characters can be used in a Pattern Match to destructure a list and separate the list’s head from the tail.
existing = [1, 2, 3, 4]
[head | tail] = existing
head
#=> 1
tail
#=> [2, 3, 4]
Here we’re going to focus on using this tool for pattern matching.
Lists Matching
Let’s look at some examples for pattern matching a list.
What if I want to pull off the head of the list but I don’t care about the rest of the list. I only want the head. I might try this:
[head] = [1, 2, 3, 4, 5]
#=> ** (MatchError) no match of right hand side value: [1, 2, 3, 4, 5]
This won’t work because the pattern I defined on the left says it is a 1 element list. But that’s not what I want. By using the pipe | I
can separate the head from the tail. Since I don’t care about the tail, I’ll use the underscore _ to ignore it.
head
#=> 1
With this pattern I’m saying, “Bind the list’s head to head and ignore the rest”.
If I have a list of length 1, what happens? There are no numbers in the tail.
head
#=> 1
tail
#=> []
Remember a list is recursive. Even when there is nothing in the tail, I will receive a new list. In this case, it happens to be empty.
This allows me to Pattern Match a list with only 1 element in it too.
If I explicitly want to match against a list with only 1 element in it, I can this way:
[first] = [1]
first
#=> 1
Multiple Heads?
You are not limited to only putting 1 element in the head portion of the pattern [head | tail] . You just need to comma separate
them.
a
#=> 1
b
#=> 2
c
#=> 3
rest
#=> [4, 5]
The pattern [a, b, c | rest] says, “With a list of at least 3 elements, bind the 1st to a , the 2nd to b , the 3rd to c and whatever
remains bind to rest .” So this pattern will not match a list with two or fewer elements.
[a, b, c | rest] = []
#=> ** (MatchError) no match of right hand side value: []
[] = []
[head | tail] = []
#=> ** (MatchError) no match of right hand side value: []
The pattern is saying, “It must be a list with at least 1 element in it. It can have more, but must have at least 1.”
[a] = [1]
a
#=> 1
[a, b] = [1, 2]
a
#=> 1
b
#=> 2
[a, b, c] = [1, 2, 3]
a
#=> 1
b
#=> 2
c
#=> 3
[1 | rest] = [1, 2, 3, 4]
rest
#=> [2, 3, 4]
This pattern says, “It must be a list with at least 1 element and it starts with a 1 .” This pattern won’t match a list that starts with
something other than a 1.
[1 | rest] = [2, 3, 4]
#=> ** (MatchError) no match of right hand side value: [2, 3, 4]
head = 12
[^head | rest] = [12, 13, 14, 15]
rest
#=> [13, 14, 15]
head = 0
[^head | rest] = [12, 13, 14, 15]
#=> ** (MatchError) no match of right hand side value: [12, 13, 14, 15]
This pattern says, “It must be a list with at least 1 element and the first element must be the value bound to the head variable.
Bind whatever else is there to the rest variable.”
Recap
Quick review of some important properties of pattern matching a List.
A List is recursive. When we pull off the “head”, the “tail” is itself a list.
We can specify patterns that a list must match an exact number of elements or “at least” a number of elements.
We can use explicit value to define the shape of the data.
We can use the Pin Operator to define the shape of the data.
Understanding how to Pattern Match lists lets us do some pretty cool things. One cool thing is we can recursively process a whole
list using pattern matching without using a for or while style loop.
Exercise #1
Write the left hand side of this statement. Your goal is to bind the head of the list to a variable named head and the rest of the list
to a variable named tail . What does the left side look like?
_your_statement = [1, 2, 3]
Showing Solution
[head | tail]
head
#=> 1
tail
#=> [2, 3]
Exercise #2
Create a match that only matches when a list has a single item.
_your_statement = [10]
Your solution should not match with values on the right like these: [10, 20] and [11, 12, 13] .
Why would we want to only match in very limited cases? We don’t want to have the “greediest” match or a match that catches the
most it can, we want the most specific match that meets our requirement. Pattern matching lets us more explicitly and perfectly
match what we are looking for.
Showing Solution
[first]
[first] = [10]
first
#=> 10
Exercise #3
Create a match that only matches when a list has at least two items.
Create a match that matches and pulls the first 2 items off the list and ignores the rest. Bind the first 2 elements to variables
named a and b .
_your_statement = [1, 2]
Showing Solution
a
#=> 1
b
#=> 2
Practice Project
A practice project is available to make it easier to play with pattern matching in increasingly advanced scenarios.
The project has no other dependencies so if you already have Elixir installed then you are ready.
Intro to TDD
ThinkingElixir.com - Pattern Matching Reference (C) 2019 Mark Ericksen
Pattern Matching a Function Body: Intro Page 43 of 74
TDD stands for “Test Driven Development “. The idea with TDD is that you spend some up-front time thinking about what your goal
is with the code you will write. You first write a test that says, “given this situation and input, the code should cause this result or
return this output.” The test is written to “assert” that the desired behavior happened. If the code doesn’t, then the test fails.
In this project the tests have already been written for you. The tests assert that a function behaves in a specific, desired way.
Initially, all the tests are failing because the functions don’t behave correctly. The functions haven’t been implemented yet and
that’s your job!
The value of the tests is they help validate and check that your solution satisfies the requirements. They are additionally helpful
because they make it easy to continue to re-check your solution when you refactor or make other code changes.
Running Tests
Try running the tests for the whole project first. From a command-line terminal, go to the directory location where you downloaded
the files and run the following command:
mix test
You should see a lot of errors. That’s expected. You are seeing all the failing tests for the whole project. Don’t worry about that, I’ll
walk you through where to start working. We’ll focus on a single file at a time and within that file we’ll focus on just a couple tests
at a time.
With the ability to run tests in the practice project, you’re ready to start really digging in to pattern matching!
defmodule Testing do
def do_work(true) do
{:ok, "I did some work!"}
end
def do_work(false) do
{:ok, "I refuse to work."}
end
end
Paste that module into an IEx shell. Using tab completion, we can see there is only 1 function declared on our module.
Testing.do_work
#=> do_work/1
When we execute the function passing in true , pattern matching kicks in and the BEAM looks at the different function clauses we
defined. It starts from the top and if the pattern matches, that version of the function gets executed.
Testing.do_work(true)
#=> {:ok, "I did some work!"}
Testing.do_work(false)
#=> {:ok, "I refuse to work."}
As defining functions goes, a function definition with types, shapes, and values in the function declaration looks weird! Honestly,
until you understand pattern matching, even reading Elixir is confusing. I remember looking at Elixir code and not knowing how to
mentally parse what it was doing! No where is that more true than with functions. When function declarations include patterns,
they just don’t even look like “normal” code.
Once I understood what was going on and how pattern matching in functions works, then I thought, “Why isn’t every language
doing this?” The problem I had with reading Elixir code was because I had never seen pattern matching in a language before. That
won’t be your problem now!
Also, function overloading is often used in other languages to define a function with a different
number of arguments. Remember, in Elixir functions are very much “arity” based. So a different
number of arguments is a different function.
I don’t think it hurts to think of it as function overloading if that helps you, however, they are
different things.
defmodule Testing do
end
When writing a function clause this way, please note the def name(args), do: return_value syntax. There is no closing end for the
function. And note the , before the do and that the do: makes it an atom. You will see this code a lot but it has some special
little syntax differences to pay attention to.
Testing.do_work("abc")
#=> ** (FunctionClauseError) no function clause matching in Testing.do_work/1
#=>
#=> The following arguments were given to Testing.do_work/1:
#=>
#=> # 1
#=> "abc"
#=>
#=> iex:3: Testing.do_work/1
It isn’t a MatchError because we aren’t using the Match Operator. It’s a FunctionClauseError . We defined a do_work function with
two clauses. The error tells us that none of the clauses we defined match the data. The error is helpful because it shows us what
the data looked like that it couldn’t match against.
You already know how to bind to a variable without specifying a type or shape for the pattern. It’s the same thing here! In this
case, if we are given some data that we don’t know how to process, we’ll treat it as an error. Let’s see what that looks like:
defmodule Testing do
def do_work(other) do
{:error, "I don't know what to do with #{inspect other}"}
end
end
The addition of a new function clause that doesn’t define a pattern works perfectly for handling all the other data that can be
thrown at our function without causing a FunctionClauseError . Let’s throw some data at it and see what it does!
Testing.do_work("abc")
#=> {:error, "I don't know what to do with "abc""}
Testing.do_work(1)
#=> {:error, "I don't know what to do with 1"}
Testing.do_work(%{a_map: true})
#=> {:error, "I don't know what to do with %{a_map: true}"}
Testing.do_work([1, 2, 3])
#=> {:error, "I don't know what to do with [1, 2, 3]"}
Order Matters!
The order of the clauses really matters! The pattern matching stops when the first match is found. The data is “captured” by the
function. Even if a better or more exact match exists, it won’t be used if another function clause matches it first.
Let’s see what happens when we put the “flip-the-lid” version first…
defmodule Testing do
def do_work(other) do
{:error, "I don't know what to do with #{inspect other}"}
end
end
The first thing you’ll notice is we get 2 compiler warnings for the more specific function clauses.
warning: this clause cannot match because a previous clause at line 3 always matches
iex:7
warning: this clause cannot match because a previous clause at line 3 always matches
iex:8
The compiler can detect when a function clause will never be reached because the first clause doesn’t define a pattern. Try
executing the function passing in true .
Testing.do_work(true)
#=> {:error, "I don't know what to do with true"}
When we call the function with data that has a perfect match, it doesn’t get executed! It is captured by the first match
encountered. So order is very important. The order is checked goes top down.
Also note when a function has multiple arguments, it is possible to define function clauses where the compiler can’t tell it isn’t
what we actually want. It won’t warn us in every situation. It is up to us to pay attention to the order in which we define our
clauses.
defmodule Testing do
def do_work(value) do
if value == true do
{:ok, "I did some work!"}
else
if value == false do
{:ok, "I refuse to work."}
else
{:error, "I don't know what to do with #{inspect value}"}
end
end
end
end
Testing.do_work(true)
#=> {:ok, "I did some work!"}
Testing.do_work(false)
#=> {:ok, "I refuse to work."}
Testing.do_work("abc")
#=> {:error, "I don't know what to do with "abc""}
While this code may feel familiar, I strongly encourage you to avoid the temptation to fall back into this style. The “Elixir way” is to
prefer the use of a pattern matching over if conditionals. The code is flatter and easier to read. It values the pattern of the data
over the explicit process of “poking” the data to figure out its shape.
Compare the if conditional version above code to the pattern matching version. The Elixir-way is declarative, direct, clear, and
much easier to reason about. At least it’s easier to reason about now that you understand pattern matching and function clauses!
The tests we are focusing on are in test/tuples_test.exs . Running the following command will execute all the tests in this file.
Running all the tests now will show they all fail.
[...]
I’ll spare you the display of the failing tests. Run it for yourself and you can see it! We’re going to tackle them one at a time. For
the first one, I’ll walk you through it more step-by-step.
Exercise #1 – Tuples.day_from_date/1
In this exercise, we want to write a function clause that matches on a 3 element tuple and returns one of the values from it. In
Erlang, dates are represented as a tuple like this: {2019, 5, 30} . The function you need to write will match on this type of date
and return the day number from it.
First, run the test for this one by itself and see it fail and the message. You can do this by putting a : and the line number after
the filename. The line number needs to be any line number that’s inside the test. I just chose line 10.
Running this the first time will include a lot of compiler warnings about unused variables. Disregard those for now.
1) test day_from_date/1 returns the day number from an erlang date (PatternMatching.TuplesTest)
test/tuples_test.exs:9
Assertion with == failed
code: assert 5 == Tuples.day_from_date({2018, 9, 5})
left: 5
right: nil
stacktrace:
test/tuples_test.exs:10: (test)
The error is nicely laid out for us. In the console it is colored for improved readability.
With a failing test ready that demonstrates the correct working behavior, it’s your turn to write the code in the file being tested. In
your editor, open lib/pattern_matching/tuples.ex . Find the function we want to write the implementation for.
defmodule PatternMatching.Tuples do
@moduledoc """
Fix or complete the code to make the tests pass.
"""
def day_from_date(erl_date) do
end
[...]
end
Modify the argument to make it a Pattern Match and modify the body to the return the desired value. After making a change to the
file, re-run the same test (hit the up arrow in the console to repeat the previous command). Does your test pass? If not, correct
any mistakes and try again. If you have a passing test or you are totally stuck, check out the solution below.
Showing Solution
The function argument is a tuple. We only care about the day variable so the others are placeholders with a leading _ . The
function body returns the variable we care about, day .
Exercise #2 – Tuples.has_three_elements?/1
The test we want to focus on is in test/tuples_test.exs . If the line numbers haven’t changed, execute the failing test using:
Using a text editor, make changes to the function has_three_elements? in the file lib/pattern_matching/tuples.ex .
Make the test pass by using pattern matching in the function declaration. Create a second function clause that handles when it
doesn’t match the tuple. Keep re-running the mix test command for that specific test as you test your solutions.
Showing Solution
Two function clauses are used. One to match when the argument is a three element tuple and the other for anything else.
def has_three_elements?(_tuple) do
false
end
Exercise #3 – Tuples.major_us_holiday/1
The test we want to focus on is in test/tuples_test.exs .
Using a text editor, make changes to the function major_us_holiday in the file lib/pattern_matching/tuples.ex .
Make the test pass by using pattern matching in the function declaration. Create multiple function clauses to handle the different
cases.
Showing Solution
def major_us_holiday(_erl_date) do
"Uh..."
end
Note that the order of the 3 functions matching the number is not important. Any order is valid. However, the “Uh…” response
does need to be the last one as it will match with any value given it.
Exercise #5 – Tuples.greet_user/1
The first test to focus on is the “happy path” or “success” version of the function. The tests for this function are broken out into
two tests. The success and failure cases.
The function should be altered to return the a greeting with the user’s name when it is passed in using an {:ok, username} tuple.
A specific error text should be returned if given an {:error, reason} tuple.
Once you have the success version passing, write the failure handling version. That test is:
Showing Solution
Exercise #6 – Tuples.add_to_result/1
This one is also broken out into two different test cases. The first is the “success” or “happy path” version. The second test is how
it handles data it can’t operate on. Here are the two tests to focus on.
This represents a common pattern in Elixir. Receiving an :ok tuple, operating on the data and returning a new :ok tuple with the
modified result.
Iterate editing the file and running tests until they both pass.
Showing Solution
defmodule Testing do
def do_work(%{"email" => email} = params) do
IO.inspect email
IO.inspect params
"Sent an email to #{email} addressed to #{params["name"]}"
end
end
Testing.do_work(params)
#=> "john@example.com"
#=> %{"email" => "john@example.com", "name" => "John"}
#=> "Sent an email to john@example.com addressed to John"
In this code, we have a string-key map called params similar to what you’d get when handling a web request. The do_work
function takes the params in and does a pattern match on some of the data. It looks like this: do_work(%{"email" => email} =
params) . What’s special here is that we are pattern matching for some data we care about and still binding everything passed in
as params .
This is a common code pattern. You can use this same approach everywhere you pattern match. It works in case statements and
even in a straight forward match expression. Check this out:
%{"email" => email} = params = %{"name" => "John", "email" => "john@example.com"}
email
#=> "john@example.com"
params
#=> %{"email" => "john@example.com", "name" => "John"}
Yes, this may look strange, but when you break it down you can see what’s happening. The match operator performs a Pattern
Match of the left and right sides. When we say params by itself, we haven’t defined a type or a data shape so it just gets bound.
This is effectively what we’re doing with the map match in the function. We still bind everything to the variable params but we
also define the pattern (data type and shape) which also pulls out the data we care most about.
This can be very helpful like in my hypothetical function below. I want to Pattern Match and access a portion of the data when it is
passed in, but I still want the full customer data because I pass it on to another function called notify_customer .
defmodule Billing do
def apply_charge(%{id: customer_id} = customer, charge) do
record_charge(customer_id, charge)
notify_customer(customer, charge)
end
end
defmodule Testing do
def do_work(%{"email" => email} = _customer_params) do
# use `email`
end
end
Having the argument named _customer_params conveys meaning for what the developer was expecting to be passed in. That
meaning may not be clear from just looking at a function declaration that says, “ do_work(%{"email" => email}) do “. A map with an
email string key could be for a user, customer, a login, or something else.
Be kind to your future self an any other developers that work on your code base. Name your variables even when the code doesn’t
need it.
An example will help demonstrate this. First, we need some nested data to pattern match against. Remember, in this example we
are focusing on the function declaration and the pattern match it uses.
params = %{
"customer" => %{
"id" => 123,
"name" => "Willy Wonka Chocolates",
"bonuses" => %{
"employees" => %{
"Oompa 1" => 1_000,
"Oompa 2" => 2_000,
"Hillary" => 1_500,
"Oompa 3" => 500
},
"total" => 5_000
}
}
}
defmodule NestedBinding do
def award_bonuses(%{"customer" => %{"bonuses" => %{"total" => bonus_total} = bonuses}} = _params) do
IO.inspect bonus_total, label: "TOTAL TO VALIDATE"
IO.inspect bonuses, label: "BONUSES"
# TODO: validate intended total and employee amounts
:ok
end
end
NestedBinding.award_bonuses(params)
#=> TOTAL TO VALIDATE: 5000
#=> BONUSES: %{
#=> "employees" => %{
#=> "Hillary" => 1500,
#=> "Oompa 1" => 1000,
#=> "Oompa 2" => 2000,
#=> "Oompa 3" => 500
#=> },
#=> "total" => 5000
#=> }
#=> :ok
Notice that we can bind to a deeply nested map to get the bonus_total and bind a variable to the full bonuses map at the same
time!
The ability to bind to a nested piece of data works on all data types. However, it is most helpful when working with maps, primarily
because maps tend to have nested data more than other data types.
Practice Exercises
The following exercises continue using the Pattern Matching project. We will continue focusing on making a single test pass at a
time.
The tests we are focusing on are in test/maps_test.exs . Running the following command will execute all the tests in this file.
Running all the tests now will show they all fail.
[...]
You should be more comfortable with the TDD (Test Driven Development) approach here. Let’s get started with an easy warm-up.
Exercise #1 – Maps.return_name/1
The test to focus on is in test/maps_test.exs . Given a map with a :name key, return the value from the function.
Showing Solution
Exercise #2 – Maps.has_sides?/1
There are two tests to focus on. The first is the “success” path and the second is handing non-matching data. This is focusing on
validating the shape of the data with less focus on the values and binding variables.
Showing Solution
Exercise #3 – Maps.net_change/1
The test we want to focus on is in test/maps_test.exs . It is under “net_change/1” and the test is “subtracts beginning balance from
ending balance”. If you haven’t modified the file causing the line numbers to change, then using the following mix command will
work.
There are two tests for this function. This function takes a map, performs a calculation and returns the result in an {:ok, result}
tuple. The result is the difference of :ending_balance and :initial_balance .
Iterate on the code changes to make the tests pass. Use pattern matching in the function declaration.
Showing Solution
def net_change(_value) do
{:error, "Missing balance information"}
end
Exercise #4 – Maps.classify_response/1
This exercise is one of my favorites! This is based on several real-world experiences of mine. At some point, you will have a project
that integrates with some other external service. That external service may return XML or JSON but not in any standardized, clean
way. You are left dealing with whatever they give you. Even APIs returning JSON can be messy and not follow good REST practices.
This exercise is inspired by those experiences. You don’t get to control the data you have to react to. Your goal is to get the
meaning and result you need from the given data.
When I was learning Elixir and encountered a situation like this, I created my first-pass solution. It was a traditional imperative
step-by-step solution. Looking at my working solution, I wasn’t satisfied. I thought, “That doesn’t feel very Elixir-like yet”. I went
back, looked at more Elixir code, tried again. My second attempt still didn’t feel quite “right” yet. After my third attempt, I ended
up with a solution that “felt right”. It was elegant and I was elated!
I stress this point because you need to know that it’s okay to take multiple passes at a solution.
The test defines 4 sample responses from an external service. We want to be able to classify the response and identify when a
request was successful and extract the desired value. There are 3 examples of how the request might fail. We want to be able to
tell them apart as they will influence how the application responds.
The response data we need to look at is spread out throughout a nested map. Pattern matching lets you create an elegant
solution.
There are 4 tests for this function. One is the happy-path solution and the other 3 are for handling the different error conditions we
expect.
Matching Success
First, let’s detect when it succeeds. Our function should return that it succeeded with an {:ok, result} tuple which include the
extracted token. The test case include a comment that explains what about the data defines a success. The setup for the test
cases includes sample response data.
Make the test pass by using pattern matching in the function declaration.
Showing Solution
Showing Solution
def classify_response(%{"success" => false, "messages" => %{"general" => %{"result_code" => -1}}} =
_response) do
{:error, :invalid}
end
Showing Solution
def classify_response(%{"success" => false, "messages" => %{"general" => %{"result_code" => 3}}} =
_response) do
{:error, :retry}
end
Showing Solution
def classify_response(%{"success" => false, "account" => %{"status_code" => "3001"}} = _response) do
{:error, :frozen}
end
Summary
All the tests for classify_response/1 should be passing!
Take a look at the full solution (hidden below) and think how the same solution would look if done imperatively using nested if
statements! I love pattern matching! It makes the code declarative and clear. I feel like I’m a better programmer because of it.
When code is written this way, it is “declarative”. The code just “declares” the shape of the data it cares about. It basically says,
“When the data matches this specific shape, it means we retry.”
When the same problem is solved without pattern matching (like when using nested if statements), reading it becomes a exercise
of mentally parsing code to figure out what it’s doing. Instead of applying a single pattern definition for a match statement, it is
step-by-step poking the data to “feel” what the shape is. “Does it have this key? Yes? So then does it have this next key?”
As you become more comfortable writing code this way, you won’t want to go back to the old way. Code becomes elegant. Your
solutions become a declarative expression of what the data means. It’s easier to understand, easier to refactor, and more fun to
write!
In fact, you can write the pattern matching solutions to the tests using only maps! With the test code available, this is a good
chance to explore and understand how this works and why there is value in using structs when you can.
The project defines two structs for us to play with here. Feel free to check them out.
lib/pattern_matching/customer.ex
lib/pattern_matching/user.ex
You can also see a description of the structs in IEx. Start the IEx using iex -S mix to load the project into the IEx session. Using the
IEx helper t() , it can describe a type for us.
$ iex -S mix
Erlang/OTP 21 [erts-10.0.6] [source] [64-bit] [smp:4:4] [ds:4:4:10] [async-threads:1] [hipe]
Interactive Elixir (1.8.1) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> t PatternMatching.Customer
@type t() :: %PatternMatching.Customer{
active: boolean(),
contact_name: nil | String.t(),
contact_number: nil | String.t(),
location: nil | String.t(),
name: String.t(),
orders: list()
}
iex(2)> t PatternMatching.User
@type t() :: %PatternMatching.User{
active: boolean(),
admin: boolean(),
age: nil | integer(),
gender: nil | :male | :female,
hair: nil | String.t(),
name: String.t(),
points: integer()
}
There are some similarities between the Customer and User structs. They both have :name and :active keys. All the other fields
are different.
When we use IEx, the Elixir commands we enter are interpreted at runtime, they aren’t compiled at build time. We still get errors
for incorrect structs, but they look different. Let’s see an example of both:
alias PatternMatching.User
user = %User{car: "Toyota"}
#=> ** (KeyError) key :car not found
#=> (pattern_matching) expanding struct: PatternMatching.User.__struct__/1
#=> iex:2: (file)
This is a runtime KeyError trying to use a key that doesn’t exist on the struct.
Example: Compiled. Assumes you modify the code to be invalid and then try to start the project, which compiles it in the process.
$ iex -S mix
Erlang/OTP 21 [erts-10.0.6] [source] [64-bit] [smp:4:4] [ds:4:4:10] [async-threads:1] [hipe]
This is helpful in catching problems ranging from a simple typo to changing a struct definition where a key was renamed or
removed.
However, if I perform the match as a map instead of the struct, I have no protections or guarantees. This alone is a significant
benefit. It’s the difference of including the struct name or not.
It is totally valid to define a function clause that matches the struct just to have that assurance and protection. Here’s an example:
Inside this function clause I am guaranteed to have a Customer struct. This is opposed to a similar function declaration without it.
def do_work(customer) do
# work
end
This function calls the argument customer but there is no guarantee it is a customer. The code would likely be written with the
assumption it is a Customer struct but it implicitly depends on all callers to always only pass in a customer struct. When this
function is passed something other than a customer, it will likely result in runtime errors.
Calling this function and passing in a Customer struct won’t error, it will just never match! I’ve described a pattern that cannot
match a Customer. When the Customer struct type is included, I get a compilation error for the invalid key.
Keep in mind that it is completely acceptable to intentionally use a map to match a struct. In this way my code can be somewhat
polymorphic. I match on attributes of the data that multiple types can share. However, if I’m expecting the data to be Customer
type, then it is preferable to explicitly declare that.
Practice Exercises
The following exercises continue using the Pattern Matching project. We will continue focusing on making a single test pass at a
time.
The tests we are focusing on are in test/structs_test.exs . Running the following command will execute all the tests in this file.
Running all the tests now will show they all fail.
Please review the solutions below even if you feel confident with your answer. You may find additional insight in the explanations.
[...]
Exercise #1 – Structs.get_name/1
In this exercise you write the function get_name/1 that takes both a User and Customer struct.
There are 2 tests for this function. One is the happy-path solution and the other handles when some other data type is given.
Make the tests pass by using pattern matching in the function declaration.
Showing Solution
You can treat the data more generically as just a map. This lets you treat it somewhat polymorphically.
Exercise #2 – Structs.create_greeting/1
In this exercise you write the function create_greeting/1 that handles receiving a User and Customer struct differently.
There are 2 tests for this function. One is the happy-path solution that handles creating a customized greeting for a User or
Customer struct. The other handles when the User or Customer being greeted is inactive.
Make the tests pass by using pattern matching in the function declaration.
Showing Solution
The first Pattern Match handles when the subject to be greeted is inactive. It matches both a User and a Customer . The other two
function clauses match the struct type to return an appropriate and customized greeting.
Exercise #3 – Structs.deactivate_user/1
In this exercise you write the function deactivate_user/1 that handles receiving a User struct, modifying it and returning the
modified struct.
There are 2 tests for this function. One is the happy-path solution that handles creating an updated User struct. The other handles
when something other than a User is passed in.
Make the tests pass by using pattern matching in the function declaration.
Showing Solution
The first function clause matches on a User struct. It returns a new User struct with the value changed. The second function
clause matches when anything other than a User struct is passed in and returns an error.
There are multiple ways this function can be written to make the tests pass. Other possible solutions are included as comments.
The solution used here has the benefit of using the struct’s type to perform the update. Doing this gives compile-time checks on
the keys being used. The other options will not. Play around with it get comfortable with the way this works.
Practice Exercises
The following exercises continue using the Pattern Matching project. We will continue focusing on making a single test pass at a
time.
The tests we are focusing on are in test/lists_test.exs . Running the following command will execute all the tests in this file. Running
all the tests now will show they all fail.
[...]
Exercise #1 – Lists.is_empty?/1
In Elixir, a function can have the question mark character ? as part of the name. By convention, this is used to convey that it
returns a boolean result. This function works this way as well. If given an empty list, true is returned. For anything else it returns
false .
Showing Solution
Exercise #2 – Lists.has_1_item?/1
This function also returns a boolean result because of the ? in the name. If given a list with exactly 1 item, return true . For
anything else it returns false .
Showing Solution
Exercise #3 – Lists.at_least_one?/1
This function also returns a boolean result because of the ? in the name. If the list is not empty, return true . For anything else it
returns false .
Showing Solution
Exercise #4 – Lists.return_first_item/1
If the list is not empty, return the first item. If the list is empty, return the atom :error .
Showing Solution
Exercise #5 – Lists.starts_with_1?/1
If the list starts with a value of 1 , then return true . Any other initial value returns false .
Showing Solution
Exercise #6 – Lists.sum_pair/1
If the list has exactly two items, add them together and return the result. If the list doesn’t have exactly two items, return the atom
:error .
Showing Solution
Exercise #7 – Lists.sum_first_2/1
Given a non-empty list, take the first two elements from it, sum them together and make the summed value be the new head of
the list. If the list doesn’t have at least two items in it, return the original passed in value.
Showing Solution
This function uses the rest of the list to build the result. All of our other exercises here have ignored the tail of the list. This is part
of how we handle recursion. We pull off the head of the list, perform some work and then recursively pass on the rest of the list.
defmodule StringTests do
end
StringTests.match_greeting("Hello Tom")
#=> {:hello, "Tom"}
StringTests.match_greeting("Greetings Jane")
#=> {:greetings, "Jane"}
StringTests.match_greeting("Good morning!")
#=> {:morning, nil}
StringTests.match_greeting("Buenos dias")
#=> :unknown
This works when we know the exact prefix we are looking for. It is case sensitive and everything after the match gets bound to the
variable. This can be useful when working with a text-based protocol over TCP or UDP. For example, a command strings like the
following can be matched and parsed quickly.
"GET /url/endpoint"
"SAY Hey guys! How's it going?"
"POKE friend_user_name"
As you can see, matching at the end isn’t allowed. For a binary pattern match to work, it must know the size of the pieces being
matched, at least in the front. It can match on a known sized beginning and catch the rest of an unknown size at the end.
defmodule Formatting do
end
Formatting.date("20181230")
#=> "12/30/2018"
The pattern match uses the << >> characters to indicate it is a binary type. In this example we define a pattern that breaks the
data into 3 chunks using the variable names year , month , and day . With each specifying how large it is.
Defining a pattern like this lets the BEAM perform fast matches. It also lets us “declare” the pattern we want which allows us to
elegantly and quickly parse the data. It’s just so cool!
Practice Exercises
The following exercises continue using the Pattern Matching project. We continue focusing on making a single test pass at a time.
The tests we are focusing on are in test/binaries_test.exs . Running the following command will execute all the tests in this file.
Running all the tests now will show they all fail.
Remember to focus on the test file as a specification for what the code should do and what the sample inputs look like.
[...]
Exercise #1 – Binary.identify_command/1
In this exercise you write the function identify_command/1 that takes a string where the start of the text contains a text-based
command. This sort of thing actually exists like with the Hypertext Transfer Protocol v1.1 (HTTP 1.1) specification. This is, of
course, a dramatically simplified usage and test case.
There are 2 tests for this function. One is the ability to correctly identify the commands we care about and the other handles
unsupported commands.
Make the tests pass by using pattern matching in the function declaration.
Showing Solution
We only care about the basic categorization of two commands, “SAY” and “WAVE”. Anything else is an unsupported command and
results in an error.
Exercise #2 – Binary.format_phone/1
In this exercise you write the function format_phone/1 that takes a string containing a US-based phone number with no formatting
or special characters.
There are 2 tests for this function. One is the ability to correctly parse the input value and return a correctly formatted string. The
other handles inputs that don’t match.
Make the tests pass by using pattern matching in the function declaration.
Showing Solution
The order doesn’t matter for the two versions that match on different sized phone numbers. The pattern is specific enough to only
match one or the other. Of course the “I match anything” version must be last.
Exercise #3 – Binary.image_type/1
Binary data can be something other than a string. Image files often have a standardized header that describes the file. We can use
pattern matching to identify the header data of a file and classify it for us.
In this exercise you write the function image_type/1 that takes a binary (not a Unicode string), containing some image file
signatures.
There are 3 tests for this function. The first two handle correctly identifying a PNG and JPG files. The other test deals with
unsupported file signatures.
Make the tests pass by using pattern matching in the function declaration.
Showing Solution
The order doesn’t matter for the two versions that match on specific file signatures. The pattern is specific enough to only match
one or the other. Of course the “I match anything” version must be last.
You could also write the binary pattern directly into the function clause. However, using a “ Module Attribute” (the @png_signature )
is helpful to define them all in one place and then reference it in a function clause.
Note that Module Attributes are private to a module. They aren’t quite like declaring a constant that can be referenced outside the
module. Also, there are some module attributes with special meaning and compiler direction. However, that topic is outside the
scope of pattern matching.
Just to clarify, when you see <<137::size(8)>> , the 137 is an exact integer value for the match. There are a lot of options for how
to define the binary pattern match. Refer to the documentation for details.
Recap
If you are interested in going deeper on binary pattern matching, then there are many great resources and examples online. The
Elixir documentation for defining a bitstring is an excellent reference. There are other great examples of binary pattern matching
by others as well. For example, if you want to learn about matching on PNG headers, you can check out articles like this one.
The important thing to remember with binary pattern matching is where it works well and where it doesn’t. It works great in these
situations:
If you want to parse data from the middle or end of a string and there isn’t a predictable location for it, then you probably want a
Regular Expression. Luckily, you have that available in RegEx! You just can’t do that much work in a pattern match function
clause. Remember, when pattern matching, the BEAM is trying to answer the question, “Should I execute this function clause?”
Binary pattern matching is awesome! Just keep this tool in mind and know that this tool is available to you when you have a
suitable problem.
To define a guard clause, we use the keyword when . This is how the function declaration is defined and where the guard clause
goes.
Let’s look at a guard clause that checks if the value passed in is an integer.
defmodule Testing do
def greet_integer(value) when is_integer(value), do: "Hello #{value}"
def greet_integer(_other), do: "meh"
end
Testing.greet_integer(123)
#=> "Hello 123"
Testing.greet_integer("Jim")
#=> "meh"
You should note that the is_integer(value) guard clause includes the bound variable value from the argument. A guard clause can
be used on any bound variable to help define more about the pattern you want.
A guard clause expression must evaluate to true or false . It is being used to determine if the function should be executed.
This is where a guard clause can help us. Let’s look at our to_string/1 function we could create.
defmodule Testing do
end
Testing.to_string("123")
#=> "123"
Testing.to_string(123)
#=> "123"
Testing.to_string(:one)
#=> "one"
Testing.to_string(12.3)
#=> "12.3"
Notice that the first pattern match tests for is_binary(value) . An Elixir string is a binary, so this test determines there is nothing to
do and returns the value as-is.
The other function clauses test the data type using guard clauses. They use is_integer/1 , is_atom/1 , and is_float/1 .
The full list of supported types can be found on the Kernel module. They are the functions that start with is_* . Here’s a shorter list
to give you a convenient idea.
is_atom(term)
is_binary(term)
is_boolean(term)
is_float(term)
is_function(term)
is_integer(term)
is_list(term)
is_map(term)
is_nil(term)
is_number(term)
is_tuple(term)
Let’s do some practice exercises to play with guard clauses and matching data types.
The tests we are focusing on are in test/guard_clauses_test.exs . This first set should go pretty fast for you.
Exercise #1 – GuardClauses.return_numbers/1
ThinkingElixir.com - Pattern Matching Reference (C) 2019 Mark Ericksen
Guard Clauses – Additional Level of Matching Page 67 of 74
Given a variety of data inputs, return the value when it is a number. If not a number, return the atom :error .
Showing Solution
Exercise #2 – GuardClauses.return_lists/1
In previous exercises, you matched lists where they had various patterns like empty, a single item, at least 1 item, etc. There isn’t
a way to just match “if it is a list” without guard clauses. Now you can!
Given a variety of data inputs, return the value when it is a list. If not a list, return the atom :error .
Showing Solution
Exercise #3 – GuardClauses.return_any_size_tuples/1
In previous exercises, matching tuples was very specific to the number of elements in the tuple. Using guard clauses you can now
match if it’s a tuple of any size!
Given a variety of data inputs, return the value when it is a tuple. If not a tuple, return the atom :error .
Showing Solution
Exercise #4 – GuardClauses.return_maps/1
Given a variety of data inputs, return the value when it is a map. If not a map, return the atom :error .
TIP: After you have your solution, make sure to check out the hidden solution as it has an extra tip on how this can be
done!
Showing Solution
In keeping with the focus on guard clauses, this is that version of the solution.
Alternate Solution: Maps are somewhat special in that you can specify a pattern match like the following. It effectively says,
“It’s a map, but I’m not specifying any keys.”
Exercise #5 – GuardClauses.run_function/1
In Elixir, functions are first-class data types. It is common to pass functions as arguments. Using guard clauses you can check that
an argument is a function.
Given a variety of data inputs, if the argument is a function, execute the function and return the function’s result. If not a function,
return the atom :error .
TIP: When a function is passed as an argument, it is treated the same as an anonymous function. This example code shows how
to declare an anonymous function with no arguments that returns the value “Hello!” when executed.
Note: to execute an anonymous function, you use a period after the name. You also must include the parenthesis.
Showing Solution
Some operators are safe for guard clauses and let us define a range of values to accept in our pattern. Here’s a shortened list of
some of the most common and helpful operators.
==
!=
+
–
*
/
<
<=
>
>=
and
or
not
in
For the full list, see the “Guards” section of Kernel Module documentation.
A particularly interesting operator is the in operator. In guard clauses it can only work with ranges (ie 1..5 ) and lists. Here’s an
example of how this can be used in an interesting way.
defmodule TestingGuard do
def place_order(%{status: status} = order) when status in ["pending", "cart"] do
"Placing order for #{order.customer_id}!"
end
def place_order(_order) do
"Not placing order"
end
end
TestingGuard.place_order(order_1)
#=> "Placing order for 10!"
TestingGuard.place_order(order_2)
#=> "Not placing order"
Keep in mind that if you had a list with 100,000 items in it, this could impact your application as the in operator stops when the
first match is found, but if the value isn’t in the list, it’s an exhaustive search to determine that the function doesn’t match. But it
works great with small, bounded sets.
Likewise, the not operator can be very helpful. Expressions like value not in [1, 2, 3] and not is_nil(value) can be very helpful.
To be safe, the BEAM only permits a small set of “known safe” functions to be used in guard
clauses. This limited set of functions can still do a lot of powerful work for you.
Given a variety of User structs, we need to classify the user as a legal adult or a minor. For this example we’ll use the US definition
of age 18 and older to be a legal adult. A User between the ages of 0 and less than 18 is a minor. We should return an error if
given a non-user data type, a nil age or a negative age. The unit tests cover all these scenarios.
Make sure to check out the solution below after you have your own working code!
Showing Solution
The age 18 has special a business logic meaning here. Because it is referenced multiple times, it makes sense to declare it once
as a module attribute and use that for the references.
@adult_age 18
def classify_user(%User{age: age} = _user) when age >= 0 and age < @adult_age do
{:ok, :minor}
end
def classify_user(_user) do
{:error, "Not a user"}
end
Note that the first function clause matches a nil age. The solution here uses a guard clause to do this because that’s the topic
we’re covering here. However, it is more direct and clearer to use a nil value in the pattern match. In that version it would look
like this:
Elixir provides a helper command for creating our own custom guard clauses. The defguard command looks like this:
Let’s return to the GuardClauses.classify_user/1 example and see how we can improve our solution.
@adult_age 18
Custom guard clauses help to clarify the intent of a pattern match. The greatest benefit is when
it helps define a pattern that has business logic meaning in your application. Especially when you
would be repeating that logic in multiple places! If you aren’t reusing a guard clause in multiple
places, then it may not be adding value.
There is also a defguardp command that works similar to defp , creating a private guard clause that is only available in the
defining module.
defmodule PatternMatching.User.Guards do
@defmodule """
Define guard clauses for working with Users.
"""
@adult_age 18
All that’s left is to import this module into the modules where we want to use it. Those guard clauses are now available for use in
our functions!
defmodule PatternMatching.GuardClauses do
alias PatternMatching.User
import PatternMatching.User.Guards
defmodule Testing do
def compare_args(arg1, arg2) when arg1 == arg2, do: :equal
def compare_args(arg1, arg2) when arg1 > arg2, do: :greater_than
def compare_args(arg1, arg2) when arg1 < arg2, do: :less_than
end
Testing.compare_args(1, 1)
#=> :equal
Testing.compare_args(2, 1)
#=> :greater_than
Testing.compare_args(1, 2)
#=> :less_than
Any variable we bind in one argument can be compared to any value bound for another argument! Without guard clauses, this
would be done inside a function using an if statement. With guard clauses, we are able to keep the filtering of the data on the
boundary of the function clause. This lets us keep the function body straight-forward and clear.
Showing Solution
The max_age is passed in and is compared to the passed in user’s age.
def award_child_points(%User{age: user_age} = user, max_age, points) when user_age <= max_age do
%User{user | points: user.points + points}
end
Recap
Guard clauses add another powerful layer to pattern matching in Elixir. We covered a lot here, it’s worth taking a moment to
mention some of the highlights to keep in mind:
Guard clauses further define a pattern for data type and shape.
Custom guard clauses make reusable business patterns easy to use.
Guard clauses allow us to define a pattern that combines multiple function arguments.
https://hexdocs.pm/elixir/master/guards.html – Higher-level documentation on how guards work, what can be used in them,
how they fail, etc.
https://hexdocs.pm/elixir/master/Kernel.html#guards – Kernel module documentation on guard clauses. This is the list of
functions and operations defined on the Kernel Module that are safe to be used in guard clauses.
https://hexdocs.pm/elixir/Kernel.html?#defguard/1 – Documentation on defguard command
A good exercise is to look at some code you just wrote and ask, “Does this feel like Elixir code?” If not, look at some examples of a
good use of pattern matching and try a small refactor. Ask yourself again, “Does this feel like Elixir code?” After just a few
iterations it can really begin to change! What really needs to change is not the code, it’s the way you think about your code.
Pattern matching is a new tool you have to solve problems. You need to start thinking about your application as data, functions,
and the patterns in your data.
Having gone through the exercises and practice code, you have a good foundation to build on. Let’s review some of the things we
covered.
The pattern goes on the left of the Match Operator. The data goes on the right.
A Pattern Match can match the data’s type, shape, and bind variables to values all in a single statement.
A Match Error occurs when no match could be made.
Pattern matching goes from top to bottom. If the first pattern doesn’t match, the next pattern is checked and so on.
The first pattern to match wins and takes the data.
Make your top patterns more specific.
The “ ^ ” Pin Operator lets you reference the value of a variable in a pattern.
The “ _ ” lets you define shape without binding to the value.
A nested if statement is an anti-pattern
Lists are “linked lists” and it is cheaper to add to the front than it is to add to the end like an array.
Lists are recursive, a list is made up of a “head” element and a tail that is itself a list.
Strings can be pattern matched.
Guard clauses are another level of a pattern match. They can be also be used to match type and shape.
Guard clauses allow you to compare bound variables to each other in a pattern match.
Pattern matching is most effective when you think differently about your code.
You are on excellent footing now for continued building and growth. You are better able to read and understand Elixir code
because pattern matching truly is everywhere in Elixir.
I love Elixir because I feel I’m a better developer when I’m working in it. Tools like pattern matching that we covered here feel like
a super power! When I work in other languages where these tools don’t exist, I really miss it. The more you work with Elixir and
pattern matching, the more natural it becomes.
You are ready for the next step and I can’t wait to share it with you!
After learning how pattern matching works in Elixir, it’s time to see how pattern matching changes the way we control the flow of
our application code. The Code Flow course covers how Pattern Matching, Immutability and Functional Programming
impacts the foundation of programming:
Branching logic
Looping
Error handling
There are new control flow patterns to learn, new language features available, and new ways to do familiar things.
More than learning what the new things are and how to use them, you also learn when to use a technique and why to choose one
approach over another. You get hands-on experience with the concepts using a downloadable project with practice exercises.
You gain experience, understanding, and confidence in your ability to make meaningful contributions to a project and doing it the
“Elixir way”. Even more than now, you will be “Thinking Elixir”.