Skip to content

&:sym calls Symbol#to_proc

Level: 201 · anyone who has written map(&:upcase) and wondered what the & does

One line: &x in an argument list calls x.to_proc and attaches the result as the block; a symbol's to_proc is a lambda that calls the named method on its first argument and hands over the rest, so map(&:upcase) and inject(&:+) are one trick — and a Method, a Hash or any class with to_proc can take the same door.

The & that turns a block into a Proc in a parameter list works the other way in an argument list: &x calls x.to_proc and attaches the result as the call's block. A symbol's to_proc is a lambda that calls the method the symbol names on its first argument, passing any further arguments along — :upcase.to_proc.call("a") is "A" and :+.to_proc.call(1, 2) is 3 (row 2) — which is why map(&:upcase) maps (row 1) and inject(&:+) folds; inject and reduce also accept the bare symbol without & (row 3). The lambda reports lambda? true, an arity of -2 and parameters of one required receiver plus a rest (row 4). Three other kinds of object answer to_proc: a Method, so &method(:double) passes a plain method and &"xaby".method(:index) passes one bound to a receiver (rows 5 and 6); any class you give a to_proc (row 7); and a Hash, whose proc maps keys to values (row 8). An object with no to_proc is a TypeError, and &nil attaches no block at all, so map(&nil) returns an Enumerator (row 9). The symbol's proc needs its receiver (row 10), and it dispatches like public_send: a private method such as puts is refused (row 11).

Verified output of symbol_to_proc_rb.rb — regenerated by tools/run_examples.py, never hand-typed.

 1. map(&:upcase):                               ["A", "B"]
 2. what &:sym builds, called by hand:           :upcase -> "A", :+ with (1, 2) -> 3
 3. so inject works; a bare symbol works too:    inject(&:+) 6, inject(:+) 6, reduce(10, :+) 16
 4. the proc's shape:                            lambda? true, arity -2, parameters [[:req], [:rest]]
 5. &method(:double), a Method's to_proc:        [2, 4], lambda? true
 6. &"xaby".method(:index), bound to a receiver: [1, 2]
 7. & calls to_proc on any object:               map(&Doubler.new) [2, 4]
 8. a Hash has to_proc, keys in, values out:     [:a, :b].map(&{a: 1, b: 2}) [1, 2]
 9. &"upcase" is refused; &nil passes no block:  TypeError: no implicit conversion of String into Proc; map(&nil) -> Enumerator
10. the proc needs a receiver:                   ArgumentError: no receiver given
11. &:sym dispatches like public_send:           NoMethodError: private method 'puts' called for an instance of Integer

Reading the output

Row 1 is the idiom. Row 2 takes the proc apart: called by hand, :upcase.to_proc upcases its argument and :+.to_proc adds its second argument to its first — receiver first, then the method's own arguments. Row 3 shows the consequence, inject(&:+) giving 6, and that inject(:+) and reduce(10, :+) accept the symbol directly, without building a proc at all. Row 4 prints lambda? true, arity -2, parameters [[:req], [:rest]]: one required argument, the receiver, then anything. Rows 5 and 6 pass a Method — [2, 4] from double, [1, 2] from "xaby".index — and row 5 confirms that a Method's proc is a lambda too. Row 7 maps through a Doubler instance whose to_proc returns a lambda; row 8 maps [:a, :b] through a hash and gets [1, 2]. Row 9 prints the TypeError for a String after &, no implicit conversion of String into Proc, and then Enumerator for map(&nil), the same result as map with no block. Row 10 is the ArgumentError: no receiver given from calling the symbol's proc with nothing; row 11 is the NoMethodError for &:puts, whose message names the private method and the receiver's class.

Compared with Python

Python needs no to_proc because everything passed to map or reduce is already a callable, and an unbound method already has the shape Symbol#to_proc manufactures: str.upper takes its receiver as the first argument, so map(str.upper, xs) is map(&:upcase) (rows 1 and 2), and functools.reduce(operator.add, xs) is inject(&:+) — with a start value as the third argument and no symbol shortcut, since a symbol is not a Python thing (row 3). operator.methodcaller("upper") is the closest object to a symbol's proc, calling the named method on whatever it is given, and inspect.signature(str.upper) shows the receiver-first shape as (self, /) (row 4). A plain function, a bound method, an object with __call__ and a dict's .get fill the same slot (rows 5 to 8), matching a Method, a bound Method, a class with to_proc and a Hash. A string or None in the callable's place is a TypeError when the map is consumed (row 9), an unbound method called with no receiver is a TypeError (row 10), and row 11 has no refusal to print: Python has no private methods, a leading underscore is a convention, and Quiet._secret is as callable as anything else. The pieces are documented under map ↗, functools.reduce ↗ and operator.methodcaller ↗.

Verified output of symbol_to_proc_py.py — regenerated by tools/run_examples.py, never hand-typed.

 1. map(str.upper, xs):                          ['A', 'B']
 2. an unbound method takes the receiver first:  str.upper('a') -> 'A', operator.add(1, 2) -> 3
 3. so reduce works; no symbol shortcut:         reduce(add, xs) 6, sum 6, reduce(add, xs, 10) 16
 4. methodcaller('upper') and str.upper's shape: methodcaller -> 'A', signature(str.upper) (self, /)
 5. map(double, xs), a plain function:           [2, 4], type function
 6. map('xaby'.index, xs), a bound method:       [1, 2]
 7. any object with __call__:                    map(Doubler(), xs) [2, 4]
 8. a dict's .get, keys in, values out:          map(d.get, ['a', 'b']) [1, 2]
 9. map('upper', xs) is refused; so is map(None, xs): TypeError; TypeError
10. an unbound method needs a receiver:          TypeError
11. no private methods: _secret is callable:     map(Quiet._secret, [Quiet()]) [42]

Try it

  1. Give a Multiplier class a to_proc so that [1, 2].map(&Multiplier.new(3)) is [3, 6], then write your own Symbol#to_proc as a method sym_proc(name) returning a lambda that calls public_send(name, *args) on its first argument — and check that map, inject and max_by accept it, and that a private method is refused the way row 11 shows.
  2. Run %w[a b].each_with_index.map(&:join) and explain the NoMethodError it raises: each_with_index yields two values, so the symbol's proc calls "a".join(0). Rewrite it with a two-parameter block, then with with_index and _1/_2 from it and _1 name the block's parameter.
Solution to 1

symbol_to_proc_kata_rb.rb in full — pasted here by tools/run_examples.py from the file CI runs.

# Exercise 1: a class whose to_proc lets & accept it, and a hand-written
# Symbol#to_proc -- a lambda that calls the named method on its first
# argument and passes the rest along, which is all the real one does.

class Multiplier
  def initialize(factor) = @factor = factor
  def to_proc = ->(x) { x * @factor }
end

def sym_proc(name)
  ->(receiver, *args) { receiver.public_send(name, *args) }
end

p [1, 2].map(&Multiplier.new(3))
p %w[a b].map(&sym_proc(:upcase))
p [1, 2, 3].inject(&sym_proc(:+))
p %w[bb a ccc].max_by(&sym_proc(:size))

# public_send is what makes it behave like &:sym on a private method:
begin
  [1].map(&sym_proc(:puts))
rescue NoMethodError => e
  puts "#{e.class}: #{e.message}"
end

Verified output of symbol_to_proc_kata_rb.rb — regenerated by tools/run_examples.py, never hand-typed.

[3, 6]
["A", "B"]
6
"ccc"
NoMethodError: private method 'puts' called for an instance of Integer

See also