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 # => "email@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) # => "firstname.lastname@example.org"
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
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
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:
#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!