DEV Community

loading...

Trailing conditionals considered harmful unless used sparingly

jerodsanto profile image Jerod Santo Originally published at jerodsanto.net ・4 min read

Note: I wrote this for my blog awhile back, but I don't write there much any more because most of my time is spent Changelogging.

One Ruby feature that I fell in love with back in the day is the ability to tack conditionals on at the end of a line. For example, this bit of code:

if some_condition?
  object.perform_some_action
end

Can be expressed as a one-liner, like this:

object.perform_some_action if some_condition?

It's a small difference, but the latter form often maps more directly to how the author thinks about the problem. It feels even better when teamed with the unless keyword:

object.perform_some_action unless some_condition?

After years of writing and reading code like this, I've slowly grown cold on the style.

Why

The reason why I'm bearish on trailing conditionals may best be expressed by a road sign I saw on a recent road trip down Interstate 80:

Not the actual sign I saw, but close enough

Notice the trailing conditional? A lot can go wrong with signs like these if the driver doesn't make it to the final line of the text. Why might that happen?

  • The sign could be blocked by some obstruction until the last second
  • The driver could be distracted by kids, the radio, their phone, etc. until it's too late
  • A sign-related exit could be imminent with the driver in the wrong lane1

In this case, WHEN FLASHING is the key indicator on the sign. Why is it the last thing mentioned? In the world of journalism they call this burying the lead.

If the sign designer place WHEN FLASHING first, the driver could often skip the rest of the text altogether2. This saves cognitive overhead that the driver can use elsewhere and avoids potential disasters that might occur if the conditional isn't understood in time.

In a slightly-tangential way, trailing conditionals violate the Principle of Least Surprise. This principle — as most important things in life — made its way in to a Mitch Hedberg joke, in which he picks a fight with the phrase "Do Not Disturb".

The problem is exacerbated when driving 80 MPH on the interstate, but it exists in our code as well. The trailing conditional feels great when you're writing the code, but it often makes it harder to read. This is most obvious when the operation that precedes the conditional is verbose. Take this fake code, for instance:

call_this_really_long_method_that_is_probably_too_long_but_that_will_not_stop_us unless some_condition?

What if you didn't scroll over to see the unless at the end? You wouldn't know what's going on at all. Admittedly, method names of this length are rare, but it is common to have trailing conditionals nested inside other control structures that have the same effect3.

Code is read much more often than it is written, so we need to optimize for readability over writeability4. Trailing conditionals tend to do the opposite.

But

As with most things in software (and writing), there are exceptions. Some uses of trailing conditionals improve readability. The best case for them in my experience is with guard clauses. Guard clauses have a few characteristics that make them quite readable with trailing conditionals:

  • They occur at the top of a method, so they are rarely nested themselves
  • They often return or raise an error, which are brief statements
  • There are often a few guard clauses together, so vertical brevity aides reading

Take a look at this method which returns a price_range string for a given object that responds to price_minimum and price_maximum:

def price_range
  return @price_range if defined? @price_range
  return "" unless price_minimum
  return "" unless price_maximum

  # ... code to determine `minimum` and `maximum` ...

  @price_range = "#{minimum}-#{maximum}"
end

The first line memoizes the price_range, since this is apparently an expensive computation. Lines 2 and 3 are guard clauses. What would this look like with traditional conditionals?

def price_range
  if defined? @price_range
    return @price_range
  end

  unless price_minimum
    return ""
  end

  unless price_maximum
    return ""
  end

  # ... code to determine `minimum` and `maximum` ...

  @price_range = "#{minimum}-#{maximum}"
end

This code requires more vertical work to parse. There's a 3rd form it could take, which is to put the conditionals first and still keep each one a one-liner:

def price_range
  if defined? @price_range return @price_range
  if !price_minimum return ""
  if !price_maximum return ""

  # ... code to determine `minimum` and `maximum` ...

  @price_range = "#{minimum}-#{maximum}"
end

This works for me, but I prefer the return-first form because any time a method returns early we want to know about that ASAP.

So

Think twice before slinging around trailing conditionals. They put the cart before the horse and in extreme cases they cause the reader to miss the horse altogether. This makes them often less readable than the traditional form.

Or maybe Mitch was right and we all just need to read faster!


  1. This is actually what happened to me. I barely deciphered the correct meaning in time. 

  2. The road being closed is the exception, not the common case. This means the lights will rarely flash and the sign is most often irrelevant. 

  3. This is yet another reason that I advocate for 80-characters or less per line. 

  4. The two are often coupled, but are sometimes at odds. 

Discussion

pic
Editor guide
Collapse
ben profile image
Ben Halpern

I've always felt the same way. I think I've been influenced by Destroy All Software Ruby vids, where Gary Bernhardt expresses this, but I may be misremembering.

But I also have admitted that most Rubyists like this stuff and it's not usually a hill I care to die on.

Collapse
jerodsanto profile image
Jerod Santo Author

Side note: My original post had Mitch Hedberg's joke embedded via a SoundCloud widget. According to the editor guide, it looks like this is one of very few platforms for which there is no embed support. Bummer!

Perhaps a good place for my first open source contrib? 🤔

Collapse
ben profile image
Ben Halpern

This is actually a perfect first issue in theory, the embeds lend themselves incredibly well to community building. But we should have better docs on how one would do that.

Collapse
jerodsanto profile image
Jerod Santo Author

I'm usually pretty good at poking and prodding other people's codebases to add features (I'm also well-versed in Ruby and Rails). Maybe point me in the general direction to get things started?

Collapse
andy profile image
Andy Zhao (he/him)

TIL that you can write if statements with the if in the beginning and without an end 🤯

Great post. Pretty funny that we can see physical examples of this happening, too...

Collapse
maestromac profile image
Mac Siri

Nice post! One-liners are one of my favorite thing about ruby but sometime I trip myself over with it. Perhaps there's linter config that can help us all avoid this?