My Struggle with Ruby Blocks
My name is Rob and I had trouble understanding Ruby blocks.
This is my story.
FADE IN:
Oh, hey, here’s a simple example of a block in Ruby:
display_name do
puts "Rob"
end
Wait, why not just do this:
display_name("Rob")
Oh, I see, because display_name
doesn’t want to decide how to display my name. Instead, it wants me to supply the line of code that does the displaying. Three cheers for flexibility!
Let’s add some comments to illustrate this difference:
# I'm passing a block of code for display_name to run
display_name do
puts "Rob"
end
# > Rob
# I'm passing a value for display_name to use with its own printing code
display_name("Rob")
# > Rob
But I don’t really understand how display_name
runs my block of code. Maybe it’ll make more sense when I look at the method definition:
def display_name
yield
end
Hmm. That method definition doesn’t exactly scream, “Yo, I take block of code and run it!” There’s no argument in the method signature that accepts my block, and I don’t see anything that actually calls my code either.
Hidden in plain view
After some digging, it turns out ANY Ruby method can invisibly accept a block and the yield
statement illustrates the calling of the given block (at runtime my code goes right where that yield
statement is).
Wow, that’s TOTALLY not obvious.
There’s an alternate–and more explicit–way of defining such a method:
def display_name(&block)
block.call if block
end
Above, there’s an argument in the method signature that indicates that there’s a block of coding being passed in and explicitly called. There’s even a check to make sure a block was provided!
Despite its clarity, nobody uses this pattern because it’s slow and Rubyist tend to favor brevity.
OK, achievement unlocked, but the example above is too trivial. It doesn’t illustrate what blocks are good for. Let’s modify it slightly:
def display_name
puts "I'm about to run your code..."
yield
puts "That felt good."
end
The method isn’t much more useful, but I kinda-sorta see where we’re going with this. The method can do all sorts of things before and after it runs my block. I’m starting to see blocks as a form of code injection.
Putting on my big boy pants
I’ve got this now. Let’s do some Rails!
@product = Product.find(params[:id])
respond_to do |format|
format.html
format.json { render json: @product.to_json }
end
OK. Based on what I know, I’m calling respond_to
and passing it 2 lines of code to run, which are:
format.html
format.json { render json: @product.to_json }
But what is format
? Where does its value come from and why am I calling methods on it?
Is format
an argument to respond_to
?
<< scratches head >>
No, that would look like this:
respond_to(arg) do |format|
Or as we Rubyist are accustomed to:
respond_to arg do |format|
Turns out that format
is an argument to my block. Its value is passed (or yielded) to me by the method I’m calling.
Let me phrase that differently: I’m calling respond_to
which does some stuff and then passes my block a value held in the variable format
.
Right, but what is format? What can I do with it?
respond_to do |format|
# Umm...
end
A minor epiphany
Here was the major realization I had about blocks: without understanding the inner-workings of the method I’m passing a block to, I can’t really write my block.
I couldn’t treat it as a black box, as badly as I wanted to. With many methods (or APIs), I can simply pass in arguments and get a return value without ever having to understand what actually goes on inside. And that encapsulation is a beautiful thing.
But with blocks, I am actively participating in writing the method I’m calling, so I have to understand the context in which my code is called. I also have to understand the objects that are yielded to my block via its arguments.
Let’s play detective:
respond_to do |format|
# What is format, really?
logger.debug format.class
end
# > ActionController::MimeResponds::Collector
I’m calling respond_to
and passing it some lines of code to run. My block is given a Collector
object to work with via the variable named format
.
Now I can look at the Collector
class and see what methods I can call (such as html
and json
). Ah-ha!
Iterators
I’d be remiss if I didn’t touch on iterators. See, in Ruby, blocks are also used quite frequently to iterate over collections:
food = %w{apple orange grape coconut}
food.each do |f|
puts "I like to eat #{f}"
end
Armed with my knowledge about blocks, this code is super-easy to understand!
I’m calling food.each
once and it does all the legwork required to loop through the array which called it (i.e., food
). Every iteration within each
executes my puts
block, passing it one of them items from the array via the variable f
.
And that’s all there is to it! :-)
Comments? Questions?
Did I miss anything? Veteran Rubyists dying to tear me to shreds? Leave a comment below.