Refactoring Elixir Code: If, Cond and Pattern Matching
I'm no where near an Elixir veteran but I found myself refactoring some imperative code into more idiomatic Elixir and it made me happy.
Over the last 20 years or so I’ve picked up and used Visual Basic 6, ASP Classic, PHP, C#, Ruby and Python. All of these languages could be described as being imperative languages (as opposed to functional languages).
There’s varying degrees of imperative and functional but none of that is important. I’m just trying to say that I never really learned a functional programming language before, short of skimming through Structure and Interpretation of Computer Programs (Lisp edition).
It’s also worth pointing out I’m tagging this under both Elixir and non-Elixir languages because I think the lessons learned by refactoring can be applied to all programming languages, the only difference is the syntax.
# Real World Example of Refactoring Elixir Code
I’m slowly but surely hacking my way through creating a custom video course hosting platform in Elixir and Phoenix (a web framework in Elixir). It’s worth pointing that I didn’t read any Elixir books, and have been skimming documentation as I go for everything.
The Feature: Extracting Initials from a First + Last Name
The platform will support having questions and answers for any lesson, similar to how GitHub deals with issues. On GitHub an issue can be opened up for a repo and each issue can have many comments. With this platform, a question can be opened up for a lesson and each question can have many answers.
That means I need a way to display a user’s name in the Q / A replies and from a usability point of view, being able to distinguish who someone is at a glance is very important.
You can optionally upload a profile image, but if you don’t upload an image it will fall back to using a randomly colored circle with your initials. Google does something very similar.
First Pass: Purely Imperative Approach Using If Statements
I’m a big of fan of getting something to work first because once you have it out of your mind and in code, you can see everything. At that point, refactoring it into something better isn’t too bad. You’ve already done the hard part which is to get the logic right.
Nested if statements for the logical win:
def initials(name) do
if name == nil or name == "" do
"?"
else
if String.contains?(name, " ") do
split_name = name |> String.split(" ")
first_letter = split_name |> List.first() |> String.slice(0, 1)
last_letter = split_name |> List.last() |> String.slice(0, 1)
"#{first_letter}#{last_letter}"
else
name |> String.slice(0, 1)
end
end
end
Just because Elixir is a functional language doesn’t mean it doesn’t have some imperative features, such as if conditionals.
My thought process here was, a name could have 3 states. It’s going to be empty, have a first and last name or just a first name.
I couldn’t find an else if
construct in Elixir so I ended up nesting
if statements. This code makes the most sense to my brain thanks to years
of working with imperative languages.
In fact, even if you don’t know Elixir but have experience with Ruby or Python I bet you could understand that code for the most part.
I ran with this for a while but it wasn’t pretty:
While the above code works, it’s not very maintainable. I mean sure, in this case it’s not too hard to keep a few lines of code in your head, but this pattern is pretty dangerous.
The problem is you have your conditionals mixed in with what your function is supposed to do. This makes figuring out what each condition is, or what it’s supposed to do pretty difficult at a glance. You’re forced to trace each line.
Second Pass: Say Hello to Cond
Elixir doesn’t have an else if
construct but it does have a thing called cond
.
Using cond to clean things up a bit:
def initials(name) do
cond do
name == nil or name == "" ->
"?"
String.contains?(name, " ") ->
split_name = name |> String.split(" ")
first_letter = split_name |> List.first() |> String.slice(0, 1)
last_letter = split_name |> List.last() |> String.slice(0, 1)
"#{first_letter}#{last_letter}"
true ->
name |> String.slice(0, 1)
end
end
Logically this is the same result as the if statement
approach in the first pass.
Personally, I think this is a little bit easier to read. The ugly nested if
condition is gone. You could say cond
is one of Elixir’s ways of dealing with
multiple conditions.
If any of those 3 conditions match, then the code under it gets executed. In the
last condition of true
, that catches everything. So if the name isn’t empty
or doesn’t have both a first or last name then it must be just a first name.
I read a little bit about pattern matching so I went on a quest:
When figuring out if whether or not I wanted to learn Elixir, I did watch a couple of Youtube videos, and saw examples of pattern matching but I haven’t tried it out yet.
This feature seemed like a great use case to try it out.
Third Pass: Pattern Matching with Elixir
It was surprisingly easy to set up.
def initials(name) when name in [nil, ""], do: "?"
def initials(name) do
split_name = name |> String.split(" ")
first_letter = split_name |> List.first() |> String.slice(0, 1)
last_letter = split_name |> List.last() |> String.slice(0, 1)
case Enum.count(split_name) do
1 ->
first_letter
_ ->
"#{first_letter}#{last_letter}"
end
end
Like the other 2 examples, this code does exactly the same thing, except this
time we’re using pattern matching on the initials
function.
If the name happens to be nil
or ""
then we return the question mark string
because the name happened to be empty.
Otherwise, we define the same function name but without the when
clause. Since
the empty case is already handled, now we can assume we have at least a first
name so we carry over our main logic from before.
This time around I also went for counting the number of elements in the split_name
list. I think it just makes it easier to see what’s going on here. If the count
is 1 then we know we only have a first name, otherwise, we use _
as a
catch-all for all other cases.
I feel like it creates a more ordered view of the 3 cases. We handle the empty case, then we handle the first name case, and finally we handle the first and last name case – all in order.
Now I see why pattern matching is so popular:
I never would have written this as a first pass as a beginner with Elixir because my mind didn’t think that way, but now that I’ve gotten to this result, I’m happy with it.
I don’t know if it’s the most idiomatic Elixir code ever written, but all I know is I find it to be much more readable and glanceable. That’s mainly due to most of the conditional logic being separated from what the function is supposed to do.
I’m going to run with this version of the code base until it becomes a problem.
But wait, can’t we refactor it even more?
I suppose.
We could extract the duplicated split_name |> List.last() |> String.slice(0, 1)
logic into a function and also put that case statement into its own function but
is it really worth it?
From a code readability point of view, having 6 or so lines of code in 1 spot makes the code very readable. I’m not 100% convinced splitting everything out will make this specific bit of code better.
What would you have done differently? Let me know below!