In Progress
Unit 1, Lesson 1
In Progress

Safe Navigation Part 2: Try, Try Again

Video transcript & code

Welcome back to this mini-series on "safe navigation". We've been talking about making it possible to jump from one object to the next without worrying about nil values causing the code to crash. In the previous episode we looked at this code, and explored a way to make the object associations reliable so we wouldn't have to check for nil at every step.

require "./models"

@product.department &&
  @product.department.curator &&
  @product.department.curator.email_address
# => "dan@example.com"

In that episode, we made the assumption that we had control over the classes of all of the objects involved, so we could modify them at will. Unfortunately, that's not always the situation. The sad truth is that many real-world codebases are infested with potential nil values, and we don't always have the ability—or the time—to address the underlying problem. Various strategies have emerged in order to deal with this situation.

If you're familiar with Rails and the ActiveSupport library, you may know about the #try method that it adds to objects. With #try, we can convert each "unsafe" message send into a call to #try. If the object responds to the message, it will be sent. Otherwise, if the object is nil, nothing will happen and no error will be raised.

require "./models"
require "active_support"
require "active_support/core_ext"

@product.try(:department).try(:curator).try(:email_address)
# => "dan@example.com"

This is a bit more concise than our && version, although it still drowns our original, concise intentions in a sea of #try messages, and symbols standing in for proper message sends.

#try unfortunately also introduces a more insidious problem. Let's say we have a number, and we want to round it down. We can use the #floor message for this.

num = 23.1
num.floor                       # => 23

Something that sometimes happens in our applications is that we take in some numeric information in string form, and forget to convert it to a number before using it as one. This is a bug that we need to be aware of, and Ruby is more than happy to let us know with an exception.

num = "23.1"
num.floor                       # =>

# ~> NoMethodError
# ~> undefined method `floor' for "23.1":String
# ~>
# ~> xmptmp-in49044q82.rb:2:in `<main>'

Now, consider the situation where the value of num is sometimes nil. We might introduce #try to account for this potentiality.

require "active_support"
require "active_support/core_ext"

num = nil
num.try(:floor)                 # => nil

Then supposing that rather than a number or a nil, we get a String in num.

require "active_support"
require "active_support/core_ext"

num = "23.1"
num.try(:floor)                 # => nil

This is not the situation we planned for when we used #try. This is a bug. But #try silently hides the error and returns nil.

What we can see here is that #try isn't actually checking for nil. It's checking to see if the object responds to the message, and returning nil otherwise. We typically use #try to deal with values that might be nil, but it has much broader behavior which can hide defects in our code.

Note that the semantics of #try has changed repeatedly over the lifetime of Rails, so you may see different behavior.

Note too that since Rails 4, there is a new bang variant of try that has the semantics we probably wanted in the first place.

require "active_support"
require "active_support/core_ext"

num = "23.1"
num.try!(:floor)                 # =>

# ~> NoMethodError
# ~> undefined method `floor' for "23.1":String
# ~>
# ~> C:/RailsInstaller/Ruby2.1.0/lib/ruby/gems/2.1.0/gems/activesupport-4.1.8...
# ~> C:/RailsInstaller/Ruby2.1.0/lib/ruby/gems/2.1.0/gems/activesupport-4.1.8...
# ~> xmptmp-in490443pB.rb:5:in `<main>'

Recently, a new operator was added to the version of Ruby currently under development. It's an operator which essentially mimics the behavior of this bang version of #try. We use it like this:

@product.?department.?curator.?email_address

Like the #try method in ActiveSupport, this new dot-question syntax only sends the message if the object is non-nil. Otherwise, nothing happens. And like the #try! variant,, this operator only checks for nil; it will still raise a NoMethodError if the wrong message is sent to a non-nil object.

The Ruby core developers call this the "safe navigation operator". In some other languages which already have an equivalent operator, it's known as the "elvis operator", for reasons that have never been clear to me. Me, I'm partial to a name Jim Gay came up for it: The Claw of Demeter. Whatever we call it, it seems this feature is coming soon to a Ruby near you.

Today we've seen some current and future methods of safely navigating across networks of objects. Now I'd like to show you a different way of going about it, one which is both strange and familiar all at once. But that's going to need an episode of it's own. So until next time: happy hacking!

Responses