DEV Community

Cover image for OO to Elixir: Clean Code With Pattern Matching
Alex Lau
Alex Lau

Posted on

OO to Elixir: Clean Code With Pattern Matching

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (2)

Collapse
 
aaronblondeau profile image
aaronblondeau

The entire concept of pattern matching functions was new to me - thanks for posting! Once your book is done, maybe it's time to create an Elixir course?

Collapse
 
keep_calm_and_code_on profile image
Alex Lau

@aaronblondeau that would be really fun! I'd love to explore making a course in general. I actually haven't used Elixir for a while but I find myself missing it more than I would have expected. There are one or two side projects I think could be good matches for using it and could be good candidates for course material as well, so fingers crossed I get to that point in the future!