Overview
Ruby implements Proc objects as first-class functions that encapsulate code blocks along with their lexical environment. A Proc captures both the executable code and the variable bindings from the scope where it was created, forming a closure. This mechanism allows code to access variables from its original context even when executed in different scopes.
The Proc class serves as Ruby's primary abstraction for callable objects. Ruby creates Proc instances through several mechanisms: the Proc.new
constructor, the proc
kernel method, lambda expressions, and block-to-proc conversion using the &
operator. Each creation method produces objects with subtly different behaviors regarding argument handling and control flow.
# Different Proc creation methods
p1 = Proc.new { |x| x * 2 }
p2 = proc { |x| x * 2 }
p3 = lambda { |x| x * 2 }
p4 = ->(x) { x * 2 }
# All are Proc objects
[p1, p2, p3, p4].all? { |p| p.is_a?(Proc) }
# => true
Proc objects capture variable references from their creation scope, not variable values. Changes to captured variables remain visible to the Proc throughout its lifetime, and changes made within the Proc affect the original variables. This binding behavior forms the foundation of Ruby's closure implementation.
counter = 0
increment = proc { counter += 1 }
increment.call # => 1
increment.call # => 2
counter # => 2
Ruby distinguishes between "procs" (created with Proc.new
or proc
) and "lambdas" (created with lambda
or ->
) through different argument checking and return behavior. The lambda?
method identifies this distinction, which affects how the Proc handles argument mismatches and return statements.
Basic Usage
Creating Proc objects requires either a block argument or an existing block context. The Proc.new
constructor accepts a block and returns a new Proc instance. Without a block argument, Proc.new
attempts to convert the current method's block into a Proc.
def create_doubler
Proc.new { |x| x * 2 }
end
doubler = create_doubler
doubler.call(5) # => 10
doubler[5] # => 10 (alternate call syntax)
The call
method executes the Proc's code with provided arguments. Ruby also supports several alternative call syntaxes: square brackets, parentheses after the Proc variable, and the ===
operator. These syntaxes prove useful in different contexts, particularly when passing Procs to methods expecting callable objects.
multiplier = proc { |x, y| x * y }
# Various call syntaxes
multiplier.call(3, 4) # => 12
multiplier[3, 4] # => 12
multiplier.(3, 4) # => 12
multiplier === [3, 4] # => 12 (with array unpacking)
Proc objects work seamlessly with iteration methods through the &
operator, which converts the Proc to a block. This conversion allows Proc objects to serve as reusable blocks across multiple method calls.
square = proc { |x| x ** 2 }
numbers = [1, 2, 3, 4, 5]
squared = numbers.map(&square) # => [1, 4, 9, 16, 25]
even_squares = numbers.select(&:even?).map(&square) # => [4, 16]
Method definitions can capture blocks as Proc objects using the &
parameter syntax. This technique enables methods to store, inspect, or selectively call received blocks.
def store_operation(&block)
@operation = block
end
def apply_stored_operation(value)
@operation ? @operation.call(value) : value
end
store_operation { |x| x * 3 }
apply_stored_operation(10) # => 30
Proc objects maintain their binding throughout their lifetime, accessing variables from their creation scope regardless of where they execute. This binding behavior enables powerful patterns like partial application and closure-based state management.
def multiplier_factory(factor)
proc { |number| number * factor }
end
times_three = multiplier_factory(3)
times_five = multiplier_factory(5)
times_three.call(7) # => 21
times_five.call(7) # => 35
Advanced Usage
Proc objects expose their binding through the binding
method, returning a Binding object that represents the complete lexical environment at the Proc's creation point. This binding includes all local variables, instance variables, class variables, and constants accessible from the creation scope.
def create_bound_proc(local_var)
@instance_var = "instance_#{local_var}"
proc do |x|
# Access to local_var and @instance_var through binding
"#{local_var}_#{@instance_var}_#{x}"
end
end
bound_proc = create_bound_proc("test")
bound_proc.call("result") # => "test_instance_test_result"
# Examine the binding
binding = bound_proc.binding
binding.local_variables # => [:local_var]
binding.local_variable_get(:local_var) # => "test"
The curry
method transforms Proc objects into curried versions that support partial application. Curried Procs collect arguments across multiple calls until they receive enough arguments to execute, then return the result. This technique proves valuable for creating specialized functions from general-purpose ones.
# Original proc requiring 3 arguments
calculator = proc { |op, x, y|
case op
when :add then x + y
when :multiply then x * y
when :power then x ** y
end
}
# Curry the proc
curried_calc = calculator.curry
# Create specialized functions through partial application
adder = curried_calc[:add]
multiplier = curried_calc[:multiply]
add_five = adder[5]
multiply_by_three = multiplier[3]
add_five[10] # => 15
multiply_by_three[7] # => 21
Proc composition enables building complex operations from simple ones using the >>
and <<
operators. The >>
operator creates a new Proc that applies the left operand first, then the right operand. The <<
operator reverses this order.
# Simple transformation procs
add_ten = proc { |x| x + 10 }
double = proc { |x| x * 2 }
to_string = proc { |x| x.to_s }
# Compose operations
transform1 = add_ten >> double >> to_string
transform2 = to_string << double << add_ten
transform1[5] # => "30" (5 + 10 = 15, 15 * 2 = 30, "30")
transform2[5] # => "30" (same result, different composition order)
Proc objects support introspection through methods like arity
, parameters
, and source_location
. The arity
method returns the number of required arguments, with negative values indicating variable argument counts. The parameters
method provides detailed parameter information including names and types.
# Different parameter configurations
simple_proc = proc { |x| x }
variable_proc = proc { |x, *rest| [x, rest] }
keyword_proc = proc { |x, y: 10, **opts| [x, y, opts] }
simple_proc.arity # => 1
variable_proc.arity # => -2 (1 required, rest variable)
keyword_proc.arity # => 1
keyword_proc.parameters
# => [[:req, :x], [:key, :y], [:keyrest, :opts]]
Advanced Proc patterns include using them as observers in callback systems and implementing strategy patterns where different Proc objects encapsulate alternative algorithms.
class EventProcessor
def initialize
@handlers = Hash.new { |h, k| h[k] = [] }
end
def on(event_type, &handler)
@handlers[event_type] << handler
end
def emit(event_type, data)
@handlers[event_type].each { |handler| handler.call(data) }
end
end
processor = EventProcessor.new
# Register multiple handlers for the same event
processor.on(:user_signup) { |user| puts "Welcome #{user[:name]}!" }
processor.on(:user_signup) { |user| UserMailer.welcome(user).deliver }
processor.on(:user_signup) { |user| Analytics.track(:signup, user) }
processor.emit(:user_signup, name: "Alice", email: "alice@example.com")
Common Pitfalls
Variable capture behavior frequently confuses developers because Proc objects capture variable references, not values. When creating multiple Proc objects in a loop, they often capture the same variable reference, leading to unexpected behavior where all Proc objects see the final loop value.
# Problem: All procs see the final value of i
procs = []
(1..3).each do |i|
procs << proc { puts i }
end
procs.each { |p| p.call }
# Output: 3, 3, 3 (not 1, 2, 3)
# Solution: Create a new scope for each iteration
procs = []
(1..3).each do |i|
procs << proc { |captured_i = i| puts captured_i }.curry[i]
end
# Alternative solution: Use block parameter
procs = (1..3).map { |i| proc { puts i } }
The distinction between procs and lambdas creates subtle behavioral differences that cause runtime errors when developers expect consistent behavior. Procs handle argument mismatches leniently, while lambdas enforce strict argument checking. Return statements behave differently, with proc returns attempting to return from the enclosing method, while lambda returns only exit the lambda itself.
def proc_vs_lambda_args
regular_proc = proc { |x, y| [x, y] }
lambda_proc = lambda { |x, y| [x, y] }
# Procs accept argument mismatches
regular_proc.call(1) # => [1, nil]
regular_proc.call(1, 2, 3) # => [1, 2] (extra args ignored)
# Lambdas enforce argument counts
lambda_proc.call(1) # ArgumentError: wrong number of arguments
lambda_proc.call(1, 2, 3) # ArgumentError: wrong number of arguments
end
def proc_vs_lambda_return
regular_proc = proc { return "proc return" }
lambda_proc = lambda { return "lambda return" }
result = lambda_proc.call # => "lambda return"
result = regular_proc.call # Returns from proc_vs_lambda_return method!
"unreachable code" # This line never executes
end
Binding-related issues arise when Proc objects capture more context than intended, potentially causing memory leaks or exposing sensitive information. Proc objects retain references to all variables in their creation scope, preventing garbage collection of large objects.
def create_processor(large_dataset)
# Problem: Proc captures reference to entire large_dataset
processor = proc { |item| item.process }
# The large_dataset cannot be garbage collected
# because the proc maintains a reference
processor
end
# Solution: Extract only needed data
def create_processor(large_dataset)
needed_config = large_dataset.config_only
proc { |item| item.process(needed_config) }
# large_dataset can now be garbage collected
end
Scope confusion occurs when developers expect Proc objects to access variables from their execution context rather than their creation context. This misunderstanding leads to NameError exceptions when Proc objects cannot access variables that exist in the calling scope.
def demonstrate_scope_confusion
execution_var = "execution scope"
# Proc created in different scope
external_proc = create_external_proc
# This fails because external_proc cannot see execution_var
external_proc.call # NameError: undefined local variable `execution_var`
end
def create_external_proc
creation_var = "creation scope"
proc { puts creation_var } # Can only see creation_var
end
Thread safety issues emerge when multiple threads access Proc objects that modify shared variables through their bindings. Since Proc objects can modify captured variables, concurrent access requires careful synchronization.
# Problematic: Race condition on shared counter
counter = 0
increment_proc = proc { counter += 1 }
# Multiple threads calling increment_proc create race conditions
threads = 10.times.map do
Thread.new { 100.times { increment_proc.call } }
end
threads.each(&:join)
puts counter # Result is unpredictable, likely less than 1000
# Solution: Use thread-safe synchronization
require 'thread'
counter = 0
mutex = Mutex.new
safe_increment = proc { mutex.synchronize { counter += 1 } }
Reference
Core Methods
Method | Parameters | Returns | Description |
---|---|---|---|
Proc.new(&block) |
block (Block) |
Proc |
Creates new Proc from block |
#call(*args, **kwargs, &block) |
Variable arguments | Object |
Executes Proc with arguments |
#[](*args) |
Variable arguments | Object |
Alias for call |
#===(*args) |
Variable arguments | Object |
Alias for call |
#yield(*args) |
Variable arguments | Object |
Alias for call |
#to_proc |
None | Proc |
Returns self |
#lambda? |
None | Boolean |
Tests if Proc is a lambda |
#arity |
None | Integer |
Returns argument count (-1 for variable) |
#binding |
None | Binding |
Returns creation context binding |
#parameters |
None | Array |
Returns parameter information |
#source_location |
None | Array |
Returns filename and line number |
Composition Methods
Method | Parameters | Returns | Description |
---|---|---|---|
#curry(arity=nil) |
arity (Integer, optional) |
Proc |
Returns curried version |
#>>(other) |
other (Proc) |
Proc |
Composes self then other |
#<<(other) |
other (Proc) |
Proc |
Composes other then self |
Creation Methods
Method | Context | Returns | Description |
---|---|---|---|
proc(&block) |
Kernel method | Proc |
Creates non-lambda Proc |
lambda(&block) |
Kernel method | Proc |
Creates lambda Proc |
->(*args) { } |
Lambda literal | Proc |
Creates lambda Proc |
&:symbol |
Symbol to proc | Proc |
Converts symbol to Proc |
Parameter Types
Type | Symbol | Description | Example |
---|---|---|---|
Required | :req |
Mandatory positional parameter | proc { |x| } |
Optional | :opt |
Optional positional parameter | proc { |x=1| } |
Rest | :rest |
Variable arguments | proc { |*args| } |
Keyword | :key |
Required keyword parameter | proc { |x:| } |
Keyword Optional | :keyopt |
Optional keyword parameter | proc { |x: 1| } |
Keyword Rest | :keyrest |
Variable keyword arguments | proc { |**opts| } |
Block | :block |
Block parameter | proc { |&blk| } |
Arity Values
Arity | Meaning | Example |
---|---|---|
0 |
No arguments | proc { } |
1 |
One required argument | proc { |x| } |
2 |
Two required arguments | proc { |x, y| } |
-1 |
Variable arguments, 0+ | proc { |*args| } |
-2 |
1 required + variable | proc { |x, *rest| } |
-3 |
2 required + variable | proc { |x, y, *rest| } |
Binding Methods
Method | Returns | Description |
---|---|---|
#local_variables |
Array<Symbol> |
Names of local variables |
#local_variable_get(symbol) |
Object |
Gets local variable value |
#local_variable_set(symbol, value) |
Object |
Sets local variable value |
#local_variable_defined?(symbol) |
Boolean |
Tests local variable existence |
#receiver |
Object |
Returns binding's receiver object |
Proc vs Lambda Differences
Behavior | Proc | Lambda |
---|---|---|
Argument checking | Lenient (fills/ignores) | Strict (ArgumentError) |
Return behavior | Returns from enclosing method | Returns from lambda only |
Break/next behavior | LocalJumpError if no loop | Returns from lambda |
Created by | Proc.new , proc |
lambda , -> |
.lambda? returns |
false |
true |