Why is safe navigation better than using try in Rails?
(6) Speed
Safe navigation is almost 4 times faster than using the try method from activesupport
require 'active_support/all'
require 'benchmark'
foo = nil
puts Benchmark.measure { 10_000_000.times { foo.try(:lala) } }
puts Benchmark.measure { 10_000_000.times { foo&.lala } }
Output
1.310000 0.000000 1.310000 ( 1.311835)
0.360000 0.000000 0.360000 ( 0.353127)
I don't think we should be comparing those two things, because they do something else.
Given this example Person
class
class Person
def name
"John"
end
end
try
try
should be used if you're not sure whether the given method exists on the object or not.
person = Person.new
person.name # => "John"
person.email # => NoMethodError: undefined method `email' for #<Person>
person.try(:email) # => nil
&.
Safe nagivation should be used if you're not sure if the object you're calling your method on is nil or not
person = nil
person.name # => NoMethodError: undefined method `name' for nil:NilClass
person&.name # => nil
You can't use them interchangeably
People often confuse those two methods, because you can use try
instead of &.
person = nil
person.name # => NoMethodError: undefined method `name' for nil:NilClass
person&.name # => nil
person.try(:name) # => nil
But you cannot use &.
instead of try
person = Person.new
person.name # => "John"
person.email # => NoMethodError: undefined method `email' for #<Person>
person.try(:email) # => nil
person&.email # NoMethodError: undefined method `email' for #<Person>
(1) &.
is generally shorter than try(...)
Depending on the scenario, this can make your code more readable.
(2) &.
is standard Ruby, as opposed to try
The method try
is not defined in a Ruby core library but rather in a Rails library. When you are not developing a RoR web app but instead are writing e.g. little helper scripts, this will get relevant pretty fast.
(I prefer Ruby over Bash, for example.)
(3) &.
makes debugging easier
The safe traversal operator will throw an error if a nonexistent method is being invoked.
>> "s"&.glubsch
NoMethodError: undefined method `glubsch' for "s":String
Only on nil
it will behave leniently:
>> nil&.glubsch
=> nil
The try
method will always return nil
.
>> "s".try(:glubsch)
=> nil
Note that this is the case with most recent versions of Ruby and Rails.
Now imagine a scenario where a method named glubsch
exists. Then you decide to rename that method but forget to rename it in one place. (Sadly, that can happen with ruby...) With the safe traversal operator, you will notice the mistake almost immediately (as soon as that line of code is executed for the first time). The try
method however will happily provide you with a nil
and you will get a nil
related error somewhere downstream in program execution. Figuring out where such a nil
came from can be hard at times.
Failing fast and hard with &.
makes debugging easier than blithely returning nil
with try
.
EDIT: There is also the variant try!
(with a bang) that behaves the same as &.
in this regard. Use that if you don't like &.
.
(4) What if I don't care if a method is implemented or not?
That would be strange. Since that would be an unexpected way of programming, please make it explicit. For example by fleshing out the two cases (implemented or not) using respond_to?
and branch off of that.
(5) What about try
's block form?
Instead of a method name, a block can be passed to try
. The block will be executed in the context of the receiver; and within the block there is no leniency applied. So with just a single method call, it will acutally behave the same as &.
.
>> "s".try{ glubsch }
NameError: undefined local variable or method `glubsch' for "s":String
For more complex computations, you might prefer this block form over introducing lots of local variables. E.g. a chain of
foo.try{ glubsch.blam }.try{ bar }.to_s
would allow foo
to be nil
but require foo.glubsch
to return a non-nil
value. Then again, you can do the same with the safe traversal operator in a more concise fashion:
foo&.glubsch.blam&.bar.to_s
Using try
's block form for complex computations IMHO is a code smell, though, because it impedes readability. When you need to implement complex logic, introduce local variables with descriptive names and maybe use an if
to branch off a nil
case. Your code will be more maintainable.
HTH!