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 |