Skip to content

Instantly share code, notes, and snippets.

@MrAlexLau
Last active January 8, 2018 16:33
Show Gist options
  • Save MrAlexLau/3966f955ddcca381f7f4971bac78d980 to your computer and use it in GitHub Desktop.
Save MrAlexLau/3966f955ddcca381f7f4971bac78d980 to your computer and use it in GitHub Desktop.
Elixir for Rubyists: 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 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}

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 method 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 method itself (whether it's error details or a user object). Rather than the equivalent ruby approach where you have to call a separate errors method to get the error details.

Pattern Matching Method Signatures

Pattern matching becomes especially powerful when used in method signatures since narrows the scope of the method before the actual body of the method is executed. The Elixir docs 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 method, whereas calling Math.zero?(1) would call the 2nd version of the method. Although this example is trivial, it's easy to see how narrow the scope becomes for each of the method bodies. By the time you reach the code in the body of the zero?(0) version of the method, 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 method. 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 methods!

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 OO approach is:

a_paladin = Paladin.new()
a_paladin.set_base_damage!(1)
a_paladin.damage(:bow) #=> 1 + 5 = 6
a_paladin.damage(:sword) #=> 1 + 10 = 11

a_wizard = Wizard.new
a_wizard.set_base_damage!(1)
a_wizard.damage() #=> 1 + 12 = 13

a_generic_character = CharacterClassThatBothWizardAndPaladinInheritFrom.new
a_generic_character.set_base_damage!(1)
a_wizard.damage() #=> 1

Another way to think about what pattern matching in method signatures does is affords you the convenience of specificity that object oriented models has, but at the method level rather than the class level.

@kmcq
Copy link

kmcq commented Jan 8, 2018

Interesting! How do you feel about the readability of this? I've only experienced methods with the same name doing different things based on how they're called in Java before and found it had some ramp-up to get used to that. For example when grepping for a method you'd get multiple definitions in the same file and potentially have to read each -- pretty similar to reading one method with some if/else in it I guess.. but yea just curious if that's been hard to get used to.

I do really like tho how it flattens the logic into single if this and this and this statements; seems like this would be readable and very exact.

Thanks for writing this up! I feel like I have a good grasp on it now 😃

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment