rubyproc

How is it that Ruby methods requiring blocks can just use Procs instead?


I am learning Ruby and was looking at the documentation of Array#map here, and it says the the syntax is as follows

map {|element| ... } → new_array
map → new_enumerator

But we are able to do arr.map(&:to_s) to stringify each element of the array. As far as I understand, &:to_s is just syntactic sugar for :to_s.to_proc so (as given here), it means that map accepts a Proc object as argument. But its method signature says otherwise.

I have a few questions regarding this.

  1. Can someone please explain this behavior and point to the relevant documentation for the same?

  2. What exactly does Proc mean in this context? Isn't to_s a method of the underlying class? What does it mean for me to a pass a Proc object of to_s which has no information about the underlying class on which it is going to be called.

Any help would be great!


Solution

  • It's not exactly accurate to say "&:to_s is just syntactic sugar for :to_s.to_proc", because that glosses over the rest of what the & character is doing.

    You are likely aware that there is a "literal block" syntax in Ruby with two variations (docs):

    foo { 1 + 1 }
    
    foo do
      1 + 1
    end
    

    A Proc object is an object with the special property that it is allowed to be used in place of the literal block syntax.

    So, for any method that accepts a literal block, you can instead pass it a Proc object—but only by using the & syntax (docs):

    my_proc = Proc.new { 1 + 1 }
    
    foo(&my_proc)
    

    The & syntax means "use this Proc object in place of this method's block argument, instead of as a regular positional argument."

    This is where the "sugar" comes in. If you use the & syntax to pass a non-Proc object, Ruby does you the favor of attempting to call to_proc on that object, to turn it into a Proc. You could do the same thing yourself, but you don't have to:

    # Equivalent:
    foo(&not_a_proc.to_proc)
    foo(&not_a_proc)
    

    In your example, the object you passed using the & syntax, :to_s, is a Symbol object. Because it is not a Proc object, Ruby calls to_proc on it. There is a method Symbol#to_proc (docs) that turns the symbol :to_s into a Proc that is approximately1 equivalent to
    { |obj| obj.to_s }.

    The end result:

    arr = [1,2,3]
    my_proc = Proc.new { |obj| obj.to_s }
    
    # Equivalent:
    arr.map { |obj| obj.to_s }
    arr.map(&my_proc)
    arr.map(&:to_s.to_proc)
    arr.map(&:to_s)
    

    1 (For some of the differences that make it only "approximately", see comments below and What's the difference between a proc and a lambda in Ruby? )


    As for

    But its method signature says otherwise.

    the documentation is a little fuzzy on how blocks and Procs are represented. There is this section of the Proc class documentation (docs) that addresses it indirectly:

    Creation

    There are several methods to create a Proc

    • Receiving a block of code into proc argument (note the &):

      def make_proc(&block)
        block
      end
      
      proc3 = make_proc {|x| x**2 }
      

    So, if you were to implement the map method yourself, the argument signature could look like this:

    def map(&block)
    

    But as you can see, in order to show the needed block parameters, Ruby method documentation is instead written as you quoted it:

    map {|element| ... } → new_array
    

    and I don't think there's any place in the documentation that explains that exact relationship any better than what's already linked above.