&: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¶
- Give a
Multiplierclass ato_procso that[1, 2].map(&Multiplier.new(3))is[3, 6], then write your ownSymbol#to_procas a methodsym_proc(name)returning a lambda that callspublic_send(name, *args)on its first argument — and check thatmap,injectandmax_byaccept it, and that a private method is refused the way row 11 shows. - Run
%w[a b].each_with_index.map(&:join)and explain theNoMethodErrorit raises:each_with_indexyields two values, so the symbol's proc calls"a".join(0). Rewrite it with a two-parameter block, then withwith_indexand_1/_2fromitand_1name 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.
See also¶
- Blocks are not objects — the same
&, in a parameter list, going the other way - Procs and lambdas differ in arity and
return— why row 4'slambda? truematters - Procs curry and compose —
:to_s.to_proc >> :size.to_proc, chaining the procs this page builds itand_1name the block's parameter — the other short spelling of{ |x| x.upcase }- Symbols are names — what
:upcaseis before it becomes a proc - The Enumerable toolbox — where
map(&:sym)does most of its work injectandeach_with_object—inject(:+)in fullsendandpublic_send— the dispatch rule behind row 11- Duck typing and
respond_to?—to_procbesideto_strandto_aryas an implicit-conversion protocol tap,thenand chaining —"5".then(&:to_i)- Rust: function pointers ↗ —
map(str::to_uppercase), a method path where Ruby writes&:upcase - Rust:
foldandreduce↗ —injectwith and without a start value, in Rust's terms