Quick & Easy Elixir Refactorings — Part 3
In the last part we looked at a technique for refactoring away conditionals at the end of your functions and before that we looked at using pattern matching to eliminate conditionals from the top of your functions.
If you’ve not checked them out yet I recommend you do
Part 1 — Functions starting with a conditional
Part 2 — Functions ending with a conditional
In this post we’ll be looking at how we can clean up some code where two or more functions need to have the same start and end but something slightly different in the middle.
Conditionals in the middle
There’s couple of variants to this kind of thing I can think of. These usually come up when we are requested to add a feature to something already in place but is something a little bit different. You know, that special case 😉
Lets say we have a function that looks something like this
defmodule Example do def parse_name(email) do
|> String.split("@")
|> hd()
|> String.split(".")
|> Enum.join(" ")
endend
We get an email address, take the bit before @ symbol, separate on a full stop to get first name and last name and join them back together to make a full name. There’s better ways of doing that but it’s good for our purposes here.
Say now an additional requirement came in and we need to sometimes capitalise the first name and last name.
First thing we could do is
def parse_pretty_name(email) do
email
|> String.split("@")
|> hd()
|> String.split(".")
|> Enum.map(&String.capitalize/1)
|> Enum.join(" ")
end
Identical to the existing one apart from the additional 4th line in the pipeline with call to String.capitalize/1
Doesn’t feel quite right duplicating all that other logic. So alternatively we could pass something like an option to the function
def parse_name(email, pretty \\ false) do
names = email
|> String.split("@")
|> hd()
|> String.split(".") if pretty do
names = Enum.map(names, &String.capitalize/1)
end
Enum.join(names, " ")
end
Doesn’t look very nice and if we compile this the Elixir compiler gives us a warning about setting a variable inside a conditional and gives a recommendation to fix it to something like this
def parse_name(email, pretty \\ false) do
names = email
|> String.split("@")
|> hd()
|> String.split(".") names =
case pretty do
true -> Enum.map(names, &String.capitalize/1)
_ -> names
end Enum.join(names, " ")
end
Aaaaaaaa…it’s getting worse! No compiler errors though 😉
Let’s have a look at an alternative where we’ll extract the bit that is different away.
def parse_name(email) do
do_parse_name(email, &(&1))
enddef parse_pretty_name(email) do
do_parse_name(email, &capitalize_names/1)
enddefp do_parse_name(email, transform) do
|> String.split("@")
|> hd()
|> String.split(".")
|> transform.()
|> Enum.join(" ")
enddefp capitalize_names(names) do
Enum.map(names, &String.capitalize/1)
end
So we’ve gone back to having a pair of functions with prescriptive names rather than options, although we could have kept the same name and pattern matched for the option here instead.
We’ve created a new private function which takes as it’s arguments the email but also a transform function which it then calls in the pipeline.
Our first function parse_name/1
now just calls the new private “do” function and passes in an anonymous function which simply returns whatever argument it gets essentially not doing anything.
The second function calls the same “do” function but uses function capturing to pass in the new capitalize_names/1
private function as the transform function. Neat 👌
I’d probably leave it at that but we could go one step further if we wanted by having the “do nothing” anonymous function as default value for the transform argument 💥
def parse_name(email, transform \\ &(&1)) do
|> String.split("@")
|> hd()
|> String.split(".")
|> transform.()
|> Enum.join(" ")
enddef parse_pretty_name(email) do
parse_name(email, &capitalize_names/1)
enddefp capitalize_names(names) do
Enum.map(names, &String.capitalize/1)
end
What I like about the final solution is that we’ve kept the neat looking easy to follow pipeline, we’ve eliminated conditionals, we’ve eliminated duplicated code and we’ve extracted the part that is different to a function on it’s own with a nice descriptive name.
This technique is great if you have some conditionals in the middle of your functions ruining your beautiful pipelines or lot of code duplicated around the differentiating piece of code.
Please do click the 👏 icon below if you liked this post and follow me here or on Twitter @efexen to be notified of the next part where we’ll look at more simple tricks for leveling up your Elixir code 👍