Here’s a code sample that I’ve grabbed more or less at random from the Rails codebase.
def form_for(record, options = {}, &block)
The first two arguments, record
and options = {}
, are straightforward to someone who’s familiar with Ruby. But the third argument, &block
, is a little more mysterious. Why the leading ampersand?
This post will be the answer to that question. In order to begin to understand what the leading ampersand is all about, let’s talk about how blocks relate to Proc
objects.
Blocks and Proc objects
Let’s talk about blocks and Proc
objects a little bit, starting with Proc
objects.
Here’s a method which takes an argument. The method doesn’t care of what type the argument is. All the method does is output the argument’s class.
After we define the method, we call the method and pass it a Proc
object. (If you’re not too familiar with Proc
objects, you may want to check out my other post, Understanding Ruby Proc objects.)
def proc_me(my_proc)
puts my_proc.class
end
proc_me(Proc.new { puts "hi" })
If you run this code, the output will be:
Proc
Not too surprising. We’re passing a Proc
object as an argument to the proc_me
method. Naturally, it thinks that my_proc
is a Proc
object.
Now let’s add another method, block_me
, which accepts a block.
def proc_me(my_proc)
puts my_proc.class
end
def block_me(&my_block)
puts my_block.class
end
proc_me(Proc.new { puts "hi" })
block_me { puts "hi" }
If we run this code the output will be:
Proc
Proc
Even though we’re passing a Proc
object the first time and a block the second time, we see Proc
for both lines.
The reason that the result of my_block.class
is Proc
is because a leading ampersand converts a block to a Proc object.
Before moving on I encourage you to try out the above code in a console. Poke around at the code and change some things to see if it enhances your understanding of what’s happening.
Converting the Proc object to a block before passing the Proc object
Here’s a slightly altered version of the above example. Notice how my_proc
has changed to &my_proc
. The other change is that Proc.new
has changed to &Proc.new
.
def proc_me(&my_proc) # an & was added here
puts my_proc.class
end
def block_me(&my_block)
puts my_block.class
end
proc_me(&Proc.new { puts "hi" }) # an & was added here
block_me { puts "hi" }
If we run this code the output is the exact same.
Proc
Proc
This is because not only does a leading ampersand convert a block to a Proc object, but a leading ampersand also converts a Proc object to a block.
When we do &Proc.new
, the leading ampersand converts the Proc
object to a block. Then the leading ampersand in def proc_me(&my_proc)
converts the block back to a Proc
object.
I again encourage you to run this code example for yourself in order to more clearly understand what’s happening.
The differences between blocks and Proc objects
Ruby has a class called Proc
but no class called Block
. Because there’s no class called Block
, nothing can be an instance of a Block
. The material that Ruby blocks are made out of is Proc objects.
What happens when we try this?
my_block = { puts "hi" }
If we try to run this, we get:
$ ruby block.rb
block.rb:1: syntax error, unexpected string literal, expecting `do' or '{' or '('
my_block = { puts "hi" }
block.rb:1: syntax error, unexpected '}', expecting end-of-input
my_block = { puts "hi" }
That’s because the syntax { puts "hi" }
doesn’t make any syntactical sense on its own. If we want to say { puts "hi" }
, there are only two ways we can do it.
First way: put it inside a Proc object
That would look like this:
Proc.new { puts "hi" }
In this way the { puts "hi" }
behavior is “packaged up” into an entity that we can then do whatever we want with. (Again, see my other post on Ruby proc objects for more details.)
Second way: use it to call a method that takes a block
That would look like this:
some_method { puts "hi" }
Why converting a block to a Proc object is necessary
Let’s take another look at our code sample from the beginning of the post.
def form_for(record, options = {}, &block)
In methods that take a block, the syntax is pretty much always &block
, never just block
. And as we’ve discussed, the leading ampersand converts the block into a Proc
object. But why does the block get converted to a Proc
object?
Since everything in Ruby is an instance of some object, and since there’s no such thing as a Ruby Block
class, there can never be an object that’s a block. In order to be able to have an instance of something that represents the behavior of a block, that thing has to take the form of a Proc
object, i.e. an instance of the class Proc
, the stuff that Ruby blocks are made out of. That’s why methods that explicitly deal with blocks convert those blocks to Proc
objects first.
Takeaways
- A leading ampersand converts a block to a
Proc
object and aProc
object to a block. - There’s no such thing as a Ruby
Block
class. Therefore no object can be an instance of a block. The material that Ruby blocks are made out of isProc
objects. - The previous points taken together are why Ruby block arguments always appear as e.g.
&block
. The block can’t be captured in a variable unless it’s first converted to aProc
object.
Another fun thing about the & is that it generally calls #to_proc on whatever follows it. So something like this:
[‘one’, ‘two’, ‘three’].map(&:upcase)
is basically:
[‘one’, ‘two’, ‘three’].map {|str| str.send(:upcase) }
because symbols have a built-in #to_proc method in which they send themselves to the proc’s argument. (Of course, map(:upcase.to_proc) (with no &) won’t work as the & has to be there to signal to use the resulting proc as a block and not an argument. (&:upcase.to_proc) will work because calling #to_proc on a proc just returns self.)
This can be taken to extremes (just for illustration) — for example:
obj = Object.new
def obj.to_proc; Proc.new { print ‘hi!’ }; end
[1,2,3].each(&obj) # => ‘hi!hi!hi!’
I’m not sure I’ve ever seen anything like that in real life but it’s cool that it works đŸ™‚
Thanks for this! I always kinda understood what was happening there, but never dug into the specifics. Once I understood that you can’t have a block be assigned to a variable, `&block` became a head-slapper.
I’m not sure the article gets to the nub of the issue for me. You talk about there being no Block object, but I think it’s more accurate to say there’s not really such a thing as a block. When you pass a code block to a method directly that is a proc. But the obvious issue is when you want to pass an existing proc in, that special “block” thing that takes an inline proc isn’t a parameter on the method. The & is just telling ruby “set this thing as the proc of the method call” when you make the call, and the & in the param list says “set this variable to the proc of the method call”
It gets interesting when you grab that proc in the method and store it for later use. That’s how you can build interesting DSLs
This stuff does my head in. plus lambdas and arities. in addition to the closure complications / differences between them all.
i can’t help but think it’s needlessly complicated.
> The material that Ruby blocks are made out of is Proc objects.
I’ve seen you state this in another article of yours, but shouldn’t it be the other way around?
According to the Ruby documentation (https://ruby-doc.org/core-3.1.2/Proc.html), “A Proc object is an encapsulation of a block of code […]”. From what I understand, this means that Proc objects basically wraps a block of code.
It makes perfect sense to me that a Proc is an abstraction/wrapper class. But, I don’t see how **Ruby blocks being made out of encapsulated blocks** makes sense.