OO to Elixir: Clean Code With Pattern Matching
Tags: elixir, ruby, cleancode, refactorit
Published: 2024-01-29 17:30:19

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:
```ruby
irb> a, b, c = [:hello, "world", 42]
irb> a
:hello
irb> b
"world"
irb> c
42
```

Per the [Elixir pattern matching docs](http://elixir-lang.github.io/getting-started/pattern-matching.html) you can do the same thing and assign multiple variables to a tuple:
```elixir
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:
```elixir
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`.
```elixir
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:
```ruby
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](http://elixir-lang.github.io/getting-started/modules-and-functions.html#named-functions) give this example:
```elixir
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:
```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!
```elixir
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:
```ruby
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.