Blocks & Yield

Master Ruby blocks and the yield keyword. Learn do/end vs {}, block_given?, and creating flexible APIs.

Intermediate 1 min read

Blocks & Yield Mastery

Master Ruby blocks and the yield keyword. Learn how to create flexible, reusable code with blocks, understand the difference between do/end and curly braces, and harness the power of yield for creating elegant APIs and DSLs.

What are Blocks?

Blocks are chunks of code that can be passed to methods. They're Ruby's way of implementing closures - code that can be executed later with access to variables from its original scope.

# Blocks are everywhere in Ruby
[1, 2, 3, 4, 5].each { |n| puts n }
[1, 2, 3, 4, 5].each do |n|
puts n * 2
end

Key Insight: Blocks are not objects - they're syntactic constructs that allow you to pass code to methods.

Block Syntax: do/end vs {}

Ruby provides two ways to write blocks: curly braces {} for single-line blocks and do/end for multi-line blocks.

# Curly braces - single line
numbers = [1, 2, 3, 4, 5]
squares = numbers.map { |n| n * n }
puts squares
# do/end - multi-line
numbers.each do |n|
if n.even?
puts "#{n} is even"
else
puts "#{n} is odd"
end
end

Precedence Warning: Curly braces have higher precedence than do/end. Use do/end for method chaining.

# This might not work as expected
result = [1, 2, 3].map { |x| x * 2 }.select { |x| x > 3 }
# Better to use do/end for chaining
result = [1, 2, 3].map do |x|
x * 2
end.select do |x|
x > 3
end

The Yield Keyword

The yield keyword is how methods execute blocks. It's the bridge between your method and the block passed to it.

# Basic yield example
def greet
puts "Hello!"
yield
puts "Goodbye!"
end
greet do
puts "How are you?"
end
# Yield with parameters
def process_items(items)
items.each do |item|
puts "Processing: #{item}"
yield(item)
end
end
process_items([1, 2, 3]) do |item|
puts " Result: #{item * 2}"
end

block_given? Method

Use block_given? to check if a block was passed to your method. This makes your methods more flexible.

# Optional block with block_given?
def process_data(data)
result = data.map { |x| x * 2 }
if block_given?
yield(result)
else
result
end
end
# With block
process_data([1, 2, 3]) { |result| puts "Sum: #{result.sum}" }
# Without block
result = process_data([1, 2, 3])
puts result

Block Parameters

Blocks can accept parameters, and you can pass multiple values to yield.

# Multiple parameters
def calculate(a, b)
sum = a + b
product = a * b
yield(sum, product)
end
calculate(3, 4) do |sum, product|
puts "Sum: #{sum}, Product: #{product}"
end
# Array destructuring in blocks
def process_pairs
pairs = [[1, 2], [3, 4], [5, 6]]
pairs.each do |x, y|
yield(x, y)
end
end
process_pairs do |x, y|
puts "#{x} + #{y} = #{x + y}"
end

Block Return Values

Blocks can return values, and yield can capture and use those return values.

# Capturing block return values
def transform_data(data)
data.map do |item|
result = yield(item)
puts "Transformed #{item} to #{result}"
result
end
end
numbers = [1, 2, 3, 4, 5]
squares = transform_data(numbers) { |n| n * n }
puts "Final result: #{squares}"

Common Block Patterns

# Timing blocks
def time_it
start_time = Time.now
result = yield
end_time = Time.now
puts "Execution time: #{end_time - start_time} seconds"
result
end
result = time_it do
sleep(1)
"Done!"
end
# Resource management
def with_file(filename)
file = File.open(filename, 'w')
begin
yield(file)
ensure
file.close
end
end
with_file('test.txt') do |file|
file.puts "Hello, World!"
end
# Conditional execution
def if_condition(condition)
yield if condition
end
if_condition(true) { puts "This will execute" }
if_condition(false) { puts "This won't execute" }

Interactive Code Runner

Practice Time: Try these block and yield examples and experiment with different block patterns. Understanding blocks will make you a more effective Ruby programmer.

Interactive Code Runner

Ruby Code Editor
Output will appear here when you run the code...

Example 1: Custom Map Method

# Implementing map with blocks
def my_map(array)
result = []
array.each do |item|
result << yield(item)
end
result
end
numbers = [1, 2, 3, 4, 5]
squares = my_map(numbers) { |n| n * n }
puts squares

Example 2: Resource Management

# Safe resource handling with blocks
def with_database_connection
connection = Database.connect
begin
yield(connection)
ensure
connection.close
end
end
with_database_connection do |db|
puts \"Connected to database\"
# Database operations here
end

Best Practices

1. Use do/end for Multi-line Blocks

Use do/end for blocks that span multiple lines and {} for single-line blocks. This improves readability.

2. Always Check block_given?

If your method can work with or without a block, always use block_given? to check before calling yield.

3. Use Blocks for Resource Management

Blocks are perfect for ensuring resources are properly cleaned up, like file handles or database connections.

4. Consider Method Chaining

When chaining methods, use do/end to avoid precedence issues with curly braces.

Common Pitfalls

1. Precedence Issues with {} vs do/end

Curly braces have higher precedence than do/end. This can cause unexpected behavior in method chaining.

2. Forgetting block_given?

Calling yield without checking block_given? will raise a LocalJumpError if no block is provided.

3. Variable Scope Confusion

Blocks create a new scope. Variables defined inside blocks don't affect the outer scope unless they're already defined.

Summary

Blocks and yield are fundamental to Ruby's expressiveness. They enable:

  • Flexible method APIs that accept custom behavior
  • Resource management with automatic cleanup
  • Functional programming patterns
  • Domain-specific languages (DSLs)
  • Iteration and collection processing

Master blocks and yield to write more expressive, flexible, and Ruby-like code.