Proc <> Code Block Conversion and Ampersand(&) in Ruby
This is the last part of a three-parts series where we take a close looks at Code Block, Proc, Lambda, and Closure in Ruby.
In Part 1, we covered:
- Definitions of Code Block, Proc, Lambda, and Closure
- Constructions
- Calling a Code Block/Proc/Lambda
- Passing and Returning Procs
In Part 2, we covered:
5. Scopes, Universes, and Lunch Boxes [FUN STUFF]
6. Differences between Procs and Lambdas
In this post, the last part of the series, we will discuss:
7. Proc <> Code Block Conversion and ampersand(&) [FUN STUFF]
- A code block vs an argument in the context of a method
- Behind the scenes of [‘hello’, ‘world’].map(&:upcase)
Let’s dive into it!
7. Proc <> Code Block Conversion and ampersand(&)
Hold on! Before we jump into this topic, let’s briefly revisit some foundations covered in Part 1:
- A proc is an object that contains a code block so that the code block can be executed later
- A proc, similar with all other objects, can be passed into a method as an argument, and a method execute a code block inside a proc by calling the proc.
- A code block can be attached to a method directly, and a method executes the code block by yielding it.
Now, here are the questions:
- can we attach a code block to a method and call it later?
- can we pass a proc into a method and yield it later?
The answers are no and no.
Because you call a proc and you yield a code block.
BUT, we can:
- attach a code block to a method, convert it to a proc, and call the proc later
- pass a proc into a method, convert it to a code block, and yield it later
The conversion between a proc and a code block happens with the help of ampersand(&).
Let’s look at an example:
In the code above, we clearly do pass in a block into the method. We also indeed call the “block” inside the method. What actually happened, thanks to the ampersand(&), was that the passed-in block got converted to a proc.
You can think of the ampersand as a magic wand: when it’s prepended to an argument of a method, it converts the appended code block to a proc and assigns the result to the argument name.
In the example, the appended code block was converted to a proc, and the resulting proc was assigned to the variable named “block”.
The other way works as well:
If an ampersand is prepended to a proc, it converts the proc into a block and appends the block to the method being called.
In line 006, we prepended an ampersand to the proc, which got converted into a block, and appended it to the method. The method later yielded the block.
There’s one thing worth emphasizing: the ampersand converts a proc into a block and appends the block to the method, NOT passing the block in as an argument to the method.
Appending a code block to a method and passing an argument into a method are TWO DIFFERENT THINGS. Although they can both be used by the method, they are two different things.
They are as different as how a jacket is different than a pair of pants. You can wear a jack. You can wear a pair of pants. You can wear a jack and a pair of pants at the same time. You can use the material of a pair of pants, convert it into a jacket, and wear it. But wearing a jacket converted from a pair of pants is not the same a wearing a pair of pants!
What happens when you confuse a code block as an argument? irb will yell at you:
In give_me_another_block method is expecting an argument. But with the ampersand prepended to a proc, we convert the proc to a block and append it to the method. Although it looks like we pass an argument to the method, we actually append a block to the method without passing any arguments in. That’s why irb gives us the wrong number of arguments method.
Now we pass the method an argument and append it a block. The same as you wearing a jacket and a pair of pants at the same time!
Enough fun with jackets and pants! Let’s switch our attention to the “magical” ampersand: what exactly is happening?
When we do &pr we are actually doing pr.to_proc.call:
- we first call the to_proc method on pr,
- then we call the proc returned by to_proc.
The magic wand ampersand(&) is nothing but another ruby syntactic sugar.
Does &pr reminds you of any old friends you have? like .map(&:upcase)? What are their relationships? Let’s think about that for a second …
The answer is, in .map(&:upcase), the ampersand is doing exactly the same thing as it does for &pr: it calls to_proc.call.
Consider [‘Hi’, ‘Yo’].map(&:upcase).
On one hand, we know it is the same as:
- [‘Hi’, ‘Yo’].map { |element| element.upcase }, which is the same as
- [‘Hi’, ‘Yo’].map { |element| element.send(:upcase) }
On the other hand, we know &:upcase is the same as :upcase.to_proc.call
So [‘Hi’, ‘Yo’].map(&:upcase) is the same as:
- [‘Hi’, ‘Yo’].map { |element| :upcase.to_proc.call(element) }
How does
[‘Hi’, ‘Yo’].map {|element| :upcase.to_proc.call(element) }
becomes
[‘Hi’, ‘Yo’].map { |element| element.send(:upcase) }
??
Well, the key lies in the definition of to_proc in the Symbol class.
And I will leave it as a homework for you :)