I spent the last three years working at a company with a Ruby on Rails monolith. 90% of my time was spent developing new micro-services in Elixir. 10% was spent making minor changes to the monolith in Ruby. As such, I didn’t really know anything about Ruby.
guardswere people that protected malls
unlesswas a word I occasionally used in sentences
<=>looked like a typo.
But after starting to look for a new job and cranking through some Leetcode problems, my Ruby got better. With every new linked list I reversed or binary search tree I sorted, my code got more concise and more idiomatic.
This blog post goes over one Leetcode problem I worked on. My initial solution was very generic, barely Ruby. More Rubbish than Rubyish. More sapphire blue than ruby red.
After some minor refactors, my solution became much more idiomatic. It was very satisfying and it would be a pleasure to read for any interviewer sitting on the other side of the Zoom call.
This problem is a modified version of https://leetcode.com/problems/reorder-data-in-log-files/.
You are given some log lines where each line has a timestamp followed by a message.
You need to sort the log lines, first by timestamp, then by message (in the cases when the timestamps are equal). For the sample input above, you should return:
You can solve this problem by sorting with a custom compare method. When comparing two log lines:
- get the timestamp and message from both lines
- compare the timestamps
- if the timestamps are equal, compare the messages
To get the timestamp and message from a log line, we can find the first space character and we know the timestamp is to the left and the message is to the right.
To sort the log lines, we can make a custom compare method which takes in two elements and returns:
-1if the first element comes before the second element
1if the first element comes after the second element
More specifically, our custom compare method should return:
-1if the first timestamp is less than the second one
1if the first timestamp is greater than the second one
- if the timestamps are equal, compare the messages
Then we can
sort the logs using the
compare method we defined.
Running our code with the sample input:
This code works but it’s quite generic — you could write very similar looking solutions in many other programming languages. It’s also rather long which is usually a smell.
In Ruby, there is a built-in operator that compares two elements —
<=>, the "spaceship" operator.
a <=> b returns
-1if a < b,
0if a == b and
1if a > b.
We can use
<=> to simplify the code that compares the timestamps and messages.
Here’s the original code.
Here’s the updated code using
We collapsed the original code from 11 lines down to 5 lines.
“Guards” are used to exit a method early by returning a value based on some condition —
return 1 if a != b. Guards make your code more concise since you can avoid having to write an explicit
else block. The code that follows the guard can assume the condition isn't true.
We can add a guard to compare the timestamps if they aren’t equal. The code that follows can compare the messages since it’s implied that the timestamps are equal.
Adding the the guard further collapsed the code from 5 lines to 2 lines.
unless is a conditional statement in Ruby that will execute code if the condition is false. It's equivalent to "if not".
return 1 if a != b is equivalent to
return 1 unless a == b.
unless? Sometimes it’s easier to express your ideas using a false condition rather than a true one. Recall the problem description:
You need to sort the log lines, first by timestamp, then by message (in the cases when the timestamps are equal)
We want to compare the timestamps,
unless they’re equal, in which case we want to compare the messages. Expressing this in Ruby is trivial.
<=>, "guards" and
unless, we went from a generic, Rubbish solution:
To an idiomatic, Rubyish solution:
It has a guard, and a spaceship, protecting our code💪.
The second solution is immensely satisfying — it is half as long as the first solution and it leverages many built-in Ruby concepts.
My potential Ruby interviewers will be able to easily understand that solution and they will appreciate how idiomatic it is.
Bonus: Classes and Mixins
Ruby is an object-oriented programming language. We can modularize our code by defining a
LogLine class with a
compare method to compare one
LogLine to another.
Then we can update
sort_logs to use our new
This object-oriented solution builds upon our idiomatic solution by adding a class which allows for better modularization.
But we can improve it even further. “Mixins” in Ruby let you extend your classes by adding additional functionality.
For example, the
Comparable mixin is used by the
Array class to add support for comparing two
Array objects. It also adds support for sorting.
Array class uses the
Comparable mixin, you can write code to compare and sort
Array objects like:
[1, 2, 3] == [1, 2, 3] # true
[1, 2, 3] == [3, 2, 1] # false
[3, 2, 1].sort # [1, 2, 3]
We can add the
Comparable mixin to our
LogLine class by defining the
compares two elements). Sound familiar? 😉
sort_logs can just call
.sort! on the array of
LogLines without having to specify a custom sort method. Our
≤=> method will automatically get used by the Comparable mixin to compare elements.
It doesn’t get much cleaner than that!