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 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.
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 😃