Understanding Ruby blocks

by Jason Swett,

When I first started using Rails I would encounter Ruby blocks in various places. I didn’t even know that they were blocks, they were just weird pieces of code that looked mysterious to me. Below is a common example, the respond_to block often found in controllers.

respond_to do |format|
  format.html # index.html.erb
  format.json { render json: @users }
end

I didn’t understand what I understand now which is that respond_to is a method which accepts a block as an argument.

In the remainder of this post I’m going to show you how to build your own block. Then, afterward, we’ll come back to this respond_to method and understand it a little better.

Writing our own block

In the remainder of this post we’re going to go through the process of writing the code that will produce the following block.

box_around do |box|
  box.text 'It was twenty years ago today'
  box.text 'Sergeant Pepper taught the band to play'
end

The above code will produce the following output when run on the command line.

$ ruby box.rb
*******************************************
* It was twenty years ago today           *
* Sergeant Pepper taught the band to play *
*******************************************

Let’s see how we can accomplish this. But first let’s talk about why we want to use this particular example.

Why this example is meaningful

I want to show you not just _how_ to create a custom Ruby block, but _why_ custom blocks are useful in the first place.

It wouldn’t do to create a block that could just as easily have been a method. If we’re going to create a block, the block should take advantage of the unique benefits of a block.

Let’s examine why our “box of asterisks” is uniquely suited to be a block as opposed to a method (or even a class). Perhaps the best way to illustrate this is to show what the code might have to look like if we had implemented this behavior using a method instead of a block.

box_around([
  'It was twenty years ago today',
  'Sergeant Pepper taught the band to play'
])

Trying it as a class turns out even worse. Remember that we have to somehow tell it when to draw the top line and the bottom line of the box.

box = Box.new
box.start
box.text 'It was twenty years ago today'
box.text 'Sergeant Pepper taught the band to play'
box.end

Here’s another illustration of why a block is better.
What if we wanted to do something fancy like this?

box_around do |box|
  box.title 'Sgt. Pepper'
  box.text 'It was twenty years ago today'
  box.text 'Sergeant Pepper taught the band to play'
end

The expected output might be something like the following.

*******************************************
*               Sgt. Pepper               *
*               -----------               *
*                                         *
* It was twenty years ago today           *
* Sergeant Pepper taught the band to play *
*******************************************

And now here’s the clincher, what if the content contains, for example, a loop?

box_around do |box|
  box.title 'Sgt. Pepper'
  box.text 'It was twenty years ago today'
  box.text 'Sergeant Pepper taught the band to play'

  5.times do
    box.text 'PAUL IS DEAD'
  end
end

There’s just no way to express such a thing as a method, not that I can think of at least. Blocks are uniquely suited for the job.

Now that you’re hopefully sold on my block example, let’s get to work.

Implementing the example

First, a simpler version

Implementing the full block in section 5.1 is actually not super easy, as we’ll see shortly. So let’s start with something easier. Let’s first implement a block of “hello world” complexity.

Below is our objective for our “hello world” block example. We want a method called crappy_box_around to which we can pass a block and have it output a box…a crappy box, but a box nonetheless.

crappy_box_around do
  puts 'This will be surrounded by a box...kind of'
end

That will give us the following output when run on the command line.

$ ruby crappy_box.rb
**************************************************
This will be surrounded by a box...kind of
**************************************************

The reason the box is crappy is that this box isn’t really a box because it only has a top and a bottom, not sides. The upside though is that the implementation is really easy. Below is the definition for our crappy_box_around method.

def crappy_box_around
  puts '*' * 50
  yield
  puts '*' * 50
end

Notice the yield keyword in the crappy_box_around method.
The yield means “take whatever block was passed and execute it here”.

Passing in an object

In the example in section 2.3.1, we called yield to blindly execute whatever block was passed into the crappy_box_around method. There’s also another way to call yield We can pass an argument so that the code that uses our block has an object to work with.

Below is an example of using a method that not only executes a block but provides an object for that block to use. I’ve repeated the crappy_box_around example from section 2.3.1 to make it easy to contrast the two.

crappy_box_around do
  puts 'This will be surrounded by a box...kind of'
end

better_box_around do |box|
  box.text 'This box is a little better'
end

Here’s how we could implement the better_box_around method to support the usage above. Notice how there’s now a Box class that does the heavy lifting. The better_box_around method itself does nothing more than supply a new instance of Box

class Box
  def text(value)
    # The "+ 4" is to account for the asterisk on the left,
    # the asterisk on the right, and the two spaces that
    # separate the text from the asterisks.
    box_width = value.length + 4

    puts '*' * box_width
    puts "* #{value} *"
    puts '*' * box_width
  end
end

def better_box_around
  yield(Box.new)
end

If you want to be able to run this code to see how it works, create a file called better_box.rb with the following content.

class Box
  def text(value)
    # The "+ 4" is to account for the asterisk on the left,
    # the asterisk on the right, and the two spaces that
    # separate the text from the asterisks.
    box_width = value.length + 4

    puts '*' * box_width
    puts "* #{value} *"
    puts '*' * box_width
  end
end

def better_box_around
  yield(Box.new)
end

better_box_around do |box|
  box.text 'This box is a little better'
end

If we run this file it will give us (true to its name) a better box, one that fits the width of its content.

$ ruby better_box.rb
*******************************
* This box is a little better *
*******************************

You can change the text that’s passed in for proof.

ruby better_box.rb
*****************
* It can shrink *
*****************

The full original example

Now let’s write the code that will give us the following output.

$ ruby much_better_box.rb
*******************************************
* It was twenty years ago today           *
* Sergeant Pepper taught the band to play *
*******************************************

As a starting point, let’s try to plug our desired code into our “better box” implementation and see the exact manner in which it falls short. Rename better_box.rb to much_better_box.rb and make the following modifications.

class Box
  def text(value)
    box_width = value.length + 4

    puts '*' * box_width
    puts "* #{value} *"
    puts '*' * box_width
  end
end

def much_better_box_around
  yield(Box.new)
end

much_better_box_around do |box|
  box.text 'It was twenty years ago today'
  box.text 'Sergeant Pepper taught the band to play'
end

This gives us the following obviously-wrong output.

$ ruby much_better_box.rb
*********************************
* It was twenty years ago today *
*********************************
*******************************************
* Sergeant Pepper taught the band to play *
*******************************************

Instead of outputting a top and bottom border for each individual line of text, we need to somehow output just one top border before all the text and one bottom border after.

class Box
  attr_reader :lines

  def initialize
    @lines = []
  end

  def border
    '*' * 50
  end

  def text(value)
    @lines << value
  end
end

def much_better_box_around
  box = Box.new
  yield(box)

  puts box.border

  box.lines.each do |value|
    puts "* #{value} *"
  end

  puts box.border
end

much_better_box_around do |box|
  box.text 'It was twenty years ago today'
  box.text 'Sergeant Pepper taught the band to play'
end

When we run that code, we get the following.

ruby much_better_box.rb
**************************************************
* It was twenty years ago today *
* Sergeant Pepper taught the band to play *
**************************************************

This is better, although there are still some problems. The border width is just a fixed 50 characters. Let's make it so the border matches the width of the longest line. Or more precisely, the width of the longest line plus a few characters to account for spacing as well as the left and right borders on each line.

class Box
  attr_reader :lines

  def initialize
    @lines = []
  end

  def border
    '*' * (length_of_longest_line + 4)
  end

  def text(value)
    @lines << value
  end

  def length_of_longest_line
    @lines.map(&:length).max
  end
end

def much_better_box_around
  box = Box.new
  yield(box)

  puts box.border

  box.lines.each do |value|
    puts "* #{value} *"
  end

  puts box.border
end

much_better_box_around do |box|
  box.text 'It was twenty years ago today'
  box.text 'Sergeant Pepper taught the band to play'
end

Here's what we get now.

$ ruby much_better_box.rb 
*******************************************
* It was twenty years ago today *
* Sergeant Pepper taught the band to play *
*******************************************

The final thing we need to do is get that right-hand border to look right.

class Box
  attr_reader :lines

  def initialize
    @lines = []
  end

  def border
    '*' * (length_of_longest_line + 4)
  end

  def text(value)
    @lines << value
  end

  def formatted_line(value)
    "* #{value.ljust(length_of_longest_line, ' ')} *"
  end

  def length_of_longest_line
    @lines.map(&:length).max
  end
end

def much_better_box_around
  box = Box.new
  yield(box)

  puts box.border

  box.lines.each do |value|
    puts box.formatted_line(value)
  end

  puts box.border
end

much_better_box_around do |box|
  box.text 'It was twenty years ago today'
  box.text 'Sergeant Pepper taught the band to play'
end

There we go! Our box is now fully working.

$ ruby much_better_box.rb
*******************************************
* It was twenty years ago today           *
* Sergeant Pepper taught the band to play *
*******************************************

Now let's go back to the controller example from the very beginning.

Coming back to the controller example

Armed with the understanding gained in the above exercises, we can now see clearly what's happening with respond_to

respond_to do |format|
  format.html # index.html.erb
  format.json { render json: @users }
end

We know now that the format part must be an instance of some object, just as box was an instance of the Box class in our examples.

In fact, by throwing a binding.pry inside the block, we can see what that object is. The format variable is an instance of something called ActionController::MimeResponds::Collector.

It's not that important to understand the details of what ActionController::MimeResponds::Collector is all about. The important thing to understand here is that format is an instance of a class, an instance that the respond_to method passes back to us.

Conclusion

For certain kinds of programming tasks, blocks can add a level of expressiveness that's hard or impossible to achieve using any other approach. Now that you're armed with a better understanding of blocks you can start taking advantage of the power of blocks in your own code.

  •  
  •  
  •  
  •  

Leave a Reply

Your email address will not be published. Required fields are marked *