One of the joys of working with new programming languages is uncovering new ways to solve problems. New patterns and tools within languages expand your horizons in terms of how to structure code.
A few years ago, I went from working with Ruby to working with Elixir and was very impressed with how the concept of "pattern matching" allowed me to write elegant functions. Even if you don't know Ruby or Elixir, the explanations below should be simple enough to demonstrate the power of pattern matching.
Pattern Matching Tuples
In Ruby, although it's not a particularly common pattern, you can assign multiple variables on the left hand side of an assignment:
irb> a, b, c = [:hello, "world", 42]
irb> a
:hello
irb> b
"world"
irb> c
42
Per the Elixir pattern matching docs you can do the same thing and assign multiple variables to a tuple:
iex> {a, b, c} = {:hello, "world", 42}
{:hello, "world", 42}
iex> a
:hello
iex> b
"world"
iex> c
42
However, in Elixir you can also specify explicit literals on the left hand side of the assignment that will only perform the assignment when the right side matches that literal:
iex> {:hello, b, c} = {:hello, "world", 42}
{:hello, "world", 42}
iex> {:not_hello, b, c} = {:hello, "world", 42}
** (MatchError) no match of right hand side value: {:hello, "world", 42}
Knowing this, a common pattern in Elixir is to return a tuple that begins with :ok
.
case update_user(params) do
{:ok, user} ->
"#{user.name} was updated."
{:error, errors} ->
"This clause will match when there is an error and update_users can populate the errors variable"
_ ->
"This clause matches any unmatched paths in this case statement"
end
Whereas in Ruby, you may write something like this:
if user.update(params) # assume this returns a boolean
"#{user.name} was updated."
elsif user.errors.any?
"The user class can hold errors in its corresponding function in the object"
else
"Something went wrong with the update"
end
One thing that's nice about the Elixir approach is that you get all of the data you need from the update_user
function itself (whether it's error details or a user object). Rather than the equivalent ruby approach where you have to call a separate errors
function to get the error details.
Pattern Matching function Signatures
Pattern matching becomes especially powerful when used in function signatures since it narrows the scope of the function before the actual body of the function is executed. The Elixir function documentation give this example:
defmodule Math do
def zero?(0) do
true
end
def zero?(x) when is_integer(x) do
false
end
end
In this case, Math.zero?(0)
would execute the first version of the function, whereas calling Math.zero?(1)
would call the 2nd version of the function. Although this example is trivial, it's easy to see how narrow the scope becomes for each of the function bodies. By the time you reach the code in the body of the zero?(0)
version of the function, you're guaranteed to know that the value being passed in is zero.
Conditionals are less frequently needed in Elixir since you can just pattern match against arguments and have multiple versions of the same function. Take this example in ruby:
def character_damage(character_type, weapon, base_damage)
# bonus damage is zero by default
bonus = 0
if character_type == :paladin
if weapon == :bow
bonus = base_damage + 5
elsif weapon == :sword
bonus = base_damage + 10
end
elsif character_type == :wizard
# wizards always get a bonus
bonus = 12
end
base_damage + bonus
end
In Elixir, you could rewrite all of these as a bunch of 1 line functions!
def character_damage(:paladin, :bow, base_damage)
base_damage + 5
end
def character_damage(:paladin, :sword, base_damage)
base_damage + 10
end
# _ matches anything
def character_damage(:wizard, _, base_damage)
base_damage + 12
end
# if none of the other signatures match, default to no bonus damage
def character_damage(_, _, base_damage)
base_damage
end
An alternative way to write the ruby example with a more object-oriented approach is:
paladin = Paladin.new()
paladin.set_base_damage!(1)
paladin.damage(:bow) #=> 1 + 5 = 6
paladin.damage(:sword) #=> 1 + 10 = 11
wizard = Wizard.new
wizard.set_base_damage!(1)
wizard.damage() #=> 1 + 12 = 13
generic_character = GenericCharacter.new
generic_character.set_base_damage!(1)
generic_character.damage() #=> 1
Bringing It Together
I love pattern matching because it allows me to write very concise, specific functions. Coming from a background of working in object-oriented languages, another way to think about what pattern matching in function signatures does is affords you the convenience of specificity that object-oriented models has, but at the function level rather than the class level.