Overview
Ruby blocks represent executable code segments that can be passed to methods and executed within different contexts. Ruby implements blocks through two distinct syntaxes: the lightweight brace syntax {}
for single-line operations and the do...end
syntax for multi-line code segments. Methods receive blocks through the yield
keyword or by converting them to Proc objects using the &
operator.
Blocks operate with lexical scoping, meaning they capture variables from their surrounding environment and maintain access to those variables when executed. Ruby provides three primary mechanisms for working with callable objects: blocks (which cannot be stored in variables), Proc objects (which can be stored and called multiple times), and lambdas (which enforce argument checking and have different return behavior).
# Brace syntax for simple operations
[1, 2, 3].each { |n| puts n * 2 }
# do...end syntax for complex operations
File.open("data.txt") do |file|
content = file.read
process_content(content)
end
# Converting blocks to Proc objects
def capture_block(&block)
stored_proc = block
stored_proc.call("argument")
end
Basic Usage
Methods receive blocks through the yield
keyword, which executes the block with optional arguments. Ruby raises a LocalJumpError
if code calls yield
without a block present. The block_given?
method checks for block presence before yielding.
def process_items(items)
return enum_for(:process_items, items) unless block_given?
items.each do |item|
result = yield(item)
puts "Processed: #{result}"
end
end
process_items([1, 2, 3]) { |n| n * n }
# Processed: 1
# Processed: 4
# Processed: 9
Block parameters appear between pipe characters and support destructuring for complex data structures. Ruby assigns nil
to excess parameters and ignores extra arguments passed to blocks.
data = [["Alice", 30], ["Bob", 25], ["Carol", 35]]
data.each do |name, age|
puts "#{name} is #{age} years old"
end
# Destructuring with excess parameters
data.each do |name, age, city|
puts "#{name} (#{age}) from #{city || 'Unknown'}"
end
Ruby's standard library extensively uses blocks for iteration, resource management, and configuration. The each
method forms the foundation for most iteration patterns, while map
transforms collections and select
filters elements based on block return values.
numbers = [1, 2, 3, 4, 5]
# Transform elements
squares = numbers.map { |n| n ** 2 }
# => [1, 4, 9, 16, 25]
# Filter elements
evens = numbers.select { |n| n.even? }
# => [2, 4]
# Resource management
File.open("config.txt") do |file|
config = file.read
# File automatically closes when block exits
end
Methods can yield multiple times within a single call, creating powerful iteration and generation patterns. The block executes in the context where it was defined, not where the method calls yield
.
def repeat(n)
n.times do |i|
yield(i + 1)
end
end
repeat(3) do |iteration|
puts "Iteration #{iteration}"
end
# Iteration 1
# Iteration 2
# Iteration 3
Advanced Usage
Converting blocks to Proc objects using the &
parameter enables storage, conditional execution, and passing blocks between methods. The ampersand operator converts blocks to Proc objects and Proc objects back to blocks.
class EventHandler
def initialize(&callback)
@callback = callback
end
def handle_event(data)
@callback&.call(data) if @callback
end
def chain_handler(&additional)
original = @callback
@callback = proc do |data|
original&.call(data)
additional&.call(data)
end
end
end
handler = EventHandler.new { |data| puts "Primary: #{data}" }
handler.chain_handler { |data| puts "Secondary: #{data}" }
handler.handle_event("test")
# Primary: test
# Secondary: test
Proc objects and lambdas differ significantly in argument handling and return behavior. Lambdas enforce argument count matching and return from the lambda itself, while Proc objects ignore extra arguments and return from the enclosing method.
def demonstrate_returns
p = proc { return "from proc" }
l = lambda { return "from lambda" }
result = p.call # This returns from demonstrate_returns
puts "After proc: #{result}" # Never executes
end
def demonstrate_lambda_returns
l = lambda { return "from lambda" }
result = l.call # This returns only from lambda
puts "After lambda: #{result}" # This executes
"method return"
end
puts demonstrate_lambda_returns
# After lambda: from lambda
# method return
Block-based DSLs leverage instance_eval
and instance_exec
to execute blocks in different contexts. This pattern enables configuration objects and builder patterns common in testing frameworks and configuration libraries.
class ConfigBuilder
def initialize
@config = {}
end
def database(&block)
db_config = DatabaseConfig.new
db_config.instance_eval(&block)
@config[:database] = db_config.to_h
end
def self.build(&block)
builder = new
builder.instance_eval(&block)
builder.to_h
end
def to_h
@config
end
end
class DatabaseConfig
def initialize
@settings = {}
end
def host(value)
@settings[:host] = value
end
def port(value)
@settings[:port] = value
end
def to_h
@settings
end
end
config = ConfigBuilder.build do
database do
host "localhost"
port 5432
end
end
Method chaining with blocks creates fluent interfaces for data processing pipelines. Each method returns an object that accepts further method calls, building complex operations from simple components.
class DataPipeline
def initialize(data)
@data = data
end
def filter(&block)
DataPipeline.new(@data.select(&block))
end
def transform(&block)
DataPipeline.new(@data.map(&block))
end
def reduce(initial = nil, &block)
if initial
@data.reduce(initial, &block)
else
@data.reduce(&block)
end
end
def to_a
@data
end
end
result = DataPipeline.new([1, 2, 3, 4, 5, 6])
.filter { |n| n.even? }
.transform { |n| n * n }
.reduce(:+)
# => 56 (4 + 16 + 36)
Common Pitfalls
Variable scope in blocks frequently causes confusion because blocks capture variables by reference, not value. Changes to captured variables affect all references to those variables.
# Problematic: All blocks reference the same variable
actions = []
(1..3).each do |i|
actions << proc { puts i }
end
actions.each(&:call)
# 3
# 3
# 3
# Correct: Create new scope for each iteration
actions = []
(1..3).each do |i|
actions << proc { |captured_i| puts captured_i }.curry[i]
end
# Alternative: Use block parameters to create new scope
actions = []
(1..3).each do |i|
actions << lambda do |captured_i|
puts captured_i
end.curry[i]
end
Return statements in blocks behave differently based on block type. Proc objects return from the enclosing method, potentially causing unexpected control flow, while lambdas return only from themselves.
def test_proc_return
[1, 2, 3].each do |n|
return "early exit" if n == 2 # Returns from test_proc_return
end
"normal exit"
end
def test_lambda_return
callback = lambda do |n|
return "lambda exit" if n == 2 # Returns only from lambda
end
[1, 2, 3].each(&callback)
"normal exit"
end
puts test_proc_return # "early exit"
puts test_lambda_return # "normal exit"
Symbol-to-proc conversion using &:method_name
works only with methods that accept no arguments. Attempting to use this syntax with methods requiring arguments raises errors or produces unexpected results.
words = ["hello", "world"]
# Correct: No arguments needed
lengths = words.map(&:length)
# => [5, 5]
# Incorrect: gsub requires arguments
# This doesn't work as expected
cleaned = words.map(&:upcase) # Works
# => ["HELLO", "WORLD"]
# But this fails
# cleaned = words.map(&:gsub) # ArgumentError
# Correct approach for methods with arguments
cleaned = words.map { |word| word.gsub(/l/, 'x') }
# => ["hexxo", "worxd"]
Block syntax precedence can create parsing ambiguity when method calls chain with blocks. Brace syntax binds more tightly than do...end
, leading to different method call associations.
# These produce different results
puts [1, 2, 3].map { |n| n * 2 } # Passes block to map
# 2
# 4
# 6
puts [1, 2, 3].map do |n| # Passes block to puts
n * 2
end
# [2, 4, 6]
# Use parentheses to clarify intent
puts([1, 2, 3].map { |n| n * 2 })
# 2
# 4
# 6
puts([1, 2, 3].map do |n|
n * 2
end)
# 2
# 4
# 6
Performance implications arise when blocks create excessive object allocations. Each block parameter creates new local variables, and complex blocks executed frequently can impact memory usage.
# Inefficient: Creates many intermediate strings
large_array = (1..100_000).to_a
result = large_array.map do |n|
"Number: #{n}".upcase.strip
end
# More efficient: Minimize object creation
result = large_array.map do |n|
"NUMBER: #{n}"
end
# Consider lazy evaluation for large datasets
result = large_array.lazy
.map { |n| "NUMBER: #{n}" }
.select { |s| s.include?("5") }
.first(10)
Reference
Core Block Methods
Method | Parameters | Returns | Description |
---|---|---|---|
yield |
*args |
Object |
Executes block with arguments |
yield |
*args, **kwargs |
Object |
Executes block with args and keywords |
block_given? |
None | Boolean |
Checks if block provided to method |
proc { } |
Block | Proc |
Creates Proc object from block |
lambda { } |
Block | Proc |
Creates lambda from block |
Proc.new |
Block | Proc |
Creates Proc object from block |
Proc Object Methods
Method | Parameters | Returns | Description |
---|---|---|---|
#call(*args) |
*args |
Object |
Executes proc with arguments |
#[](*args) |
*args |
Object |
Alias for call |
#===(*args) |
*args |
Object |
Alias for call (case equality) |
#arity |
None | Integer |
Number of required parameters |
#lambda? |
None | Boolean |
Returns true if proc is lambda |
#curry |
Integer (optional) |
Proc |
Returns curried version |
#source_location |
None | Array |
File and line where defined |
#parameters |
None | Array |
Parameter information |
Block Conversion Operators
Operator | Usage | Description |
---|---|---|
&block |
Method parameter | Converts block to Proc object |
&proc |
Method argument | Converts Proc to block |
&:symbol |
Method argument | Converts symbol to proc via to_proc |
Common Iterator Methods with Blocks
Method | Block Parameters | Returns | Description |
---|---|---|---|
each |
element |
Enumerable |
Iterates over elements |
map |
element |
Array |
Transforms elements |
select |
element |
Array |
Filters elements (truthy) |
reject |
element |
Array |
Filters elements (falsy) |
find |
element |
Object/nil |
Returns first match |
reduce |
accumulator, element |
Object |
Reduces to single value |
each_with_index |
element, index |
Enumerable |
Iterates with index |
each_with_object |
element, object |
Object |
Iterates with object |
Block Syntax Rules
Context | Syntax | Usage |
---|---|---|
Single line | { |params| code } |
Simple operations |
Multi-line | do |params|..end |
Complex operations |
No parameters | { code } or do..end |
Parameter-less blocks |
Multiple parameters | { |a, b, c| code } |
Destructuring assignment |
Splat parameters | { |*args| code } |
Variable arguments |
Keyword parameters | { |**kwargs| code } |
Keyword arguments |
Error Types
Exception | Cause | Prevention |
---|---|---|
LocalJumpError |
yield without block | Use block_given? |
ArgumentError |
Wrong argument count (lambdas) | Match parameter count |
NoMethodError |
Symbol to_proc on invalid method | Verify method exists |
Performance Characteristics
Pattern | Memory Impact | CPU Impact | Recommendation |
---|---|---|---|
Simple blocks | Low | Low | Preferred for simple operations |
Proc objects | Medium | Low | Good for reusable code |
Lambda objects | Medium | Low | Use for strict argument checking |
Instance_eval blocks | High | Medium | Limit to DSL contexts |
Nested blocks | Medium | Medium | Avoid deep nesting |