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 a just a bit more than a chunk of code, too. 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
Invoking a Block
The simplest way to invoke a block is to use the yield
keyword. Here’s an example:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
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
:
1 2 3 4 5 6 7 8 9 10 11 12 |
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 |
Ruby convention uses 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, Rubyists don’t consider it good practice.
Note also that the yield
keyword accepts arguments. Here, the block invocation passes the #runBlock
‘s name
argument to the block’s n
argument.
Internally, Ruby compiles a method, and the 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 #addTwo
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.
1 2 3 4 5 6 7 8 |
def addTwo(array) array.map { |element| element + 2 } end myArray = [1, 2, 3, 4, 5, 6] p addTwo(myArray) # => [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.
1 2 3 4 5 6 7 8 |
def getElements(array) array.select { |element| element.even? } end myArray = [1, 2, 3, 4, 5, 6] p getElements(myArray) # => [2, 4, 6] |
The #getElements
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; the #select
method pushes these elements onto the array that it 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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
def showProg(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 #showProg
method accepts an interval
argument. Line 11 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 #showProg
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
puts 'Fetching data...' start = Time.now().to_i showProg(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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
puts 'Simulating a different time-consuming process...' start = Time.now().to_i showProg(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 ... #=> All done! |
So here, we have a 20-second process, with a progress notification (in French) every 5 seconds.
Advantages of Blocks
These two different blocks injected in to the same method show 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. Here are the other three:
Ruby: Scope and Closures
Ruby: Procs, Lambdas and Bindings
Ruby: Block Parameters and Return Values