Ruby: Blocks

Ruby: Blocks

What are Blocks?

Blocks in Ruby are chunks of code that are — almost — nothing but chunks of code. A block is not a method. It is not a proc or a lambda. And, despite the popular statement that “in Ruby, everything is an object,” a block is not an object, either.

I say “almost” nothing but a chunk of code because a Ruby block is not quite that, either. It can accept arguments. It returns a value. And, since it has permanent access to the context in which it is created, it is also a closure.

Blocks permit a chunk of code to be passed to a method. The method can then invoke the block as needed. This is called yielding. The method executes, and can yield control to the block when desired.

These are the rules for blocks:

  • Blocks can’t stand alone — they must follow a method invocation
  • A block argument is optional in a method invocation
  • All methods implicitly accept a block argument
  • Methods may ignore a block argument
  • If a method attempts to invoke its block argument, and the caller does not provide one, the method will throw an error

The simplest way to invoke a block is to use the yield keyword. Here’s an example:

def test() 
  puts "Starting the test() method."
  puts "Next, we'll execute the block."
  yield
  puts "Back to the method. All done!"
end

test() { puts "Here's the block code..." }

#=> Starting the test() method. 
#=> Next, we'll execute the block.
#=> Here's the block code...
#=> Back to the method. All done!        

Ruby has two different syntaxes for blocks. Here’s an example of each, both passed to a method called runBlock:

def runBlock(name) 
  yield name
end

runBlock('Bob') { |n| puts "Hello, my name is #{n}." }

runBlock('Bob Rodes') do |n|
  puts "Hello, my name is #{n}."
  puts 'I hope you are well today.'
end        

Rubyists use bracket notation for one-line blocks, and the do notation for larger blocks. While it is possible to use bracket notation for multiple-line blocks, it isn’t considered good practice.

Note also that the yield keyword accepts arguments. Here, the block invocation passes runBlock‘s name argument to the block’s n argument.

Internally, Ruby compiles a method, and a block that is passed to it, as two separate entities. When the method executes, it can call the block at some specific point or points.

Blocks in Ruby’s Native Methods

The most common use of blocks is in conjunction with Ruby’s various iterator methods (each, map, select, etc.). With these, the iterator method provides the process of iteration, and the block provides the specifics of what each iteration of the iterator does. The iterator implements the process of iteration, and defines what it will do with the result of each iteration. The block defines what that result will be.

Here’s an example, using the map method. In this bit of code, the add_two method accepts an array argument. The array calls the map method, passing it a block. The method iterates through the array, passing its elements one by one to the block. The block adds two to the element and returns it.

def add_two(array)
  array.map { |element| element + 2 }
end

my_array = [1, 2, 3, 4, 5, 6]
p add_two(my_array) # => [3, 4, 5, 6, 7, 8]        

Ruby’s map method iterates through its receiver. (A receiver is a caller of a method. With the map method, the receiver is usually an array, but it can be an instance of any class that includes Enumerable.) Each element in the receiver gets passed to a supplied block. The block returns a value, and the map method pushes this value into a new array. When the iteration of the receiver is complete, the map method returns the new array.

Here’s one more example, using the select method. The #select method also iterates through its receiver in the same way as the #map method. The block returns a true or false value. If true, the select method pushes the element into a new array. If false the method does nothing. When the iteration of the receiver is complete, the select method returns the new array.

def get_events(array)
  array.select { |element| element.even? }
end

my_array = [1, 2, 3, 4, 5, 6]
p get_events(my_array) # => [2, 4, 6]        

The get_events method accepts an array argument. The array calls the select method, passing it a block. The method iterates through the array, passing the elements one by one to the block. The block returns true for each element that is an even number; these elements get pushed on to the array that the select method returns.

Using Custom Blocks

Beyond using Ruby’s iterator methods, we can use blocks in any situation where we might want to pass flow control of a method to a chunk of code that the caller provides. Suppose, for example, we want to implement a progress notification method. The method will yield to a block every so many seconds, as specified in an argument. So:

def show_prog(interval)
  start = Time.now().to_i
  last = start

  loop do
    current = Time.now().to_i
    if current >= last + interval
      elapsed = current - start
      last = current
      break if yield elapsed
    end
  end
end        

This show_prog method accepts an interval argument. The line break if yield elapsed is where the block argument gets injected into the method (using yield). If the block returns true, the loop exits. The elapsed variable gets passed to the yield invocation.

If you were going to use something like this in the real world, you would have to use multiple threads or Ruby’s Async gem to keep show_prog from blocking whatever it was reporting progress on. But we can simulate a long-running process to show how the yield invocation works, and to demonstrate that the block controls custom behavior.

puts 'Fetching data...'
start = Time.now().to_i

show_prog(2) do |s| 
  puts "Simulating long data retrieval; #{s} seconds gone by..." 
  Time.now().to_i >= start + 10
end

puts 'Data fetched!'

#=> Fetching data...
#=> Simulating long data retrieval; 2 seconds gone by...
#=> Simulating long data retrieval; 4 seconds gone by...
#=> Simulating long data retrieval; 6 seconds gone by...
#=> Simulating long data retrieval; 8 seconds gone by...
#=> Simulating long data retrieval; 10 seconds gone by...
#=> Data fetched!        

Here we simulate a 10-second process, with a progress notification every 2 seconds.

Note that the block has access to the start variable declared in the main context. This is because, unlike Ruby methods, Ruby blocks are closures.

Here’s another call to #showProg with a different block, which demonstrates that you can use blocks to customize the behavior of the method:

puts 'Simulating a different time-consuming process...'
start = Time.now().to_i

show_prog(5) do |s| 
  puts "Et puis en français : #{s} secondes passées ..." 
  Time.now().to_i >= start + 20
end
puts 'All done!'

#=> Simulating a different time-consuming process...
#=> Et puis en français : 5 secondes passées ...
#=> Et puis en français : 10 secondes passées ...
#=> Et puis en français : 15 secondes passées ...
#=> Et puis en français : 20 secondes passées ...        

So here, we have a 20-second process, with a progress notification (in French) every 5 seconds.

Advantages of Blocks

Injecting these two different blocks into the same method shows the flexibility that yielding to blocks affords. A method can do something very generic like iterating through a collection of items, and then, for each iteration, pass control of what to do to the caller in the form of yielding to a passed-in block.

Ruby blocks are an example of the Inversion of Control or IOC pattern (more info here). One of the perceived advantages of the Inversion of Control model is this decoupling of the repeatable aspects of a problem from its one-off aspects. Separating out the block allows a separate focus on what gets repeated with every block call, and that repetition makes for very robust code.

Related Articles

This article is one of a series of four, in no particular order. Here are the other three:

Ruby: Scope and Closures

Ruby: Procs, Lambdas and Bindings

Ruby: Block Parameters and Return Values

To view or add a comment, sign in

More articles by Robert Rodes

Explore content categories