ruby

How does the &.method syntax (safe navigation operator) works in Ruby?


I noticed if you type: object &, you get the object back. For example:

1.class   # => Integer
1 &.class # => Integer
'hello'.then { |x| x.equal?(x &.itself) }    # => true
[1, 2, 3] &.map(&:next)    # => [2, 3, 4]

I am unable to find a documentation for the syntax for object &.method How does this syntax work?


Solution

  • There are 2 seperate operators here:

    1. Safe navigation operator &. - It is safe navigation operator which was introduced in Ruby 2.3.0. It basically returns nil if the callee is nil instead of raising excecption undefined method called for Nil class. eg:

      a = 1
      a.next
      # => 2
      a&.next
      # => 2
      a = nil
      a.next
      # => NoMethodError (undefined method `next' for nil:NilClass)
      a&.next
      # => nil ## No exception, returns nil
      

      You can read about it more here and documentation

    2. Unary & : This operator is a little more complex. It is almost equivalent to calling #to_proc but not quite that. But for this discussion let us think like that. So, if you have a Proc, calling with & in front of it will call #to_proc on the Proc and convert it into a block

      multiply_by_2 = Proc.new { |x| x * 2 }
      # => #<Proc:0x00007fb4771cf560>
      # &multiply_by_2 almost equivalent to { |x| x * 2 } but is not correct syntax
      [1, 2].map(&multiply_by_2)
      # => [2, 4]
      # equivalent to  [1, 2].map { |x| x * 2 }
      

      But what happens if we give a symbol like :abc to & operator instead of a proc. It will try to call #to_proc on the symbol and ruby has defined Symbol#to_proc which roughly translates to something like this:

      def to_proc
        # this will return some block like { |x| x.send(:abc) }
        lambda { |x| x.send(self) }
      end
      

      So &:abc roughly translates to this block { |x| x.abc } using the below transformation

      &:abc =====> :abc.to_proc =====> { |x| x.send(:abc) } ====> { |x| x.abc }
      

      So, instead of doing [1, 2, 3].map { |x| x.next }, you could do [1, 2, 3].map(&:next) as &:next is roughly equivalent to the block { |x| x.next }.

      See unary & (which is the main source of what I have written here) for more reading.