CrackedRuby logo

CrackedRuby

Code Blocks

A comprehensive guide to Ruby's code block system covering blocks, procs, lambdas, and yield mechanics.

Ruby Language Fundamentals Basic Syntax and Structure
1.1.3

Overview

Ruby's code block system provides a mechanism for passing executable code to methods. The system consists of blocks (anonymous functions attached to method calls), procs (objects that encapsulate blocks), lambdas (strict procs with argument checking), and the yield keyword for executing passed blocks. Ruby implements blocks as closures that capture their lexical environment, maintaining access to variables from their defining scope.

The Proc class serves as the foundation for Ruby's callable objects. Every block can be converted to a Proc object, while lambdas represent a special subset of procs with stricter semantics. The yield keyword provides the primary mechanism for executing blocks within methods, while block_given? checks for block presence.

def greet
  yield "Hello" if block_given?
end

greet { |message| puts "#{message}, World!" }
# => "Hello, World!"

proc_object = Proc.new { |x| x * 2 }
lambda_object = lambda { |x| x * 2 }
result = proc_object.call(5)
# => 10

Ruby's block system enables powerful patterns including iterators, callbacks, and domain-specific languages. Blocks capture their lexical scope, creating closures that maintain references to local variables, instance variables, and constants defined in their creation context.

Basic Usage

Method definitions accept blocks implicitly without declaring them in the parameter list. The yield keyword executes the passed block with optional arguments. Methods should check block_given? before yielding to avoid LocalJumpError exceptions when no block is provided.

def each_with_index(array)
  index = 0
  array.each do |element|
    yield element, index if block_given?
    index += 1
  end
end

numbers = [10, 20, 30]
each_with_index(numbers) do |value, idx|
  puts "Index #{idx}: #{value}"
end
# Index 0: 10
# Index 1: 20
# Index 2: 30

The Proc.new constructor creates proc objects from blocks. Procs maintain their creation context and can be stored, passed around, and called multiple times. The call method executes procs with arguments, while square bracket notation provides syntactic sugar.

multiplier = Proc.new { |x, factor| x * factor }
doubled = multiplier.call(5, 2)
tripled = multiplier[5, 3]
# doubled => 10, tripled => 15

# Procs capture lexical scope
outer_var = "captured"
my_proc = Proc.new { puts outer_var }
my_proc.call
# => "captured"

Lambda creation uses the lambda keyword or stabby lambda syntax ->. Lambdas enforce argument count strictly and use method-like return semantics. They raise ArgumentError for incorrect argument counts, unlike regular procs which ignore extra arguments or assign nil to missing ones.

strict_lambda = lambda { |x, y| x + y }
arrow_lambda = ->(x, y) { x + y }

begin
  strict_lambda.call(1)  # Missing argument
rescue ArgumentError => e
  puts "Lambda requires exact arguments: #{e.message}"
end

flexible_proc = Proc.new { |x, y| (x || 0) + (y || 0) }
result = flexible_proc.call(1)  # y becomes nil
# => 1

Block syntax uses either curly braces for single-line blocks or do...end for multi-line blocks. Ruby convention favors braces for functional-style operations and do...end for procedural operations, though both forms are functionally equivalent.

# Functional style with braces
numbers.select { |n| n.even? }

# Procedural style with do...end
numbers.each do |number|
  processed = number * 2
  puts "Result: #{processed}"
end

Advanced Usage

Methods can capture blocks explicitly using the & parameter prefix, converting blocks to proc objects. This technique enables storing blocks for later execution, passing blocks to other methods, or manipulating blocks as first-class objects.

class EventHandler
  def initialize(&block)
    @callback = block
  end

  def trigger(event_data)
    @callback.call(event_data) if @callback
  end

  def add_filter(&filter_block)
    original_callback = @callback
    @callback = lambda do |data|
      filtered_data = filter_block.call(data)
      original_callback.call(filtered_data) if original_callback
    end
  end
end

handler = EventHandler.new { |data| puts "Processing: #{data}" }
handler.add_filter { |data| data.upcase }
handler.trigger("hello world")
# => "Processing: HELLO WORLD"

The define_method class method creates methods dynamically using blocks, enabling metaprogramming patterns and runtime method generation. These dynamically defined methods capture their defining context, creating powerful abstraction mechanisms.

class AttributeAccessor
  def self.attr_reader_with_prefix(prefix, *names)
    names.each do |name|
      define_method("#{prefix}_#{name}") do
        instance_variable_get("@#{name}")
      end
    end
  end

  def self.attr_writer_with_validation(*names, &validator)
    names.each do |name|
      define_method("#{name}=") do |value|
        if validator.call(value)
          instance_variable_set("@#{name}", value)
        else
          raise ArgumentError, "Invalid value for #{name}: #{value}"
        end
      end
    end
  end
end

class User < AttributeAccessor
  attr_reader_with_prefix :user, :name, :email
  attr_writer_with_validation :age do |value|
    value.is_a?(Integer) && value > 0
  end

  def initialize(name, email, age)
    @name, @email = name, email
    self.age = age
  end
end

Block binding captures the complete lexical environment including local variables, constants, and method definitions. The binding method returns a Binding object representing the current scope, while instance_eval and class_eval execute blocks in different contexts.

class ScopeExplorer
  def initialize(value)
    @instance_var = value
  end

  def explore_scopes(&block)
    local_var = "method local"

    # Execute in current context
    puts "Current context:"
    yield local_var, @instance_var

    # Execute in instance context
    puts "Instance context:"
    instance_eval(&block)

    # Create new binding
    new_binding = binding
    puts "Captured binding has access to local_var: #{eval('local_var', new_binding)}"
  end
end

explorer = ScopeExplorer.new("instance value")
explorer.explore_scopes do |local, instance|
  puts "Block sees: local=#{local}, instance=#{instance || @instance_var}"
end

Proc composition enables functional programming patterns by combining multiple procs into processing pipelines. The >> and << operators provide composition methods for chaining transformations.

class ProcPipeline
  def initialize(*procs)
    @procs = procs
  end

  def call(input)
    @procs.reduce(input) { |result, proc| proc.call(result) }
  end

  def >>(other_proc)
    ProcPipeline.new(*@procs, other_proc)
  end
end

# Create transformation pipeline
upcase_proc = ->(str) { str.upcase }
reverse_proc = ->(str) { str.reverse }
exclaim_proc = ->(str) { "#{str}!" }

pipeline = ProcPipeline.new(upcase_proc, reverse_proc, exclaim_proc)
result = pipeline.call("hello")
# => "OLLEH!"

# Chain with >> operator
extended_pipeline = pipeline >> ->(str) { "[#{str}]" }
final_result = extended_pipeline.call("world")
# => "[DLROW!]"

Common Pitfalls

Return behavior differs significantly between blocks, procs, and lambdas. Blocks and procs return from their defining method, potentially causing unexpected control flow, while lambdas return to their immediate caller like regular methods.

def test_returns
  puts "Method start"

  # Block return - returns from test_returns
  [1, 2, 3].each do |n|
    return "early exit" if n == 2  # Returns from test_returns!
  end

  puts "This won't execute"
  "method end"
end

def safe_test_returns
  puts "Method start"

  # Lambda return - returns only from lambda
  processor = lambda do |n|
    return "lambda exit" if n == 2  # Returns only from lambda
  end

  [1, 2, 3].each(&processor)
  puts "This executes"
  "method end"
end

result1 = test_returns       # => "early exit"
result2 = safe_test_returns  # => "method end"

Variable scope in blocks creates closures that can lead to unexpected behavior when blocks outlive their defining scope or when loop variables are captured incorrectly.

# Problematic: All blocks reference same variable
handlers = []
(1..3).each do |i|
  handlers << Proc.new { puts "Handler #{i}" }
end

handlers.each(&:call)
# Handler 3
# Handler 3
# Handler 3

# Solution: Create new scope for each iteration
corrected_handlers = []
(1..3).each do |i|
  corrected_handlers << (lambda do |captured_i|
    Proc.new { puts "Handler #{captured_i}" }
  end).call(i)
end

corrected_handlers.each(&:call)
# Handler 1
# Handler 2
# Handler 3

Argument handling between procs and lambdas creates subtle bugs when transitioning between the two. Procs silently handle argument mismatches while lambdas raise exceptions, leading to inconsistent behavior.

def demonstrate_argument_handling
  flexible_proc = Proc.new { |a, b, c| [a, b, c] }
  strict_lambda = lambda { |a, b, c| [a, b, c] }

  # Proc handles missing arguments gracefully
  proc_result = flexible_proc.call(1, 2)  # c becomes nil
  puts "Proc result: #{proc_result.inspect}"  # [1, 2, nil]

  # Lambda raises ArgumentError
  begin
    lambda_result = strict_lambda.call(1, 2)
  rescue ArgumentError => e
    puts "Lambda error: #{e.message}"
  end

  # Proc ignores extra arguments
  proc_extra = flexible_proc.call(1, 2, 3, 4, 5)
  puts "Proc with extras: #{proc_extra.inspect}"  # [1, 2, 3]
end

Block_given? checks can introduce race conditions in multi-threaded environments or when blocks are conditionally passed. Always structure code to handle both block and non-block scenarios consistently.

class SafeIterator
  def initialize(collection)
    @collection = collection
  end

  # Problematic: Inconsistent behavior
  def each_bad
    if block_given?
      @collection.each { |item| yield item }
    else
      @collection.each { |item| puts item }  # Different behavior!
    end
  end

  # Better: Consistent interface
  def each_good(&block)
    default_block = block || ->(item) { puts item }
    @collection.each(&default_block)
  end

  # Best: Return enumerator when no block
  def each_best
    return enum_for(:each_best) unless block_given?
    @collection.each { |item| yield item }
  end
end

iterator = SafeIterator.new([1, 2, 3])
iterator.each_best.map(&:to_s)  # => ["1", "2", "3"]

Reference

Block and Proc Creation Methods

Method Parameters Returns Description
Proc.new { } Block Proc Creates proc from block
proc { } Block Proc Alias for Proc.new
lambda { } Block Proc Creates lambda (strict proc)
->() { } Block Proc Stabby lambda syntax
&:symbol Symbol Proc Symbol to proc conversion

Proc Instance Methods

Method Parameters Returns Description
#call(*args) Arguments Object Executes proc with arguments
#[](*args) Arguments Object Alias for call
#===(*args) Arguments Object Alias for call (case statement)
#arity None Integer Number of required arguments
#lambda? None Boolean True if proc is lambda
#source_location None Array File and line number where defined
#binding None Binding Binding object of proc's context
#parameters None Array Parameter information
#curry Integer (optional) Proc Returns curried proc

Yield and Block Methods

Method Parameters Returns Description
yield(*args) Arguments Object Executes attached block
block_given? None Boolean True if block was passed
Kernel#proc Block Proc Global proc creation method

Method Definition with Blocks

Method Parameters Returns Description
define_method(name, &block) Symbol/String, Block Symbol Defines method using block
define_singleton_method(name, &block) Symbol/String, Block Symbol Defines singleton method

Binding and Evaluation Methods

Method Parameters Returns Description
binding None Binding Current binding object
eval(string, binding) String, Binding Object Evaluates string in binding
instance_eval(&block) Block Object Evaluates block in receiver context
class_eval(&block) Block Object Evaluates block in class context
module_eval(&block) Block Object Alias for class_eval

Argument Handling Comparison

Feature Proc Lambda
Argument count checking Flexible Strict
Missing arguments Assigned nil Raises ArgumentError
Extra arguments Ignored Raises ArgumentError
Return behavior Returns from defining method Returns to immediate caller
Created with Proc.new, proc lambda, ->

Block Syntax Patterns

| Pattern | Syntax | Use Case |
| ------------------- | ------------------ | ---------------- | --------- | ----------------------- |
| Single line | { | x | x \* 2 } | Functional operations |
| Multi-line | do | x | ...end | Procedural operations |
| No parameters | { puts "hello" } | Simple callbacks |
| Multiple parameters | { | a, b, c | ... } | Complex transformations |
| Splat parameters | { | \*args | ... } | Variable argument lists |
| Block parameter | { | &block | ... } | Capturing nested blocks |

Common Block Patterns

| Pattern | Example | Description |
| --------- | ------------------ | ----------- | ------------------ | -------------------- |
| Iterator | array.each { | item | ... } | Process each element |
| Filter | array.select { | item | condition } | Filter elements |
| Transform | array.map { | item | transform(item) } | Transform elements |
| Reduce | array.reduce(0) { | sum, n | sum + n } | Aggregate values |
| Callback | on_complete { | result | handle(result) } | Event handling |
| Builder | build_object { | builder | ... } | Object construction |

Error Types

Error Condition Description
LocalJumpError yield without block No block provided to yield
ArgumentError Wrong argument count (lambda) Lambda called with incorrect arguments
TypeError Non-callable object Attempting to call non-proc object

Performance Characteristics

Operation Performance Memory
Block creation Very fast Minimal
Proc creation Fast Small object
Lambda creation Fast Small object
Block call via yield Fastest No allocation
Proc#call Fast Minimal allocation
Method definition Moderate Method object creation