CrackedRuby logo

CrackedRuby

Block Parameters (&block)

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