Ruby’s Unary ‘&’ Operator

&block party

My goal in this post is to dig into and clarify some aspects of Ruby’s unary & operator. This article assumes familiarity with blocks and Proc objects.

If you’ve used Ruby for just a little bit, you’ve likely seen code that looks like this:

class TellMeAboutBlock
def accept_block(&block)
end
end

or this:

[1,2].map(&:to_s)
# => ["1", "2"]

These two pieces of code have in common the use of &, but what’s really happening here? In the first example, & converts the block into a Proc object, and in the second example & converts :to_s into a block that map can accept.

Generally, the unary & operator converts blocks and non-Proc objects to Proc objects, and Proc objects into blocks. This Blog Post nicely summarizes the three ways in which &object is evaluated:

  1. If object is a block, & converts object into a Proc.
  2. If object is a Proc, & 'unpacks’ the Proc object and treats it as a literal block.
  3. If object is neither a block nor a Proc, & first calls object.to_proc in an attempt to convert object into a Proc, and then converts the resulting Proc into a block.

&block -> Proc

Let’s look at the first case, in which object is a block, and & converts the object into a Proc.

class TellMeAboutBlock
def tell_me_class(&block)
p block.class
end
end
n = TellMeAboutBlock.new
n.tell_me_class {p "Random Block"}
# => Proc

In this code, the method #tell_me_class(&block) returns the class of the &block argument. When n.tell_me_class {"Random Block"} is run, "Proc" is returned.

What’s interesting about this result is that block expressions like {|n| n+1} or {"Random Block"} are not Ruby objects; they do not have a class. For example, this will result in a SyntaxError:

{|n| n+1}.class
# => 
SyntaxError: (irb):4: syntax error, unexpected '|', expecting '}'

This means that the method #tell_me_class does something to the &block parameter it accepts to convert it into a Proc object. This is where the & operator comes into the picture. When a method is defined, it can anticipate converting any block passed to it into a Proc object by prefixing the last parameter with the & operator. The &block parameter must be the last parameter in the method definition.

When the method is invoked and is given a block, the block will be converted into a Proc object. So, with our #tell_me_class method:

class TellMeAboutBlock
def tell_me_class(&block)
puts block.class
end
end

when we call tell_me_class and pass it a block:

n = TellMeAboutBlock.new
n.tell_me_class {p "Random Block"}
# => Proc

the block passed to the method is converted into a Proc. It’s interesting to note that yield still works, even if the block is converted into a Proc object. For example:

class TellMeAboutBlock
def tell_me_class(&block)
puts block.class
yield
end
end
n = TellMeAboutBlock.new
n.tell_me_class {p "Random Block"}
# =>
Proc
"Random Block"

Now what happens if we pass a Proc object directly into the #tell_me_class method? Like this:

class TellMeAboutBlock
def tell_me_class(&block)
puts block.class
end
end
n = TellMeAboutBlock.new
my_proc = Proc.new {p "Random Block"}
n.tell_me_class(my_proc)
# => wrong number of arguments (1 for 0) (ArgumentError)

This results in an ArgumentError because #tell_me_class expects a block, not a ‘normal’ argument.

But remember the second way &object is evaluated:

If object is a Proc, & unpacks object and passes it as a block.

&Proc -> block

Let’s give it a shot. If we use & to unpack my_proc and allow Ruby to pass it as a literal block:

n.tell_me_class(&my_proc)
# => Proc

it evaluates correctly again. Also of note is that &block preserves the lambda status of a Proc:

my_lam = ->(n) { n.to_s}
n.tell_me_class(&my_lam)
# => #<Proc:0x007fe07502e3f8@proc_post.rb:15 (lambda)>

One question that arises is: why do we get an ArgumentError — as opposed to a TypeError — when we pass a non-Proc object into #tell_me_class?Doesn’t this method take one argument, a block?

It turns out that even though the method takes a &block parameter, the arity (the number of arguments a method takes) does not include the &block parameter. The arity of the tell_me_class(&block) method is 0, shown here:

class TellMeAboutBlock
def tell_me_class(&block)
puts block.class
end
end
n = TellMeAboutBlock.new
n.method(:tell_me_class).arity
# => 0

Similarly, a method with two parameters — one of which is &block — will have an arity of 1:

class TellMeAboutBlock
def two_params_one_is_block(param, &block)
end
end
n = TellMeAboutBlock.new
n.method(:two_params_one_is_block).arity
# => 1

Passing a Ruby object (e.g. Proc objects, FixNum objects, String objects, etc.) as an argument into a method that takes only a &block parameter will result in an ArgumentError because the method does not expect any non-block arguments.

For example:

class TellMeAboutBlock
def tell_me_class(&block)
puts block.class
end
end
n = TellMeAboutBlock.new
my_proc = Proc.new {p "Random Block"}
n.tell_me_class(my_proc)
# => wrong number of arguments (1 for 0) (ArgumentError)
n.tell_me_class(1)
# => wrong number of arguments (1 for 0) (ArgumentError)
n.tell_me_class("Hello")
# => wrong number of arguments (1 for 0) (ArgumentError)

you can, however, pass nothing into this method and not receive an error:

n.tell_me_class()
# => NilClass

From the perspective of the method, no block has been provided, so &block is nil.

However, passing nil explicitly results in an ArgumentError.

n.tell_me_class(nil)
# => wrong number of arguments (1 for 0) (ArgumentError)

This is because nil in Ruby is not ‘nothing’ — it is an object of the NilClass, so the method tell_me_class perceives it is receiving one argument, which violates its arity (0).

Small Note On Performance

Converting a block into a Proc is not cheap, and it is advisable when possible to use yield instead of block.call inside the method.

This is preferable:

class TellMeAboutBlock
def tell_me_class(&block)
yield
end
end

To this:

class TellMeAboutBlock
def tell_me_class(&block)
block.call
end
end

Feel free to read more about the performance of block.call vs yield in this blog post, which benchmarks the two strategies.


&non-Proc -> block

Now we come to the third way in which &object is evaluated:

If object is neither a block nor a Proc, & first calls object.to_proc in an attempt to convert object into a Proc, and then converts the resulting Proc into a block.

A common example of this is:

[1,2].map(&:to_s)
# => ["1", "2"]

What’s happening here? First, we know that map can take a block. From the Ruby Docs:

map { |obj| block } → array
Returns a new array with the results of running block once for every element in enum.

map can only take a block or nothing at all; it cannot take a Proc object. If we try to pass a Proc into map, we see the familiar ArgumentError:

my_proc = Proc.new {|n| n.to_s}
[1,2].map(my_proc)
# => wrong number of arguments (1 for 0) (ArgumentError)

But, as before, we can use & to convert the Proc into a block and pass it into map:

my_proc = Proc.new {|n| n.to_s}
[1,2].map(&my_proc)
# => ["1", "2"]

So, &:to_s in [1,2].map(&:to_s) must be generating a block somehow. At first glance, we see the & operator, so we think: “This must either be converting a block into a Proc or a Proc into a block.” Since map only takes a block, the latter must the case. However, Ruby does not see a Proc object; instead, it sees :to_s. How then does :to_s become a Proc for Ruby to convert into a block?

The answer is that Ruby calls #to_proc on the object after the & (:to_s in this case).

To verify this is true, let’s take a slight detour and write our own#to_proc method in our own MyFineClass. Then when we call [1,2,3].map(&MyFineClass.new), we should see "hello" on the console.

class MyFineClass
def to_proc
puts "hello"
end
end
my_fine_object = MyFineClass.new
[1,2,3].map(&my_fine_object)
# => hello

Good, so we’ve verified that if & is followed by a non-Proc object, Ruby will attempt to convert the non-Proc object into a Proc by calling #to_proc on the non-Proc object.

Now, the reason we often see expressions like [1,2].map(&:to_s) is that :to_s is a Symbol , and Symbol has a #to_proc method that returns a Proc object:

:to_s.to_proc
# => #<Proc:0x007fd0051133e8>

A String object, however, does not have a #to_proc method:

"to_s".to_proc
# => NoMethodError: undefined method `to_proc' for "to_s":String

And this is why we don’t see:

[1,2].map(&"to_s")

So when [1,2].map(&:to_s) is executed, Ruby calls to_proc on :to_s and converts :to_s into a Proc.

We can quickly recreate for illustrative purposes a naive version of Symbol#to_proc:

class Symbol
def to_proc
->(object){object.send(self)}
end
end

The idea behind Symbol#to_proc is that it returns a Proc that takes the Symbol (:to_s in this case) and calls it as a method on the Proc argument (object), using Ruby’s send method. self in this particular case is :to_s.

The Proc returned by Symbol#to_proc is then converted into a block by & and is passed into map:

[1,2].map(&->(object){object.send(:to_s)})

map will successively iterate over [1,2] and first evaluate 1.send(:to_s), then2.send(:to_s).

Let’s recap these steps. The signal flow for [1,2].map(&:to_s) is:

  1. Ruby sees & and expects a Proc after the &.
  2. Instead, Ruby sees a Symbol:to_s, so it calls :to_s.to_proc to convert it into a Proc. This Proc will look like this:
->(object){object.send(:to_s)}

3. Ruby must now convert the Proc into a block because it is passed to map, which takes a block. & converts a Proc into a block, so now the expression looks like this:

[1,2].map(&->(object){object.send(:to_s)})

4. For each element in the [1,2] array, the block is called, using each element of the array as the argument. For example, the first iteration of [1,2].map(&:to_s) will include the following call:

->(n){n.send(:to_s)}.call(1)

And the second iteration will include this call:

->(n){n.send(:to_s)}.call(2)

5. When [1,2].map(&:to_s) finishes iterating, it returns a new array: ["1","2"] , as we’d expect.


The unary & operator is an essential part of Ruby, as it’s a multifaceted tool that wields blocks andProc objects.

I hope this was helpful. Thanks for reading.