2019-08-02 09:02

Writing Better Ruby (Part 1)

While reading the excellent Ruby Style Guide I came across best practices I often forget while writing Ruby. In order to plasticize my brain, and perhaps help you prevent making the same mistakes, I wrote down ten of them. I'll probably create a follow-up post with additional mistakes.

1. No non-nil checks

Don’t do explicit non-nil checks unless you’re dealing with boolean values – style guide

There's no need to call !obj.nil? or test for nil inequality unless you want to check whether a Boolean value is really not nil i.e. whether it has been set to true or false.

I suspect I sometimes add a useless nil? to make it explicit to the reader[1] that I really mean to test for nil

Ref [1]: I always write code thinking John Carmack will review my code and yell at me – I swear this helps.

For Rails there are additional methods that test for nil such as blank? which kind of softens the "badness" of making explicit (non-)nil checks since they test more than just nil. If you look at blank.rb you'll see that Rails adds blank? to 9 classes. Frankly I've always had a slight dislike of blank? because it kind of says "I don't really know what kind of state this variable is in so I test for a broad set of conditions".

2. Prefer __send__

Prefer __send__ over send, as send may overlap with existing methods – style guide

Since send is often used as a method name it's likely to override Ruby's BasicObject send. If you use __send__ then you take away the ambiguity.

3. No method_missing

Avoid using method_missing for metaprogramming because backtraces become messy, the behavior is not listed in #methods, and misspelled method calls might silently work – style guide

The Ruby Style Guide article explains how to use method_missing if there's really no alternative.

Since Ruby 1.9 there is Proc#curry which can be used in cases where you would otherwise have used method_missing. This article gives examples of curry. The article also explains the difference between partial application and currying.

4. Non-Capturing Regexp

Use non-capturing groups when you don’t use the captured result – style guide

The article gives two examples:

# bad
/(first|second)/

# good
/(?:first|second)/

A good example of using non-capturing groups is extracting the domain name from a URL.

For instance this regex (https?:\/\/)([^\/\?\&]+) yields two match groups:

1. https://
2. example.com

When you turn the first group into a non-capturing group via (?:https?:\/\/)([^\/\?\&]+) it only yields what you're looking for:

1. example.com

Granted, this tip is not Ruby-specific but still it's something I tend to forget.

5. Date & Time

Prefer Time.now over Time.new when retrieving the current system time – style guide

and

Don’t use DateTime unless you need to account for historical calendar reform - and if you do, explicitly specify the start argument to clearly state your intentions.

Although the style guide itself doesn't provide the reason for choosing Time over DateTime, this Gist does.

6. Named Format Tokens

When using named format string tokens, favor %<name>s over %{name} because it encodes information about the type of the value – style guide

The article gives two examples:

# bad
format('Hello, %{name}', name: 'John')

# good
format('Hello, %<name>s', name: 'John')

This left me scratching my head for a second because I didn't know about the "good" way, but it makes total sense now because just like sprintf the s in %<name>s indicates you're dealing with a string. Passing an integer will throw an ArgumentError.

7. Don’t Abuse gsub

Don’t use String#gsub in scenarios in which you can use a faster and more specialized alternative – style guide

You have three choices: tr, sub, and gsub:

tr

tr is the most bare bones one. It stands for translate characters, and is named after the tr command in Unix.

It can do things like:

"hello".tr('el', 'ip') #=> "hippo"

and

"hello".tr('a-y', 'b-z') #=> "ifmmp"

sub

Ruby doc states: [..] Returns a copy of str with the first occurrence of pattern replaced by the second argument.

It can do things like this:

hello".sub(/[aeiou]/, '*') #=> "h*llo"

Nice touch: sub() supports blocks:

"hello".sub(/./) {|s| s.ord.to_s + ' ' } #=> "104 ello"

gsub

This is the Swiss Army knife of string replacement. The docs state: Returns a copy of str with all occurrences of pattern substituted for the second argument.

The Ruby Style Guide makes the case for not abusing gsub because 1) tr and sub can be substantially faster, and 2) it's a good habit to use the smallest, fastest, and most specialized tool available.

I ran a naive benchmark on a 10k line text file to see the difference between the three methods. The sub methods wins (of course, it only substitutes a single character per line in this benchmark), then comes tr, and then gsub.

8. No to_s

Don’t use Object#to_s on interpolated objects. It’s invoked on them automatically – style guide

I find myself making this mistake in my quest to be specific (again, to the reader of my code) but it's unnecessary.

9. Single Quote

Prefer single-quoted strings when you don’t need string interpolation or special symbols such as \t, \n, ', etc – style guide

This is one of the reasons why you should be running Rubocop early and often (and automated) in your projects because repairing this formatting flaw late in the game is a lot of work.

The reason is, I guess, two-fold: 1) you make it clear you're not intending to do string interpolation, and 2) you make it slightly easier for the parser to perform an optimization (it knows it doesn't have the deal with interpolation).

10. String Interpolation

Prefer string interpolation and string formatting instead of string concatenation – style guide

The examples speak for themselves. The main reason why email_with_name = "#{user.name} <#{user.email}>" is better than email_with_name = user.name + ' <' + user.email + '>' is that the latter creates 3 strings and the former just 1.

That's a wrap

In the next installment I will discuss the next 10 mistakes I often make.

Edit: I've posted part 2.

Permalink — Comments or kudos? Find me on Twitter.

Are you a project or product manager? Ship better software with Releasewise!