I’ve long advised people to learn multiple programming languages, as each new language you learn will make you better at all the ones you already know. Not just languages with different syntax, but languages that challenge how you look at problems.
For example, Java and C# are similar enough that moving from one to the other is really just learning a new syntax. Moving from Java to Ruby or to Lisp will force you to think differently and that’s where the magic happens.
Back in the mid-1990’s, I was working with C++ and trying to really internalize the concepts around object oriented (OO) programming. I understood the mechanics but was failing to really understand the value or why it was better than what we’d been doing before. Part of my struggle was that C++ made it easy to fall back on what I already knew and so taking that leap into something completely different was hard.
Then I got an opportunity to do six months of work in Smalltalk and that’s where OO became completely obvious to me. In Smalltalk, OO was everything - you couldn’t fall back on old habits so learning the new way became both necessary and easy.
Today, we’re starting to see concepts of pattern matching being added to languages like Java1 and Ruby2, just as OO had been added to C. But pattern matching is really a foreign concept to how these languages normally work, and just as with OO and C++, it’s too easy to fall back on old habits.
With that in mind, I’ve recently started working with Elixir. Pattern matching is deeply embedded in Elixir and really can’t be avoided, just as OO was deeply central to Smalltalk. Thanks to this, I’m now starting to really appreciate the pattern matching approach and I’m going to write up some thoughts as I go through this journey.
Let’s start by looking at FizzBuzz. This is a deliberately trivial example that’s often used as a starting point. Given a number…
- if that number is evenly divisible by 3 then return “Fizz”
- if it’s evenly divisible by 5 then return “Buzz”
- if it’s evenly divisible by both 3 and 5 then return “FizzBuzz”
If we were writing this in a language like java, we might write something like
public static String convert(int number) {
if( number % 3 == 0 && number % 5 == 0 ) {
return "FizzBuzz";
}
else if( number % 3 == 0 ) {
return "Fizz";
}
else if( number % 5 == 0 ) {
return "Buzz";
}
else {
return String.valueOf(number);
}
}
Let’s look at a first pass in Elixir that’s similar to that.
def convert(number) do
cond do
rem(number, 3) == 0 && rem(number, 5) == 0 -> "FizzBuzz"
rem(number, 3) == 0 -> "Fizz"
rem(number, 5) == 0 -> "Buzz"
true -> Integer.to_string(number)
end
end
Notes:
- The
cond
is like a switch where we evaluate the condition on each line. We can see that if it satisfies the condition before the arrow, we return the result after the arrow. - The elixir
rem(x,y)
function is a modula operator. It returns what is remaining after you divide x by y. - Return statements are less explicit than we find in java. The return from this function is the last statement evaluated, which is normal for languages like ruby or smalltalk but may seem weird if you’re coming from a C-like language.
This last example is fairly straight-forward but not very Elixir-like. A better Elixir solution would use pattern matching and this is where we start to really see the difference.
def convert(number) do
case {rem(number, 3), rem(number, 5), number} do
{0, 0, _} -> "FizzBuzz"
{0, _, _} -> "Fizz"
{_, 0, _} -> "Buzz"
{_, _, _} -> Integer.to_string(number)
end
end
Let’s unpack this.
- The case statement is taking a tuple of three numbers. The modula for 3, the modula for 5 and the number itself. In each statement below that, it does pattern matching, in sequence until it finds a match.
{0, 0, _}
will match only if the first two values are exactly zero. The third parameter can be anything.{0, _, _}
will match exactly zero for the first value and anything for the second and third.{_, 0, _}
will match anything for the first and third values and exactly zero for the second.{_, _, _}
will match any possible values for the three.
This was the first example of pattern matching that really challenged my assumptions, although it gets far more interesting as we go. This is where it no longer looks like a typical procedural or OO program.
Next up: Prime factors in elixir
-
Limited support for pattern matching was first introduced in java in 2020 and continues to be enhanced as part of Project Amber. It’s still marked as a preview feature, which means you have to specifically enable it. ↩
-
Early support for pattern matching was added to ruby in 2.7 in 2019 and continues to be enhanced. ↩