What is the pre-Ruby2.3 equivalent to the safe navigation operator (&. or "ampersand-dot")?
Asked Answered
S

2

3

The answers to every question I can find (Q1, Q2) regarding Ruby's new safe navigation operator (&.) wrongly declare that obj&.foo is equivalent to obj && obj.foo.

It's easy to demonstrate that this equivalence is incorrect:

obj = false
obj && obj.foo  # => false
obj&.foo        # => NoMethodError: undefined method `foo' for false:FalseClass

Further, there is the problem of multiple evaluation. Replacing obj with an expression having side effects shows that the side effects are doubled only in the && expression:

def inc() @x += 1 end

@x = 0
inc && inc.itself  # => 2

@x = 0
inc&.itself        # => 1

What is the most concise pre-2.3 equivalent to obj&.foo that avoids these issues?

Showboat answered 4/1, 2016 at 23:59 Comment(4)
I don't think there is a pre-2.3 equivalent. That's why they added it.Misapprehend
But considering that Rails try method seems to function like the & I would guess they implemented very similarly: inc.try(:itself) #=> 1. You can view the try source here github.com/rails/rails/blob/…Misapprehend
@Mike I was able to come up with (x = inc; x.itself unless x.nil?) as a pre-2.3 equivalent. I'm hoping there's something less verbose. ActiveSupport's try may end up replicating a lot of the &. functionality, but it's not a direct equivalent.Showboat
&. is not equivalent to obj && obj.foo, but in most cases it is. u&.profile reminds us as short form of u && u.profile. says Matz in bugs.ruby-lang.org/issues/11537#note-42.Goble
S
1

The safe navigation operator in Ruby 2.3 works almost exactly the same as the try! method added by ActiveSupport, minus its block handling.

A simplified version of that could look like this:

class Object
  def try(method, *args, &block)
    return nil if self.nil?
    public_send(method, *args, &block)
  end
end

You can use this like

obj.try(:foo).try(:each){|i| puts i}

This try method implements various details of the safe navigation operator, including:

  • It always returns nil if the receiver is nil, regardless of whether nil actually implements the queried method or not.
  • It raises a NoMethodError if the non-nil receiver doesn't support the method.
  • It doesn't swallow any exceptions on method calls.

Due to differences in language semantics, it can not (fully) implement other features of the real safe navigation operator, including:

  • Our try method always evaluates additional arguments, in contrast to the safe navigation operator. Consider this example

    nil&.foo(bar())
    

    Here, bar() is not evaluated. When using our try method as

    nil.try(:foo, bar())
    

    we always call the bar method first, regardless of whether we later call foo with it or not.

  • obj&.attr += 1 is valid syntax in Ruby 2.3.0 which can not be emulated with just a single method call in previous language versions.

Note that when actually implementing this code in production, you should have a look at Refinements instead of patching core classes.

Sherwoodsherwynd answered 5/1, 2016 at 12:27 Comment(8)
What do you mean, minus its block handling?Showboat
Well, you can call ActiveSupport's try! method with a block (and no other arguments) and it will yield to the block unless the receiver is nil. This is not possible with the the safe navigation operator. See the (above linked) source for details.Sherwoodsherwynd
Your code does not handle try(:nil?) on nil as pointed out in Jesse Sielaff's comment above. a = nil; a.try(:nil?) #=> nilAnalemma
Which is exactly what the safe navigation operator does. nil&.nil? returns nil in Ruby 2.3.0.Sherwoodsherwynd
You are correct, however that behaviour is extremely surprising. So in versions lower than ruby 2.3, nil.to_i #=> 0 and with the safe operator nil&.to_i #=> nil. So even methods that nil understands are just thrown away and nil is returned instead of actually calling the method... TILAnalemma
Your answer makes an excellent point about bar never being evaluated in the &. expression versus being evaluated in the regular . expression. I hadn't considered that feature.Showboat
I don't think it's true that the &....+= functionality can't be emulated in pre-2.3 Ruby. The "macro"-like substitution I came up with above, (x = {{receiver expression}}; x.{{method call}} unless x.nil?) is perfectly capable of emulating it.Showboat
I edited the sentence to be more precise. Basically, what you would have to do is to check the receiver and then call attr and attr= only if it is not nil. As these two method calls are independent of each other (i.e. the receiver obj) can't know it is called with a += operator, you can't handle it in a single try call.Sherwoodsherwynd
A
0

I think the most similar method that the safe traversal operators emulate is Rails' try method. However not exactly, we need to handle the case when the object is not nil but also does not respond to the method.

Which will return nil if the method can not evaluate the given method.

We can rewrite try pretty simply by:

class Object
  def try(method)
    if !self.respond_to?(method) && !self.nil?
      raise NoMethodError, "undefined method #{ method } for #{ self.class }"
    else
      begin
        self.public_send(method) 
      rescue NoMethodError
        nil
      end
    end
  end
end

Then it can be used in much the same way:

Ruby 2.2 and lower:

a = nil
a.try(:foo).try(:bar).try(:baz)
# => nil

a = false
a.try(:foo)
# => NoMethodError: undefined method :foo for FalseClass

Equivalent in Ruby 2.3

a = nil
a&.foo&.bar&.baz
# => nil

a = false
a&.foo
# => NoMethodError
Analemma answered 5/1, 2016 at 0:22 Comment(7)
Admittedly, this is the height of nitpicking, but I'm not sure this is an exact equivalent. Given obj = nil, I get obj&.foo&.nil? # => nil and obj.try(:foo).try(:nil?) # => true.Showboat
Interesting observation. I'll admit that I'm stumped. It might be the case that an intermediary "nil but not really nil" value is returned via the safe traversal operator that the above definition of try does not do.Analemma
(1) You swallow exceptions on public_send and (2) you don't return nil on any method calls on nil itself, both of which deviate from the behavior of the safe navigation operator.Sherwoodsherwynd
1) You do not swallow exceptions. The difference between send and public_send is that public_send only calls public methods. If the method does not exist, it still raises a NoMethodError, and if the method does exist and errors, so will the code. 2) The nil point is valid but as discussed above, would require a different return type other than nil to behave EXACTLY like the safe navigation operator.Analemma
@Analemma self.public_send(method) rescue nil swallows any exception which might be raised, including the NoMethodError, regardless of whether you use send or public_send here. Also, you don't need "another" nil. If the receiver is nil, the safe navigation operator always returns nil. The same nil that is used everywhere else in Ruby.Sherwoodsherwynd
A final point which was not covered yet in your code is that objects are not required to signal if they respond to a message in their respond_to?. It is certainly a good idea to do so but not required. If you e.g. simply overwrite method_missing without also specifying respond_to_missing?, you can still handle the method call normally, including by using the safe navigation operator. Your code raises a NoMethodError instead.Sherwoodsherwynd
1. Yes the rescue gets rid of exceptions, not the public_send as your first comment indicated. 2. This code does not handle the case where someone defined method_missing and not respond_to but it also doesn't handle code that changes itself dynamically every single invocation, because who would be crazy enough to do that?Analemma

© 2022 - 2024 — McMap. All rights reserved.