CrackedRuby logo

CrackedRuby

Proc vs Lambda Differences

Overview

Ruby provides two similar but distinct callable objects: Proc and Lambda. Both wrap blocks of code that can be stored in variables and executed later, but they differ in argument handling, return behavior, and block conversion semantics.

Proc objects are created using Proc.new, the proc kernel method, or by converting blocks with & syntax. Lambda objects are created using the lambda keyword, -> syntax, or the Lambda constructor. While both inherit from the Proc class, Ruby tracks their type internally and applies different execution rules.

The primary differences center on argument checking, return statement behavior, and block conversion. Procs exhibit lenient argument handling, allowing extra or missing arguments without errors. Lambdas enforce strict argument arity, raising ArgumentError for mismatched parameters. Return statements in Procs return from the enclosing method, while Lambda returns only exit the Lambda itself.

# Creating Proc objects
proc_obj = Proc.new { |x| x * 2 }
proc_method = proc { |x| x * 2 }

# Creating Lambda objects
lambda_obj = lambda { |x| x * 2 }
lambda_arrow = -> (x) { x * 2 }

# Type checking
proc_obj.lambda?     # => false
lambda_obj.lambda?   # => true

Ruby's block conversion system treats Procs and Lambdas differently when passed to methods expecting blocks. Understanding these distinctions prevents subtle bugs in metaprogramming, functional programming patterns, and callback systems.

Basic Usage

Proc and Lambda objects share the same interface for creation and execution but exhibit different behaviors. The call method executes both types, accepting arguments and returning the block's result.

Argument handling reveals the first major difference. Procs accept any number of arguments, assigning nil to missing parameters and ignoring extras. Lambdas require exact argument counts, raising ArgumentError for mismatches.

# Proc argument flexibility
flexible_proc = Proc.new { |a, b, c| [a, b, c] }
flexible_proc.call(1)        # => [1, nil, nil]
flexible_proc.call(1, 2)     # => [1, 2, nil]
flexible_proc.call(1, 2, 3, 4)  # => [1, 2, 3] (ignores 4)

# Lambda argument strictness  
strict_lambda = lambda { |a, b, c| [a, b, c] }
strict_lambda.call(1)        # => ArgumentError: wrong number of arguments
strict_lambda.call(1, 2, 3)  # => [1, 2, 3]

Return statement behavior creates the second fundamental difference. Procs return from the method that created them, potentially causing unexpected control flow. Lambdas return only from themselves, behaving like regular method calls.

def proc_return_test
  p = Proc.new { return "from proc" }
  p.call
  "after proc"  # Never reached
end

def lambda_return_test  
  l = lambda { return "from lambda" }
  l.call
  "after lambda"  # This executes
end

proc_return_test    # => "from proc"
lambda_return_test  # => "after lambda"

Both types support multiple calling syntaxes. The call method provides explicit invocation, while [] and === offer alternative syntaxes. The () syntax works only with Lambda objects in newer Ruby versions.

multiplier = lambda { |x| x * 3 }

multiplier.call(5)  # => 15
multiplier[5]       # => 15  
multiplier === 5    # => 15
multiplier.(5)      # => 15 (Lambda only)

Block conversion using the & operator treats Procs and Lambdas differently when passing them to methods. This affects how they interact with iterators and other block-accepting methods.

Advanced Usage

Metaprogramming scenarios reveal sophisticated differences between Proc and Lambda behavior. When defining methods dynamically, the choice between Proc and Lambda affects method semantics, particularly around parameter validation and return behavior.

Method definition through define_method accepts both Procs and Lambdas but applies their respective argument and return semantics to the created methods. Lambda-based methods enforce strict arity checking, while Proc-based methods remain flexible.

class DynamicMethods
  # Proc-based method - flexible arguments
  define_method(:flexible_add) do |a, b = 0, c = 0|
    a + b + c  
  end
  
  # Lambda-based method - strict arguments
  define_method(:strict_multiply, lambda { |a, b, c|
    a * b * c
  })
end

obj = DynamicMethods.new
obj.flexible_add(5)        # => 5
obj.flexible_add(5, 3)     # => 8
obj.strict_multiply(2, 3)  # => ArgumentError

Currying and partial application demonstrate advanced functional programming patterns. Both Procs and Lambdas support currying, but their argument handling differences affect the currying behavior.

# Lambda currying with strict arity
curry_lambda = lambda { |a, b, c| a + b + c }.curry
step1 = curry_lambda.call(10)
step2 = step1.call(20)  
result = step2.call(5)  # => 35

# Proc currying behavior
curry_proc = proc { |a, b, c| (a || 0) + (b || 0) + (c || 0) }.curry
partial = curry_proc.call(10, 20)  # Less predictable due to flexible args

Composition patterns using Procs and Lambdas create functional pipelines. The >> and << operators enable chaining, but return behavior differences affect error handling and control flow.

# Lambda composition for reliable pipelines
validate = lambda { |x| x.is_a?(Numeric) ? x : raise(ArgumentError) }
double = lambda { |x| x * 2 }  
format = lambda { |x| "Result: #{x}" }

pipeline = validate >> double >> format
pipeline.call(5)  # => "Result: 10"

# Complex composition with error handling
safe_divide = lambda do |x, y|
  return Float::INFINITY if y.zero?
  x / y
end

math_pipeline = lambda { |a, b| [a.to_f, b.to_f] } >>
                lambda { |nums| safe_divide.call(*nums) } >>
                lambda { |result| result.round(2) }

Closure behavior interacts with variable scoping differently for Procs and Lambdas, particularly in iterative contexts and when closures outlive their creation scope.

def create_closures
  multipliers = []
  
  5.times do |i|
    # Proc captures variable reference
    multipliers << Proc.new { |x| x * i }
  end
  
  lambda_multipliers = []
  5.times do |i|  
    # Lambda captures variable reference (same behavior)
    lambda_multipliers << lambda { |x| x * i }
  end
  
  [multipliers, lambda_multipliers]
end

procs, lambdas = create_closures
procs[0].call(10)   # => 40 (i is 4 for all)  
lambdas[0].call(10) # => 40 (same closure behavior)

Common Pitfalls

Argument handling differences create the most frequent source of confusion. Developers often assume consistent behavior between Procs and Lambdas, leading to runtime errors when switching between them. The flexibility of Procs can mask missing arguments, while Lambda strictness can break previously working code.

# Dangerous Proc argument flexibility
def process_data(processor)
  data = [1, 2, 3, 4, 5]
  data.map { |item| processor.call(item, data.length) }
end

# This works but may hide bugs
lenient_processor = Proc.new { |value, context|
  value * (context || 1)  # Context might be nil
}

# This catches argument errors immediately  
strict_processor = lambda { |value, context|
  value * context
}

process_data(lenient_processor)  # => [1, 2, 3, 4, 5] (context ignored)
# process_data(strict_processor)   # => ArgumentError if args mismatch

Return behavior creates subtle bugs in callback systems and iterators. Procs returning from enclosing methods can terminate iteration prematurely or cause unexpected exits from calling methods.

def dangerous_iterator
  numbers = [1, 2, 3, 4, 5]
  
  # Dangerous: Proc return exits the method
  callback = Proc.new do |n|
    return "found: #{n}" if n == 3  # Exits dangerous_iterator!
  end
  
  results = numbers.map(&callback)
  "completed iteration"  # Never reached
end

def safe_iterator  
  numbers = [1, 2, 3, 4, 5]
  
  # Safe: Lambda return only exits the lambda
  callback = lambda do |n|
    return "found: #{n}" if n == 3  # Only exits lambda
  end
  
  results = numbers.map(&callback)  
  "completed iteration"  # This executes
end

dangerous_iterator()  # => "found: 3"
safe_iterator()       # => "completed iteration"

Block conversion confusion occurs when mixing & syntax with Proc and Lambda objects. The conversion behaves differently depending on the object type and calling context.

class BlockConverter
  def self.test_conversion(&block)
    puts "Block type: #{block.class}"
    puts "Lambda?: #{block.lambda?}"
    block
  end
end

# Direct block conversion
converted = BlockConverter.test_conversion { |x| x * 2 }
# => Block type: Proc, Lambda?: false

# Proc object conversion  
proc_obj = Proc.new { |x| x * 2 }
converted_proc = BlockConverter.test_conversion(&proc_obj)  
# => Block type: Proc, Lambda?: false

# Lambda object conversion maintains type
lambda_obj = lambda { |x| x * 2 }
converted_lambda = BlockConverter.test_conversion(&lambda_obj)
# => Block type: Proc, Lambda?: true

Performance implications often go unnoticed. Lambda objects generally execute faster due to optimized argument handling, while Proc objects incur overhead for argument flexibility checking.

# Performance-sensitive callback system
def performance_test(callback, iterations = 100_000)
  start_time = Time.now
  
  iterations.times do |i|
    callback.call(i, i * 2)
  end
  
  Time.now - start_time
end

proc_callback = Proc.new { |a, b| a + b }
lambda_callback = lambda { |a, b| a + b }

# Lambda typically executes faster
proc_time = performance_test(proc_callback)
lambda_time = performance_test(lambda_callback)

Debugging challenges arise from the different stack trace behavior. Procs can create confusing stack traces when returns cross method boundaries, making error tracking difficult in complex applications.

def create_problematic_proc
  Proc.new do |condition|
    return "early exit" if condition
    "normal execution"
  end
end

def caller_method
  handler = create_problematic_proc
  result = handler.call(true)  # Return exits create_problematic_proc
  "this never executes"
end

# Stack traces can be misleading
begin
  caller_method()
rescue => e
  puts e.backtrace  # May not point to actual return location
end

Reference

Object Creation Methods

Method Syntax Creates Lambda? Notes
Proc.new Proc.new { block } Proc false Standard constructor
proc proc { block } Proc false Kernel method
lambda lambda { block } Lambda true Keyword constructor
-> -> { block } Lambda true Arrow syntax
& conversion method(&obj) Varies Preserves Converts to block

Argument Handling Comparison

Aspect Proc Behavior Lambda Behavior
Missing arguments Assigns nil Raises ArgumentError
Extra arguments Ignores surplus Raises ArgumentError
Arity checking Flexible Strict
Splat handling Permissive Standard
Keyword arguments Flexible Enforced

Execution Methods

Method Syntax Support Returns Notes
call obj.call(args) Both Block result Standard invocation
[] obj[args] Both Block result Array-style call
=== obj === arg Both Block result Case equality
() obj.(args) Lambda only Block result Direct call syntax
yield Internal use Both Block result Block context only

Return Statement Behavior

Context Proc Return Lambda Return Notes
Method body Exits enclosing method Exits lambda only Critical difference
Top level Exits script Exits lambda only Script context
Block context Exits block Exits lambda only Iterator usage
Rescue clause Exits enclosing method Exits lambda only Exception handling

Type Checking Methods

Method Returns Description
#lambda? Boolean Returns true for Lambda objects
#curry Proc/Lambda Returns curried version
#arity Integer Number of required arguments
#parameters Array Parameter information
#source_location Array File and line number
#binding Binding Execution context

Currying and Composition

Operation Syntax Result Type Notes
Curry obj.curry Same as original Enables partial application
Compose right f >> g Proc Pipes f output to g
Compose left f << g Proc Pipes g output to f
Partial application curried.call(args) Curried Proc/Lambda Returns new callable

Common Error Types

Error Cause Proc Lambda Solution
ArgumentError Wrong argument count No Yes Match parameter count
LocalJumpError Invalid return Method context Rare Use Lambda for returns
NoMethodError Missing method Both Both Check object type
TypeError Wrong object type Both Both Validate callable object

Performance Characteristics

Aspect Proc Lambda Notes
Call overhead Higher Lower Argument checking cost
Memory usage Similar Similar Negligible difference
Creation cost Lower Higher Type checking overhead
Optimization Limited Better JIT compiler friendly