Search code examples
rubysymbolsproc

Why does Ruby only allow to refer to methods by symbols when calling to_proc on them?


This works:

strings = ["1", "2", "3"]
nums = strings.map(&:to_i)

We see that & (to_proc method) applied to a Symbol object is turned into a block. This, however, doesn't work:

strings = ["1", "2", "3"]
nums = strings.map(&to_i)
nums = strings.map("to_i".to_sym.to_proc) #...neither does this

Why doesn't it work? Isn't there any alternative way to write the code above? I'm confused because there are two ways to access a method of a class:

"1".method(:to_i).call #works as well as
"1".method("to_i").call 

So the method's name can be accessed either by symbol or a string.


Solution

  • Given the following valid Ruby example:

    "to_i".to_sym.to_proc.call("1")
    

    To call #to_proc on a Symbol creates a proc that receive one parameter: the object that should receive an invocation of the method named by the symbol. Something like this:

    ->(object) {
      object.send(:to_i)
    }
    

    What the & do is to pass the block returned by #to_proc as a proper invocation block to the method being called. When used with Enumerable#map, the final result is that the passed block will be invoked for each element on the collection and receive the current iteration element being passed as parameter to the block.

    So the ["1", "2"].map(&:to_i) syntax is a shortcut to something like this:

    block = ->(el) {
      el.send(:to_i)
    }
    
    ["1", "2"].map &block
    

    About your examples:

    When you try to call &to_i in your example, Ruby will try to invoke a variable or method named to_i. Since it doesn't exists in the scope (it is a method of each String in the Array, not in the "global" scope), it'll generate an error.

    About the other example: "to_i".to_sym.to_proc will transform the :to_i into a proc that when invoked with one parameter, will try to call the method named after the symbol (:to_i) against the target parameter. This means that you could use the & to transform the proc into a "invocation block":

    ["1", "2"].map(&"to_i".to_sym.to_proc)