If you’re someone who’s sick of writing huge IF and CASE statements, you’re not alone. As a relative newbie to functional languages, pattern matching is something I really enjoyed. It’s a great way to write idiomatic code - without the need for those long nested conditional statements.
Pattern matching is definitely not something new. It’s existed for decades as a powerful functional programming technique. I personally enjoy the way Elixir implements it, so much so that I always look out for ways to use it.
So, what is pattern matching? The name reveals as much - it’s a way to process and extract data based on its structure. With it, the “=” operator works more like a matching operator than an assignment operator. Let me show you what I mean.
It’s not the assignment operator anymore!
With pattern matching, the “=” operator is used to match values on either side. For example, if you were to do,
iex(1)> 4 = 5
** (MatchError) no match of right hand side value: 5
You get a “MatchError” exception. Here, Elixir is not trying to assign the value of “5” to “4”, but rather seeing if they’re equal.
If you’re associating this with the common “==” parameter, well you’re almost there. Technically MatchError could be equated to false, but the error information gives us more detail into what is exactly not matching. More on this down below!
Let’s see what happens when we try it with a variable.
iex(4)> x = 4
4
iex(5)> 5 = x + 1
5
iex(6)> 5 = x + 2
** (MatchError) no match of right hand side value: 6
In the first statement, x gets assigned the value of 4 because x is nil/null/empty.
In the second statement, x retains the value of 4 and since both sides match, we get no error.
However in the last statement, a MatchError is raised.
What’s really great is that you can use this across different data types,
Here’s another example,
iex(7)> [1, x, 5] = [1, 10, 5]
[1, 10, 5]
iex(8)> x
10
This is incredibly useful in parsing JSON responses,
iex(9)> %{topic: x} = %{name: "Swaathi", company: "Skcript", topic: "Elixir", source: "Internet"}
%{company: "Skcript", name: "Swaathi", source: "Internet", topic: "Elixir"}
iex(10)> x
"Elixir"
It works great with nested lists too!
Matching in case statements
Pattern matching in case statements makes the code so much more idiomatic. For example,
iex(6)> case {4, 5, 6} do
...(6)> {4, 5} -> "One"
...(6)> {4, 5, x} -> "X is #{x}"
...(6)> _ -> "Three"
...(6)> end
"X is 6"
You can see that not only the pattern is matched, but the missing element is also assigned to the variable “X”.
You can pattern match in if statements too - but it’s really frowned upon in the Elixir world. Generally if statements are less idiomatic and extremely limited as compared to case statements so you’ll see them used only in really rare scenarios.
Control the flow of logic
As someone who used to worship OO programming, I tried to bring most of those concepts into the functional world. I quickly realized the error in my ways.
Let’s take an example of a payment library. In the OOP world, there would be shared state across objects and a function for each payment method. Something like this,
class Payment
attr_accessor :amount
def initialize(amount)
self.amount = amount
end
def pay_with_paypal(username, password)
...
end
def pay_with_card(number, expires_on, cvv)
...
end
end
And then to execute, I would have called this,
p = Payment.new(100)
p.pay_with_paypal(“xxx”, “xxx”)
Or,
p.pay_with_card(“xxx”, “xxx”, “xxx”)
Now my OOP brain would convert this into FP like so,
defmodule Payments do
def pay_with_paypal(login: login, password: password) do
...
end
def pay_with_card(number: number, expires: expires, code: code) do
...
end
end
Well looks about right, no?
Not exactly. In Elixir, you can pattern match in functions too! This proves to be extremely useful when the function that needs to be executed is dependent on the parameters passed. The more idiomatic way to write this code would be,
defmodule Payments do
def pay(:paypal, login: login, password: password) do
...
end
def pay(:card, number: number, expires: expires, code: code) do
...
end
end
Here your function name remains the same (since you’re trying to pay at the end of the day), and the function that gets executed will be based on the first parameter.
Like so,
Payments.pay(:paypal, login: "xxx", password: "xxx")
What’s great about pattern matching in functions is that you can extract the parameter value as and when you’re determining what function to execute.
def run(%{state: "ready"}, ctx) do
...
end
def run(%{state: "pending"}, %{initialized: false } = ctx) do
...
end
In the first function, the value of ctx could just about be anything, however the second function will only get executed when the second parameter matches the pattern of “{initialized: false}” (when “initialized” is false).
It doesn’t end there. You can tag on “guard” statements too to functions!
def run(%{state: "ready", data: data}, ctx) when length(data) > 0 do
...
end
Now this function will execute only when “state” is ready AND the length of “data” is greater than zero. How useful is that! (Now you might start realizing why if statements are frowned upon.)
If the calling function matches none of these statements, Elixir will throw an error. However if you want to catch that too, you can just do this.
def run(_, _) do
...
end
Here the “underscore” parameter acts as a catchall and will execute when none of the other functions match. Be careful to place this at the very end as the order of matching is top to bottom.
Conclusion
Pattern matching is a great way to write idiomatic functional code. It’s a powerful tenant of functional programming that makes it a joy to write conditional statements. Once you master it, you can clean up your code and optimize it in tons of places!
Top comments (0)