Overview
Block parameters in Ruby provide a way to explicitly capture blocks passed to methods, making them available as first-class objects. When a method parameter is prefixed with &
, Ruby converts any block passed to that method into a Proc
object and assigns it to that parameter.
The &block
syntax serves as a bridge between Ruby's implicit block syntax and explicit Proc
objects. This mechanism allows methods to store, inspect, modify, and call blocks programmatically, enabling powerful metaprogramming patterns and flexible API designs.
def example_method(&block)
puts block.class # Proc
block.call if block
end
example_method { puts "Hello from block" }
# Output: Proc
# Output: Hello from block
Ruby automatically converts blocks to Proc
objects when captured with &block
, and can convert Proc
objects back to blocks when passed with &
prefix. This bidirectional conversion forms the foundation of Ruby's block manipulation capabilities.
def process_data(data, &formatter)
return data unless formatter
data.map { |item| formatter.call(item) }
end
uppercase_proc = proc { |str| str.upcase }
result = process_data(['hello', 'world'], &uppercase_proc)
# => ["HELLO", "WORLD"]
Basic Usage
Capturing and Calling Blocks
The most straightforward use of block parameters involves capturing a block and calling it conditionally:
def greet(name, &block)
puts "Hello, #{name}!"
block.call(name) if block_given?
end
greet("Alice") { |name| puts "Nice to meet you, #{name}!" }
# Output: Hello, Alice!
# Output: Nice to meet you, Alice!
The block_given?
method checks whether a block was passed to the current method. This provides a clean way to make blocks optional without raising errors.
Storing Blocks for Later Execution
Block parameters allow you to store blocks as Proc
objects for deferred execution:
class EventHandler
def initialize
@callbacks = []
end
def on_event(&callback)
@callbacks << callback if callback
end
def trigger_event(data)
@callbacks.each { |callback| callback.call(data) }
end
end
handler = EventHandler.new
handler.on_event { |data| puts "Handler 1: #{data}" }
handler.on_event { |data| puts "Handler 2: #{data.upcase}" }
handler.trigger_event("hello")
# Output: Handler 1: hello
# Output: Handler 2: HELLO
Block Parameter vs Yield
While yield
calls blocks directly, block parameters provide more flexibility:
# Using yield
def with_yield
yield "data" if block_given?
end
# Using block parameter
def with_block_param(&block)
block.call("data") if block
end
Block parameters allow you to pass the block to other methods, store it in variables, or call it multiple times with different arguments.
Converting Between Blocks and Procs
The &
operator converts between blocks and Proc
objects in both directions:
# Proc to block conversion
my_proc = proc { |x| x * 2 }
[1, 2, 3].map(&my_proc) # => [2, 4, 6]
# Block to Proc conversion (via parameter)
def capture_block(&block)
block
end
doubled = capture_block { |x| x * 2 }
[1, 2, 3].map(&doubled) # => [2, 4, 6]
Advanced Usage
Block Composition and Chaining
Block parameters enable sophisticated composition patterns:
class Pipeline
def initialize(&initial_block)
@transformations = initial_block ? [initial_block] : []
end
def then(&block)
@transformations << block if block
self
end
def execute(value)
@transformations.reduce(value) do |current_value, transformation|
transformation.call(current_value)
end
end
end
pipeline = Pipeline.new { |x| x.to_s }
.then { |x| x.upcase }
.then { |x| "Result: #{x}" }
puts pipeline.execute(42) # => "Result: 42"
Conditional Block Execution
Block parameters facilitate complex conditional logic around block execution:
class ConditionalProcessor
def initialize(condition_proc, &action)
@condition = condition_proc
@action = action
end
def process(data)
return data unless @action && @condition.call(data)
case @action.arity
when 0
@action.call
when 1
@action.call(data)
else
@action.call(data, self)
end
end
end
even_condition = proc { |num| num.even? }
processor = ConditionalProcessor.new(even_condition) do |num|
"#{num} is even!"
end
puts processor.process(4) # => "4 is even!"
puts processor.process(3) # => 3
Block Decoration and Middleware
Block parameters support decorator and middleware patterns:
class BlockDecorator
def self.with_timing(&block)
return unless block
proc do |*args|
start_time = Time.now
result = block.call(*args)
end_time = Time.now
puts "Execution time: #{end_time - start_time} seconds"
result
end
end
def self.with_logging(prefix, &block)
return unless block
proc do |*args|
puts "#{prefix}: Starting with args #{args}"
result = block.call(*args)
puts "#{prefix}: Finished with result #{result}"
result
end
end
end
# Compose decorators
decorated_block = BlockDecorator.with_timing do |x, y|
sleep(0.1) # Simulate work
x + y
end
timed_and_logged = BlockDecorator.with_logging("MATH", &decorated_block)
result = timed_and_logged.call(5, 3)
# Output: MATH: Starting with args [5, 3]
# Output: Execution time: 0.1... seconds
# Output: MATH: Finished with result 8
Error Handling & Debugging
Block Parameter Validation
Validating block parameters prevents runtime errors and provides clear feedback:
class BlockValidator
def self.require_block(method_name, &block)
raise ArgumentError, "Block required for #{method_name}" unless block
block
end
def self.validate_arity(expected_arity, &block)
return unless block
actual_arity = block.arity
if actual_arity != expected_arity && actual_arity != -1
raise ArgumentError,
"Block expects #{expected_arity} arguments, got #{actual_arity}"
end
block
end
end
def process_items(items, &processor)
processor = BlockValidator.require_block(__method__, &processor)
processor = BlockValidator.validate_arity(1, &processor)
items.map { |item| processor.call(item) }
end
# This will raise an error
begin
process_items([1, 2, 3])
rescue ArgumentError => e
puts e.message # => "Block required for process_items"
end
Exception Handling in Block Execution
Proper exception handling around block execution prevents cascading failures:
class SafeBlockExecutor
def self.execute_with_fallback(fallback_value, &block)
return fallback_value unless block
begin
block.call
rescue StandardError => e
puts "Block execution failed: #{e.message}"
fallback_value
end
end
def self.execute_with_retry(max_attempts: 3, &block)
return unless block
attempts = 0
begin
attempts += 1
block.call
rescue StandardError => e
if attempts < max_attempts
puts "Attempt #{attempts} failed: #{e.message}. Retrying..."
retry
else
puts "All #{max_attempts} attempts failed: #{e.message}"
raise
end
end
end
end
# Fallback example
result = SafeBlockExecutor.execute_with_fallback("default") do
raise "Something went wrong"
end
puts result # => "default"
# Retry example
SafeBlockExecutor.execute_with_retry(max_attempts: 2) do
puts "Attempting operation..."
rand < 0.5 ? (raise "Random failure") : "Success!"
end
Debugging Block Parameters
Debugging tools for inspecting block parameters:
module BlockDebugger
def debug_block(method_name, &block)
puts "=== Block Debug Info for #{method_name} ==="
if block
puts "Block present: true"
puts "Block class: #{block.class}"
puts "Block arity: #{block.arity}"
puts "Block source location: #{block.source_location}"
else
puts "Block present: false"
end
puts "=== End Debug Info ==="
block
end
end
class TestClass
include BlockDebugger
def test_method(&block)
block = debug_block(__method__, &block)
block.call("test") if block
end
end
TestClass.new.test_method { |arg| puts "Called with: #{arg}" }
Testing Strategies
Testing Methods with Block Parameters
Testing methods that accept blocks requires careful consideration of different scenarios:
require 'minitest/autorun'
class BlockParameterTest < Minitest::Test
def setup
@processor = DataProcessor.new
end
def test_with_block_provided
results = []
@processor.process_items([1, 2, 3]) { |item| results << item * 2 }
assert_equal [2, 4, 6], results
end
def test_without_block_provided
result = @processor.process_items([1, 2, 3])
assert_equal [1, 2, 3], result # Should return original data
end
def test_block_receives_correct_arguments
received_args = []
@processor.process_with_context("test") do |data, context|
received_args = [data, context]
end
assert_equal ["test", @processor], received_args
end
def test_block_called_correct_number_of_times
call_count = 0
@processor.process_items([1, 2, 3]) { |item| call_count += 1 }
assert_equal 3, call_count
end
end
class DataProcessor
def process_items(items, &block)
return items unless block
items.map { |item| block.call(item) }
end
def process_with_context(data, &block)
block.call(data, self) if block
end
end
Mocking and Stubbing Blocks
Effective testing strategies for block-based APIs:
require 'minitest/autorun'
require 'minitest/mock'
class BlockMockingTest < Minitest::Test
def test_block_execution_with_mock
mock_block = Minitest::Mock.new
mock_block.expect :call, "mocked result", ["input"]
processor = BlockProcessor.new
result = processor.execute("input", &mock_block)
assert_equal "mocked result", result
mock_block.verify
end
def test_conditional_block_execution
executed = false
processor = ConditionalBlockProcessor.new
processor.maybe_execute(true) { executed = true }
assert executed, "Block should have been executed when condition is true"
executed = false
processor.maybe_execute(false) { executed = true }
refute executed, "Block should not have been executed when condition is false"
end
def test_block_parameter_forwarding
forwarded_args = nil
target_method = proc { |*args| forwarded_args = args }
forwarder = BlockForwarder.new
forwarder.forward_to_block("arg1", "arg2", &target_method)
assert_equal ["arg1", "arg2"], forwarded_args
end
end
class BlockProcessor
def execute(input, &block)
block ? block.call(input) : input
end
end
class ConditionalBlockProcessor
def maybe_execute(condition, &block)
block.call if condition && block
end
end
class BlockForwarder
def forward_to_block(*args, &block)
block.call(*args) if block
end
end
Common Pitfalls
Block vs Proc Conversion Issues
The &
operator behavior can be confusing when dealing with different callable objects:
# Pitfall: Assuming all callables work the same way
def problematic_method(&block)
# This works with blocks
some_method(&block)
end
# But this doesn't work as expected with Method objects
method_obj = "hello".method(:upcase)
# problematic_method(&method_obj) # This raises TypeError
# Solution: Handle different callable types
def flexible_method(&block)
case block
when Proc
some_method(&block)
when Method
some_method(&block.to_proc)
else
some_method { |*args| block.call(*args) } if block.respond_to?(:call)
end
end
Block Arity Confusion
Block arity (number of parameters) can lead to unexpected behavior:
# Pitfall: Assuming blocks always accept the arguments you pass
def risky_block_call(&block)
# This might fail if block expects different number of arguments
block.call("arg1", "arg2", "arg3") if block
end
# Blocks with different arities behave differently
risky_block_call { |a| puts a } # Works: extra args ignored
risky_block_call { |a, b, c, d| puts a } # Works: missing args become nil
# Solution: Check arity or use splat arguments
def safe_block_call(*args, &block)
return unless block
case block.arity
when -1 # Variable arguments (uses splat)
block.call(*args)
when 0
block.call
else
# Pass only the number of arguments the block expects
block.call(*args.first(block.arity))
end
end
Scope and Variable Capture Issues
Block parameters capture variables from their definition scope, which can cause unexpected behavior:
# Pitfall: Assuming blocks capture current values
def create_callbacks
callbacks = []
# Wrong way - all callbacks will use the final value of i
(1..3).each do |i|
callbacks << proc { puts "Callback #{i}" }
end
callbacks
end
callbacks = create_callbacks
callbacks.each(&:call)
# Output: Callback 3 (three times) - not what we wanted!
# Solution: Explicitly capture the loop variable
def create_callbacks_correctly
callbacks = []
(1..3).each do |i|
# Capture i in a local scope
callbacks << proc { |captured_i = i| puts "Callback #{captured_i}" }
end
callbacks
end
# Better solution: Use block parameters to pass the value
def create_callbacks_with_params
(1..3).map do |i|
proc { |value = i| puts "Callback #{value}" }
end
end
Performance Implications
Block parameters have performance characteristics that differ from direct yield:
require 'benchmark'
def method_with_yield
yield "data" if block_given?
end
def method_with_block_param(&block)
block.call("data") if block
end
# Benchmark shows yield is faster due to avoiding Proc creation
Benchmark.bm do |x|
n = 1_000_000
x.report("yield:") do
n.times { method_with_yield { |data| data.upcase } }
end
x.report("block param:") do
n.times { method_with_block_param { |data| data.upcase } }
end
end
# Use yield for simple cases, block parameters for complex manipulation
Reference
Block Parameter Syntax
Syntax | Description | Example |
---|---|---|
&block |
Captures block as Proc parameter | def method(&block) |
&proc_obj |
Converts Proc to block when calling | array.map(&proc_obj) |
block.call(args) |
Calls captured block with arguments | block.call("hello") |
block_given? |
Checks if block was passed to method | return unless block_given? |
Proc Object Methods
Method | Returns | Description |
---|---|---|
#call(*args) |
Object |
Executes the proc with given arguments |
#arity |
Integer |
Number of parameters (-1 for variable args) |
#parameters |
Array |
Parameter information as [type, name] pairs |
#source_location |
Array |
[filename, line_number] where proc was defined |
#binding |
Binding |
Binding object containing proc's local scope |
#lambda? |
Boolean |
Whether proc was created with lambda syntax |
#curry |
Proc |
Returns curried version of the proc |
#to_proc |
Proc |
Returns self (for consistency with other objects) |
Block Conversion Rules
From Type | To Type | Conversion Method | Notes |
---|---|---|---|
Block | Proc | &block parameter |
Automatic in method signature |
Proc | Block | &proc_obj |
When passing as argument |
Method | Block | &method_obj |
Method#to_proc called automatically |
Symbol | Block | &:symbol |
Symbol#to_proc creates accessor block |
Lambda | Block | &lambda_obj |
Stricter argument checking preserved |
Common Block Patterns
Pattern | Use Case | Example |
---|---|---|
Callback | Event handling | on_click(&callback) |
Iterator | Data processing | items.each(&processor) |
Filter | Conditional selection | items.select(&predicate) |
Transform | Data transformation | items.map(&transformer) |
Validator | Input validation | validate_input(&validator) |
Builder | Object construction | build_object(&configurator) |
Performance Characteristics
Operation | Performance | Memory Usage | Use Case |
---|---|---|---|
yield |
Fastest | Lowest | Simple block calls |
Block parameter | Medium | Medium | Block manipulation needed |
Proc creation | Slower | Higher | Block storage/composition |
Lambda creation | Slower | Higher | Strict argument checking needed |
Error Types
Error | Cause | Solution |
---|---|---|
ArgumentError |
Wrong number of arguments to block | Check block arity before calling |
NoBlockGiven |
Calling yield without block | Use block_given? check |
TypeError |
Invalid conversion to Proc | Ensure object responds to to_proc |
LocalJumpError |
Return/break outside proper context | Use lambda or avoid return in proc |