danielewski.dev

Behind the Scenes of obj.foo(&:bar)

As a Ruby developer, it’s common to use the short hand obj.foo(&:bar) instead of obj.foo { |el| el.bar }. How does it work behind the scene?

Example:

["a", "b", "c"].map { |el| el.upcase } # => ["A", "B", "C"]
["a", "b", "c"].map(&:upcase) # => ["A", "B", "C"]

Multiple steps are involved in this “magic” trick. Let’s review them step by step.

It begins with the & operator. In the argument list, putting a & in front of a proc instance will tell Ruby to use it as a block.

proc = proc { |el| el.upcase }
["a", "b", "c"].map(&proc) # => ["A", "B", "C"]

What if we use something other than a proc?

str = "upcase"
["a", "b", "c"].map(&str) # => TypeError: wrong argument type String (expected Proc)

Woops. We get this error because Ruby is trying to cast str to a proc by calling to_proc on it in order to use it as a block. However to_proc is not defined for String. Let’s define it by monkey patching String. Don’t try this at home.

class String
  def to_proc
    proc { |obj, args| obj.send(self, *args) }
  end
end

str = "upcase"
["a", "b", "c"].map(&str) # => ["A", "B", "C"]

In ["a", "b", "c"].map(&:upcase) we can easily see that & is followed by the symbol :upcase.

Now, we are aware that to_proc will be called on :upcase. The big difference is that to_proc is defined for Symbol and it basically returns proc { |el| el.upcase }.

  • Symbol#to_proc is called on :upcase, which gives something like proc { |el| el.upcase }
  • & interprets this proc as a block
  • Array#map invokes this block for each of its elements

That’s why you can simply write:

["a", "b", "c"].map(&:upcase)