ruby & shortcut for to_proc seems to act irregularly

Viewed 59

so i'm trying to work out how the sym.to_proc method works, supposedly any object that implements to_proc has a shorthand which is simply an & before the object, but this shorthand's behavior seems erratic:

this works

irb(main):022:0> p = :to_s.to_proc
=> #<Proc:0x00000000056c33b0(&:to_s)>

but this doesn't

irb(main):023:0> p = &:to_s
Traceback (most recent call last):
    3: from X:/Ruby26-x64/bin/irb.cmd:31:in `<main>'
    2: from X:/Ruby26-x64/bin/irb.cmd:31:in `load'
    1: from X:/Ruby26-x64/lib/ruby/gems/2.6.0/gems/irb-1.0.0/exe/irb:11:in `<top (required)>'
SyntaxError ((irb):23: syntax error, unexpected &)
p = &:to_s
    ^

and now the other way around, this works

irb(main):024:0> (0..10).map &:to_s
=> ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10"]

but this doesn't!

irb(main):025:0> (0..10).map(:to_s.to_proc)
Traceback (most recent call last):
    5: from X:/Ruby26-x64/bin/irb.cmd:31:in `<main>'
    4: from X:/Ruby26-x64/bin/irb.cmd:31:in `load'
    3: from X:/Ruby26-x64/lib/ruby/gems/2.6.0/gems/irb-1.0.0/exe/irb:11:in `<top (required)>'
    2: from (irb):25
    1: from (irb):25:in `map'
ArgumentError (wrong number of arguments (given 1, expected 0))

these errors don't seem to make sense, i'm really at a loss here.

2 Answers

The & unary prefix ampersand operator / sigil can be used in exactly two places:

  • In a parameter list.
  • In an argument list.

In a parameter list, the & unary prefix ampersand sigil means "Take the block that was passed as an argument, roll it up into a Proc object, and bind it to this parameter."

In an argument list, the & unary prefix ampersand operator means "Take the Proc object that was passed as an argument, unroll it into a block, and pass it as a block argument." More precisely, and that is what enables "tricks" like Symbol#to_proc, it means "Take the Proc object that was passed as an argument or convert the object using to_proc if it is not already a Proc, unroll it into a block, and pass it as a block argument."

In some sense, the two are exactly the inverse of each other.

In your first example's second case, you have neither a parameter list nor an argument list, so neither the & unary prefix ampersand sigil nor the & unary prefix ampersand operator are syntactically legal.

In your second example's second case, you are passing the result of :to_s.to_proc, which is a Proc object, as an argument to Range#map. However, Range#map does not have any parameters (other than the implicit block parameter that every method has in Ruby), so you are passing one argument where none are expected. You have to unroll your Proc argument into a block like this:

(0..10).map(&(:to_s.to_proc))

which can be shortened to

(0..10).map(&:to_s)

because the & unary prefix ampersand operator will perform the conversion on its own if the operand is not already a Proc object.

& is an internal operator that explicitly calls #to_proc on the argument and converts the result to a block.

While a block is somewhat similar to proc, it is not exactly the same. It cannot exist alone, which means one cannot create block instances in a wild. The other way round, one cannot pass proc instances to methods expecting block. They have to be converted to blocks upfront (with &.)

p = :to_s.to_proc     # fine, proc returned
p = &:to_s            # error, attempt to create block instance
(0..10).map &:to_s    # fine, the same as map { |e| e.to_s }
map(:to_s.to_proc)    # error, block is required, proc passed

# bonus
map(&:to_s.to_proc)   # fine, Proc.to_proc == self
map(&->(e){ e.to_s }) # fine, proc was converted to block
Related