When Alternatives are Unhelpful: A Ruby Case Study

Monday, March 01, 2021

In design (of software and non-software products), is it good to provide several alternatives? Let's see this scenario:

You're learning Ruby. You learn that to write an array, you use ["a", "b", "c"], like most other languages. Cool. One day, while reading some library code, you come across some weird code like this: %w(a b c). Confused, you google it, only to find out it's still an array. Same thing as ["a", "b", "c"]. A bit puzzling why it exists, but okay.

A few days later, you come across the %w construct again, but this time with a string: %w"a b c". Time to play a guessing game. Is this meant to be ["a b c"], ["a", "b", "c"], or something else? Turns out, it does the same thing as before. In this case, %w will split the string for you.

Fast forward a few days, and you encounter %w once more, but now it's with {} rather than (): %w{a b c}. You guess that it's probably the same thing, double-check, and yes it is.

Do you see a problem here? Having too many different ways to do one thing can lead to:

  1. Unnecessary time spent looking up and learning irrelevant shit. I already know how to write an array. Rather than focusing on learning the logic of the code I'm reading, I have to interrupt myself multiple times to look up some strange syntax which often turns out to be a different way of doing something I already know(!) This isn't fictional, by the way. It's happened to me multiple times, and it's one of the things that frustrates me about Ruby.

  2. More bikeshedding. More options means more variation, so people will look for some consistency. People will spend time asking "which should I use?" and others will spend time arguing about the answer. For example: Ruby gives you two ways to throw an exception (raise vs fail). This Stack Overflow thread is basically people debating which to use, even though both constructs mean exactly the same thing. (You can debate that the semantics are different, but that's made-up.)

Fun fact: while writing this article, I found out about some more ways to write arrays:

%W[a b c] #=> ["a", "b", "c"]
%i[a b c] #=> [:a, :b, :c]
%I[a b c] #=> [:a, :b, :c]

Oh, and how about strings? You thought that was only single/double quotes and <<<HEREDOC syntax?

%q[This is a string, not an array]
%Q{This too}
%(Yep, and this!)
?a # => a string with a single-character

Even better, the delimiter doesn't have to be [], {} or (). You can use any symbol. So:

%w"This is an array" # => ["This", "is", "an", "array"]
%[email protected] is a [email protected] #=> "This is a string"

It doesn't help that they're all cryptic, and there's no way to tell what the operators do unless you consult the docs.

But they have different uses!

Now, you might argue that each of these array methods has slightly different behaviours and that justifies them. Hmmm. Let's take our examples.

  • %w{a b c} is the same as ["a", "b", "c"], just without quotes.
  • %W supports interpolation, so you can do %W{John #{name}} and get ["John", "Peter"]. I don't see how this is needed, when you can just as easily write ["John", name], or "John #{name}".split.
  • %i and %I will give you arrays of symbols (%I supports interpolation, like above), but again, you can just write [:a, :b, :c] yourself. It isn't that hard or noisy.

The string approaches are a little more useful, with the major benefit being that you won't have to escape quotes inside the string: %q{He said, "Oh shit."} versus "He said, \"Oh shit.\".

I'll agree that some of these are occasional nice-to-haves. Some of them are even pretty cool. However, I don't think they're necessarily worth it. Ruby's philosophy, one I wholly endorse, is "developer productivity". However, providing too many options can run counter to that.

In fact, the differences can add to the confusion. For example, "hello #{name}".split and %w"hello #{name}" will not give you the same results, even though they both appear to use quoted strings:

name = "Peter"
"hello #{name}".split #=> ["hello", "Peter"]
%w"hello #{name}" #=> ["hello", "\#{name}"]

These are just a few scenarios, but there are many more examples of this in Ruby. Of course, Ruby isn't the only language with unnecessary alternatives, but it's one where this is often celebrated as a feature β€” something I, quite frankly, do not get. I've also seen this happen with libraries that provide too many aliases for a method, leading to a bit of confusion.

Sometimes, less is more. Sometimes you can create a better overall experience for your users by limiting the available options and enforcing some consistency (something Apple has figured outπŸ™„). If you're going to provide alternatives, keep them to a reasonable limit and make their use cases and benefits obvious.


HeyπŸ‘‹. I write about interesting software engineering challenges. Want to get updated when I publish new posts? Just visit tntcl.app/blog.shalvah.me.

(Confession: I built Tentacle.βœ‹ It helps you keep a clean inbox by combining your favourite blogs into one weekly newsletter.)

Powered By Swish