This is the first in a series of posts for helping Ruby devs understand some of the concepts in Elixir.
Pattern Matching
Pattern Matching is one of my favorite Elixir features. Let’s take a look. (using an Elixir Map)
%{foo: bar} = %{foo: "baz"}
The above is matching a pattern. Don’t think of =
as assignment, you
should think of =
as equality. The left-hand side of the =
is
equal to the right-hand side. Through pattern matching the variable
bar
is assigned the value "baz"
. Consider:
[foo, bar] = [1, 2]
foo
is assigned 1
and bar
is assigned 2
. Patterns can match to
any depth:
[foo, bar, [baz]] = [1, 2, [3]]
here foo
and bar
have the same value from the previous example but
baz
is now assigned the value of 3
. Alternatively if we had written:
[foo, bar, baz] = [1, 2, [3]]
baz
is now assigned the value of [3]
. This would be an example of a
semi-greedy matcher. You can expand upon this to greedily match the
entire statement:
my_list = [1, 2, [3]]
Now my_list
greedily matched to the entire right-hand side of the
=
. So why is this cool? Let’s take a look at a Ruby method that
has some conditions:
def foo(a, b, c)
if a == :something
...
elsif b == :other
...
else
...
end
end
The above is likely something familar to many Ruby devs. This presents some problems. Any methods with several code paths increases the complexity of the method. Complex methods can be difficult to test in isolation. Let’s take a look at how this would be implemented in Elixir:
def foo(:something, b, c) do
...
end
def foo(a, :other, c) do
...
end
def foo(a, b, c) do
...
end
The first question Ruby devs have is why are there three functions of the same
name? In Elixir you can define multiple functions of the same name as
long as the function signatures are unique. Functions are matched
against the values passed in. So foo(:something, 2, 3)
would match the
first foo
defined. foo(1, :other, 3)
matches the second. foo(1, 2, 3)
matches the third. Match priority is the order in which the
functions are defined.
Now our functions are concise, and focused on the very specific behavior. The conditional is obfuscated through the pattern matching but this is a common design pattern in Elixir so it should be embraced.
The pattern matching can be more complex:
def foo(%{foo: bar}, "baz") do
...
end
The above will match: foo(%{foo: "zeb"}, "baz")
but would not match
foo(%{foo: "zeb"}, "bar")
because the second argument does not match.
Take a look at the Elixir Pattern Matching Guide for more information.