The most interesting addition to Ruby 2.3.0 is the Safe Navigation Operator(&.). A similar
operator has been present in C# and Groovy for a long time with a slightly different syntax - ?..
So what does it do?
TLDR
It is used for avoiding NoMethodError exceptions when calling methods on objects that may be
nil.
account = nil
account.number
# => NoMethodError (undefined method `number' for nil:NilClass)`)
account&.number
# => nil
Example
Imagine you have an account that has an owner and you want to get the owner’s address. If
you want to be safe and not risk a nil error, you would write something like the following:
if account && account.owner && account.owner.address
...
end
This is really verbose and annoying to type. ActiveSupport includes the try method which has a
similar behaviour (but with few key differences that will be discussed later):
if account.try(:owner).try(:address)
...
end
It accomplishes the same thing - it either returns the address or nil if some value along the
chain is nil. The first example may also return false if, for example, the owner is set to
false.
Using the safe navigation operator (&.)
We can rewrite the previous example using the safe navigation operator:
account&.owner&.address
The syntax is a bit awkward but I guess we will have to deal with it because it does make the code more compact.
More examples
Let’s compare all three approaches in more detail.
account = Account.new(owner: nil) # account without an owner
account.owner.address
# => NoMethodError: undefined method `address' for nil:NilClass
account && account.owner && account.owner.address
# => nil
account.try(:owner).try(:address)
# => nil
account&.owner&.address
# => nil
No surprises so far. What if owner is false (unlikely but not impossible in the exciting world
of shitty code)?
account = Account.new(owner: false)
account.owner.address
# => NoMethodError: undefined method `address' for false:FalseClass j
account && account.owner && account.owner.address
# => false
account.try(:owner).try(:address)
# => nil
account&.owner&.address
# => undefined method `address' for false:FalseClass`
Here comes the first surprise - the &. syntax only skips nil but recognizes false! It is not
exactly equivalent to the s1 && s1.s2 && s1.s2.s3 syntax.
What if the owner is present but doesn’t respond to address?
account = Account.new(owner: Object.new)
account.owner.address
# => NoMethodError: undefined method `address' for #<Object:0x00559996b5bde8>
account && account.owner && account.owner.address
# => NoMethodError: undefined method `address' for #<Object:0x00559996b5bde8>`
account.try(:owner).try(:address)
# => nil
account&.owner&.address
# => NoMethodError: undefined method `address' for #<Object:0x00559996b5bde8>`
Oops, the try method doesn’t check if the receiver responds to the given method. This is why
it’s always better to use the stricter version of try - try!:
account.try!(:owner).try!(:address)
# => NoMethodError: undefined method `address' for #<Object:0x00559996b5bde8>`
Bonus 2.3 features - Array#dig and Hash#dig
The #dig method is probably the most useful feature in this version. No longer do we have to
write abominations like the following:
address = params[:account].try(:[], :owner).try(:[], :address)
# or
address = params[:account].fetch(:owner) { {} }.fetch(:address)
We can now simply use Hash#dig and accomplish the same thing:
address = params.dig(:account, :owner, :address)