Blocks and iterators

Ruby · Essentials · 14 min

Blocks are Ruby's signature feature: almost every loop, callback and DSL in Ruby is a block.

A block is a chunk of code passed to a method call, written with braces for one-liners or do ... end for multi-line code:

[1, 2, 3].each { |n| puts n }
File.open("log.txt") do |f|
  puts f.readline
end

The method decides when and how often to run the block, passing values in through the |params|. Core iterators:

  • each runs the block for each element and returns the receiver.
  • map returns a new array of the block's results.
  • select/filter keeps elements where the block is truthy; reject drops them.
  • find returns the first match, any?/all?/count test conditions.
  • each_with_index, each_slice(2), times, upto.

Inside your own method, yield calls the block given by the caller, and block_given? checks whether there is one. Methods that set up and tear down something around a block (like File.open) are a key Ruby idiom: the cleanup happens even if the block raises.

Shorthands: map(&:upcase) calls a method on each element, and in Ruby 3.4 it refers to the single block parameter: nums.map { it * 2 }.

Blocks are closures: they can read and modify local variables from the surrounding scope. next value ends the current block call with that value; break exits the whole method call.

Example

nums = [1, 2, 3, 4, 5]
p nums.map { |n| n * n }          # [1, 4, 9, 16, 25]
p nums.select(&:even?)            # [2, 4]
p nums.find { it > 3 }            # 4 (Ruby 3.4 `it`)
p %w[a b].map(&:upcase)           # ["A", "B"]

def twice
  return :no_block unless block_given?
  yield 1
  yield 2
end
twice { |x| puts "got #{x}" }     # got 1 / got 2
p twice                           # :no_block

def timed(label)
  start = Time.now
  yield
ensure
  puts "#{label} took #{(Time.now - start).round(3)}s"
end
timed("sum") { (1..1_000_000).sum }   # sum took 0.0s (roughly)

total = 0
nums.each { |n| total += n }      # blocks see outer locals
p total                           # 15

Remember

  • Blocks are code passed to methods; yield runs them
  • map transforms, select filters, each just iterates
  • &:method and it are shorthands for simple blocks

Watch out for

  • Using each when you need the transformed values; each returns the original array, not the block results.

Check yourself

  1. What does [1, 2, 3].each { |n| n * 10 } return?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.ruby-lang.org/en/3.4/syntax/calling_methods_rdoc.html#label-Block+Argument