Overview
Anonymous block arguments in Ruby refer to blocks that are passed to methods without being explicitly named as parameters. Ruby provides several mechanisms for working with these blocks: the yield
keyword for calling anonymous blocks, block_given?
for checking block presence, and the &
operator for converting between blocks and Proc objects.
Ruby treats blocks as anonymous arguments by default. When a method receives a block, Ruby doesn't require the method signature to declare a block parameter. The method can call the block using yield
or check for its existence with block_given?
.
def process_items(items)
return enum_for(:process_items, items) unless block_given?
items.each do |item|
yield(item)
end
end
process_items([1, 2, 3]) { |n| puts n * 2 }
# Output: 2, 4, 6
The &
operator converts blocks to Proc objects and vice versa. When used in method parameters, &block
captures the passed block as a Proc. When used in method calls, &proc
converts a Proc back to a block.
def capture_block(&block)
block.call("Hello") if block
end
def forward_block(items, &block)
items.map(&block)
end
capture_block { |msg| puts msg }
# Output: Hello
numbers = [1, 2, 3]
forward_block(numbers) { |n| n ** 2 }
# => [1, 4, 9]
Basic Usage
The most common pattern for anonymous blocks uses yield
to call the block and block_given?
to check if a block was provided. This approach keeps method signatures clean while maintaining flexibility.
def with_timing
return unless block_given?
start_time = Time.now
result = yield
end_time = Time.now
puts "Execution took #{end_time - start_time} seconds"
result
end
with_timing do
sleep(0.1)
42
end
# Output: Execution took 0.1001 seconds
# => 42
Methods can pass arguments to blocks through yield
. The number and type of arguments should be consistent to maintain a clear interface.
def each_with_metadata(collection)
collection.each_with_index do |item, index|
yield(item, index, collection.size) if block_given?
end
end
each_with_metadata(['a', 'b', 'c']) do |item, index, total|
puts "#{index + 1}/#{total}: #{item}"
end
# Output:
# 1/3: a
# 2/3: b
# 3/3: c
The &
operator provides explicit block parameter handling. Use &block
when you need to store, pass along, or manipulate the block as an object.
class EventEmitter
def initialize
@listeners = {}
end
def on(event, &block)
@listeners[event] ||= []
@listeners[event] << block
end
def emit(event, data = nil)
return unless @listeners[event]
@listeners[event].each do |listener|
listener.call(data)
end
end
end
emitter = EventEmitter.new
emitter.on(:data) { |msg| puts "Received: #{msg}" }
emitter.emit(:data, "Hello World")
# Output: Received: Hello World
Advanced Usage
Complex block handling often requires distinguishing between different block signatures or handling multiple execution paths. Ruby's block introspection capabilities support these patterns.
def flexible_iterator(collection, &block)
return enum_for(:flexible_iterator, collection) unless block
case block.arity
when 1
collection.each { |item| yield(item) }
when 2
collection.each_with_index { |item, idx| yield(item, idx) }
else
collection.each_with_index do |item, idx|
yield(item, idx, collection)
end
end
end
flexible_iterator([10, 20, 30]) { |x| puts x }
# Output: 10, 20, 30
flexible_iterator([10, 20, 30]) { |x, i| puts "#{i}: #{x}" }
# Output: 0: 10, 1: 20, 2: 30
Block composition allows chaining and combining blocks for complex data transformations. This pattern is common in functional programming approaches.
class Pipeline
def initialize(&initial_block)
@blocks = initial_block ? [initial_block] : []
end
def then(&block)
@blocks << block
self
end
def call(input)
@blocks.reduce(input) { |acc, block| block.call(acc) }
end
end
result = Pipeline
.new { |x| x.to_s }
.then { |x| x.upcase }
.then { |x| "[#{x}]" }
.call(42)
puts result
# Output: [42]
Conditional block execution patterns handle varying block behaviors based on runtime conditions or input characteristics.
def smart_each(collection, &block)
return enum_for(:smart_each, collection) unless block
if collection.respond_to?(:each_slice) && block.arity > 1
chunk_size = [block.arity, collection.size].min
collection.each_slice(chunk_size) do |slice|
yield(*slice)
end
else
collection.each(&block)
end
end
smart_each([1, 2, 3, 4, 5, 6]) { |a, b| puts "#{a} + #{b} = #{a + b}" }
# Output:
# 1 + 2 = 3
# 3 + 4 = 7
# 5 + 6 = 11
Common Pitfalls
Block parameter binding can create unexpected behavior when blocks capture variables from their defining scope. These closures maintain references to original variables, not their values at block creation time.
def problematic_collectors
collectors = []
(1..3).each do |i|
collectors << lambda { puts "Value: #{i}" }
end
collectors
end
# Incorrect approach - all blocks reference the same variable
collectors = problematic_collectors
collectors.each(&:call)
# Output: Value: 3, Value: 3, Value: 3
def correct_collectors
collectors = []
(1..3).each do |i|
collectors << lambda { |captured| puts "Value: #{captured}" }.curry[i]
end
collectors
end
# Correct approach - capture values at creation time
collectors = correct_collectors
collectors.each(&:call)
# Output: Value: 1, Value: 2, Value: 3
Method signature ambiguity occurs when methods accept both regular parameters and blocks without clear documentation of expected block signatures.
# Problematic - unclear what the block should expect
def process_data(source, options = {}, &processor)
# Block signature is unclear from method definition
data = load_data(source, options)
data.map(&processor)
end
# Better - document expected block signature
def process_data(source, options = {}, &processor)
# Block should accept (item, index, metadata) and return processed item
raise ArgumentError, "Block required" unless block_given?
data = load_data(source, options)
data.map.with_index do |item, index|
metadata = { source: source, position: index }
yield(item, index, metadata)
end
end
Block vs Proc conversion confusion arises when mixing &
operator usage with direct Proc creation. Understanding when Ruby automatically converts between blocks and Procs prevents subtle bugs.
# Confusion: mixing block and Proc patterns
class Confusing
def add_handler(&block)
@handler = block # Stored as Proc
end
def trigger_with_block
yield if block_given? # Expects a block, not a Proc
end
def trigger_with_proc
@handler.call if @handler # Calls stored Proc
end
end
# Clear separation of concerns
class Clear
def add_handler(&block)
@handler = block
end
def trigger
@handler.call if @handler
end
def trigger_with_args(*args)
@handler.call(*args) if @handler
end
end
clear = Clear.new
clear.add_handler { |msg| puts "Handler: #{msg}" }
clear.trigger_with_args("Test")
# Output: Handler: Test
Return value handling in blocks can create unexpected behavior when blocks return values that callers don't expect or when early returns affect control flow.
# Problematic - return in block affects method flow
def dangerous_iterator(items)
items.each do |item|
result = yield(item)
puts "Processed: #{result}"
end
puts "All done"
end
# This breaks the iteration unexpectedly
dangerous_iterator([1, 2, 3]) do |x|
return x * 2 if x == 2 # Returns from dangerous_iterator, not just the block
x * 2
end
# Output: Processed: 2 (then method exits)
# Better - handle block returns gracefully
def safe_iterator(items)
items.each do |item|
begin
result = yield(item)
puts "Processed: #{result}"
rescue LocalJumpError => e
puts "Block returned early with: #{e.exit_value}"
break
end
end
puts "All done"
end
Reference
Core Block Methods
Method | Parameters | Returns | Description |
---|---|---|---|
yield(*args) |
Variable arguments | Block return value | Calls the anonymous block with arguments |
block_given? |
None | Boolean | Returns true if a block was passed to the method |
Proc.new(&block) |
Block parameter | Proc | Creates a Proc from a block |
proc.call(*args) |
Variable arguments | Proc return value | Calls a Proc with arguments |
proc.arity |
None | Integer | Returns number of parameters the Proc expects |
Block Conversion Operators
Operator | Usage | Description |
---|---|---|
&block |
Method parameter | Converts passed block to Proc parameter |
&proc |
Method argument | Converts Proc to block for method call |
method(&:symbol) |
Symbol to Proc | Converts symbol to Proc calling that method |
Block Introspection Methods
Method | Returns | Description |
---|---|---|
proc.arity |
Integer | Number of required parameters (-1 for variable) |
proc.lambda? |
Boolean | Returns true if created with lambda syntax |
proc.source_location |
Array | File and line number where Proc was defined |
proc.parameters |
Array | Parameter names and types |
Common Block Patterns
Pattern | Syntax | Use Case |
---|---|---|
Anonymous yield | yield(args) |
Simple block execution |
Conditional execution | yield(args) if block_given? |
Optional block handling |
Block capture | def method(&block) |
Store block for later use |
Block forwarding | other_method(&block) |
Pass block to another method |
Proc conversion | block.call(args) |
Explicit Proc execution |
Error Types
Error | Cause | Solution |
---|---|---|
LocalJumpError |
Return/break outside proper context | Use next instead of return in blocks |
ArgumentError |
Wrong number of block arguments | Check block arity before calling |
NoMethodError |
Calling methods on nil block | Use block_given? checks |
Performance Considerations
Operation | Performance | Notes |
---|---|---|
yield |
Fastest | Direct block invocation |
block.call |
Slower | Proc object method call |
&block conversion |
Allocation cost | Creates Proc object |
block_given? |
Fast | Simple boolean check |