CrackedRuby logo

CrackedRuby

Block Parameters and Return Values

Overview

Ruby treats blocks as first-class objects that methods can receive, execute, and manipulate. Block parameters represent the arguments passed into blocks when methods yield control, while return values encompass the data that flows back from method execution, block evaluation, and yield operations.

Every Ruby method can accept a block, whether explicitly declared or not. When a method calls yield, Ruby executes the associated block and passes arguments as block parameters. The block can access these parameters through the parameter list defined between pipe characters |param1, param2|.

def process_data(array)
  array.each { |item| yield(item, item.length) }
end

process_data(['ruby', 'python']) { |lang, size| puts "#{lang}: #{size}" }
# ruby: 4
# python: 6

Return values flow in multiple directions. Methods return values to their callers, blocks return values to the methods that execute them, and yield expressions return the result of block execution back to the yielding method.

def transform_and_sum(numbers)
  total = 0
  numbers.each do |num|
    result = yield(num)  # yield returns block's return value
    total += result
  end
  total  # method's return value
end

sum = transform_and_sum([1, 2, 3]) { |n| n * 2 }  # => 12

Ruby provides multiple ways to work with blocks beyond basic yielding. The block_given? method checks for block presence, Proc.new captures blocks as objects, and the &block parameter syntax converts blocks to Proc instances. Each approach affects how parameters pass into blocks and how return values flow back.

Basic Usage

Methods yield values to blocks by calling yield with arguments. The block receives these arguments as parameters defined in its parameter list. Block parameters follow standard variable scoping rules within the block's execution context.

def calculate_with_index(items)
  items.each_with_index do |item, index|
    yield(item, index, item * index)
  end
end

calculate_with_index([5, 10, 15]) do |value, pos, product|
  puts "Position #{pos}: #{value} * #{pos} = #{product}"
end
# Position 0: 5 * 0 = 0
# Position 1: 10 * 1 = 10
# Position 2: 15 * 2 = 30

Block parameter counts do not need to match the number of yielded arguments. Ruby assigns nil to missing parameters and ignores extra yielded values. This flexibility allows methods to yield varying argument counts without breaking calling code.

def flexible_yield
  yield(1)
  yield(2, 'extra')
  yield(3, 'more', 'data')
end

flexible_yield { |a, b| puts "a=#{a}, b=#{b}" }
# a=1, b=
# a=2, b=extra
# a=3, b=more

The yield expression returns the value of the block's last evaluated expression. Methods can capture and use this return value for further processing, accumulation, or conditional logic.

def collect_results(items)
  results = []
  items.each do |item|
    processed = yield(item)
    results << processed if processed
  end
  results
end

numbers = collect_results([1, 2, 3, 4, 5]) do |n|
  n.even? ? n * 2 : nil
end
# => [4, 8]

Block parameters can include default values, splat operators, and keyword arguments, similar to method parameters. The double splat ** captures keyword arguments passed through yield.

def advanced_yield
  yield(1, 2, extra: 'data', flag: true)
end

advanced_yield do |a, b=0, *rest, **kwargs|
  puts "a=#{a}, b=#{b}, kwargs=#{kwargs}"
end
# a=1, b=2, kwargs={:extra=>"data", :flag=>true}

Advanced Usage

Ruby converts blocks to Proc objects using the & operator in method parameter lists. This conversion allows methods to store, pass, and manipulate blocks as first-class objects while maintaining access to block parameters and return values.

def store_and_call(data, &block)
  stored_proc = block
  data.map do |item|
    result = stored_proc.call(item)
    [item, result]
  end
end

pairs = store_and_call([1, 2, 3]) { |n| n ** 2 }
# => [[1, 1], [2, 4], [3, 9]]

Methods can accept multiple Proc objects as regular parameters and execute them with different argument sets. Each Proc maintains its own parameter signature and return value handling.

def dual_processing(items, transformer, filter)
  results = []
  items.each do |item|
    transformed = transformer.call(item)
    if filter.call(transformed)
      results << transformed
    end
  end
  results
end

numbers = [1, 2, 3, 4, 5]
square = ->(n) { n * n }
even_filter = ->(n) { n.even? }

filtered = dual_processing(numbers, square, even_filter)
# => [4, 16]

Block parameters can destructure array and hash arguments automatically. When yielding arrays, Ruby unpacks them into separate block parameters. Hash destructuring requires explicit parameter syntax.

def yield_pairs
  [['a', 1], ['b', 2], ['c', 3]].each { |pair| yield(pair) }
end

# Array destructuring
yield_pairs { |(key, value)| puts "#{key} => #{value}" }
# a => 1
# b => 2
# c => 3

def yield_hashes
  [{name: 'Ruby', year: 1995}, {name: 'Python', year: 1991}].each { |h| yield(h) }
end

# Hash parameter destructuring
yield_hashes { |name:, year:| puts "#{name} (#{year})" }
# Ruby (1995)
# Python (1991)

The return statement inside blocks affects the enclosing method, not just the block. This behavior can cause unexpected control flow when blocks contain explicit return statements.

def method_with_block_return
  [1, 2, 3].each do |n|
    return "Found #{n}" if n == 2  # returns from method_with_block_return
  end
  "Not found"
end

result = method_with_block_return  # => "Found 2"

Blocks can capture and modify variables from their enclosing scope through closures. The captured variables remain accessible even after the original scope ends, maintaining their modified values.

def create_accumulator(start = 0)
  total = start
  ->(value) do
    total += value
    total
  end
end

acc = create_accumulator(10)
puts acc.call(5)   # => 15
puts acc.call(3)   # => 18
puts acc.call(2)   # => 20

Common Pitfalls

Block parameter shadowing occurs when block parameters use the same names as variables in the enclosing scope. Ruby creates new local variables within the block, preventing access to the outer variables.

def shadowing_example
  value = "outer"
  [1, 2].each do |value|  # shadows outer 'value'
    puts "Block value: #{value}"
  end
  puts "Outer value: #{value}"  # still "outer"
end

shadowing_example
# Block value: 1
# Block value: 2  
# Outer value: outer

To access outer variables when block parameters have the same names, Ruby provides block-local variable syntax using semicolons in the parameter list.

def block_local_variables
  x, y = 10, 20
  [1, 2].each do |value; x, y|  # x, y are block-local
    x = value * 2
    y = value * 3
    puts "Block: x=#{x}, y=#{y}"
  end
  puts "Outer: x=#{x}, y=#{y}"  # unchanged
end

block_local_variables
# Block: x=2, y=3
# Block: x=4, y=6
# Outer: x=10, y=20

The yield expression raises LocalJumpError when called without an associated block. Always check block_given? before yielding, or provide default behavior when no block exists.

def safe_yield(value)
  if block_given?
    yield(value)
  else
    value.to_s.upcase  # default behavior
  end
end

puts safe_yield("hello")           # => "HELLO"
puts safe_yield("world") { |s| s * 2 }  # => "worldworld"

Block return values can be nil even when blocks appear to return other values. Ruby returns nil from iteration methods regardless of block return values.

def misleading_returns
  result = [1, 2, 3].each { |n| n * 2 }  # each returns original array
  puts result.class  # => Array
  
  result2 = [1, 2, 3].map { |n| n * 2 }  # map returns new array
  puts result2.class  # => Array
  
  result3 = 5.times { |i| i * 3 }  # times returns original number
  puts result3.class  # => Integer
end

Proc and lambda objects handle return statements differently. Procs treat return as returning from the enclosing method, while lambdas treat return as returning from the lambda itself.

def proc_vs_lambda_return
  my_proc = Proc.new { return "proc return" }
  my_lambda = ->(){ return "lambda return" }
  
  # This would cause LocalJumpError if executed
  # my_proc.call
  
  result = my_lambda.call  # => "lambda return"
  puts result
  "method end"
end

Block parameters with default values only receive defaults when fewer arguments are yielded, not when nil is explicitly yielded.

def default_parameter_gotcha
  yield(nil)
  yield()
end

default_parameter_gotcha do |value = "default"|
  puts "Value: #{value.inspect}"
end
# Value: nil        # explicit nil, no default used
# Value: "default"  # no argument, default used

Reference

Core Block Methods

Method Parameters Returns Description
yield(*args) Splat arguments Block return value Executes associated block with arguments
block_given? None Boolean Checks if block was passed to method
Proc.new None Proc Creates Proc from current block
proc { } Block Proc Creates Proc object from block
lambda { } Block Proc Creates lambda (strict Proc) from block
->() { } Block Proc Stabby lambda syntax for creating lambdas

Block Parameter Syntax

Syntax Description Example
|a, b| Basic parameters { |x, y| x + y }
|a, b=default| Default values { |x, y=0| x + y }
|a, *rest| Splat parameters { |first, *others| [first, others] }
|**kwargs| Keyword arguments { |**opts| opts[:key] }
|(x, y)| Array destructuring { |(a, b)| a + b }
|a; local| Block-local variables { |x; temp| temp = x * 2 }

Proc vs Lambda Differences

Aspect Proc Lambda
Argument checking Flexible, assigns nil to missing Strict, raises ArgumentError
return behavior Returns from enclosing method Returns from lambda only
Creation syntax Proc.new, proc lambda, ->
#lambda? method Returns false Returns true

Block Return Value Patterns

Method Type Returns Block Value Usage
each, times Original receiver Ignored
map, collect New array of block returns Collected into array
select, filter Elements where block is truthy Used for filtering
reduce, inject Accumulated value Used in accumulation
find, detect First element where block is truthy Used for matching
all?, any? Boolean Used for boolean evaluation

Common Block Iterator Methods

Method Parameters Block Parameters Returns
Array#each None |element| Original array
Array#each_with_index None |element, index| Original array
Hash#each None |key, value| Original hash
Integer#times None |index| Original integer
Range#each None |value| Original range
Enumerable#map None |element| New array
Enumerable#select None |element| New array/collection

Error Conditions

Error Cause Solution
LocalJumpError yield without block Check block_given?
ArgumentError Lambda argument mismatch Match parameter count
LocalJumpError return in orphaned Proc Use lambda or avoid return
Variable shadowing Block parameter shadows outer variable Use block-local variable syntax