Idiomatic Ruby through Leetcode

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.

To me:

  • guards were people that protected malls
  • unless was a word I occasionally used in sentences
  • <=> looked like a typo.
Image for post
Image for post

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.

Leetcode Problem

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:

Generic Ruby

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:

  • -1 if the first element comes before the second element
  • 1 if the first element comes after the second element

More specifically, our custom compare method should return:

  • -1 if the first timestamp is less than the second one
  • 1 if 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.

Idiomatic Ruby

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.

Why use 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.

Conclusion

By using <=>, "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💪.

Image for post
Image for post

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 LogLine class.

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.

Since the 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 <=>method (which compares two elements). Sound familiar? 😉

Now 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!

Written by

Software engineer with a passion for clarity and brevity.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store