CrackedRuby logo

CrackedRuby

Proc Objects

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