Overview
Ruby implements closures through blocks, procs, and lambdas, which capture variables from their lexical environment and retain access to those variables even after the original scope exits. Lexical scoping determines which variables a closure can access based on where the closure is defined in the source code, not where it executes.
Ruby provides three primary closure mechanisms: blocks (anonymous closures passed to methods), Proc objects (first-class closure objects), and lambdas (strict Proc variants with different argument and return behavior). Each closure type captures local variables, instance variables, and constants from its defining scope.
outer_var = "captured"
# Block closure
[1, 2, 3].each { |n| puts "#{n}: #{outer_var}" }
# Proc closure
my_proc = Proc.new { |n| puts "#{n}: #{outer_var}" }
my_proc.call(5)
# Lambda closure
my_lambda = lambda { |n| puts "#{n}: #{outer_var}" }
my_lambda.call(6)
Lexical scoping creates a hierarchy where inner scopes access variables from outer scopes, but not vice versa. When Ruby encounters a variable reference, it searches from the current scope outward through enclosing scopes until finding the variable or reaching the top level.
Variable capture occurs at closure creation time. The closure maintains references to the actual variable objects, not copies of their values. Changes to captured variables reflect in both the closure and the original scope.
counter = 0
increment = Proc.new { counter += 1 }
puts counter # => 0
increment.call # => 1
puts counter # => 1
Basic Usage
Blocks represent the most common closure usage in Ruby. Methods receive blocks implicitly and execute them using yield
. Block parameters appear between pipe characters, and the block body accesses both parameters and captured variables.
def repeat_with_index(times)
times.times do |index|
yield(index)
end
end
message = "Hello"
repeat_with_index(3) { |i| puts "#{message} #{i}" }
# Output:
# Hello 0
# Hello 1
# Hello 2
The block_given?
method checks for block presence, enabling methods to behave differently based on block availability. Methods can also accept blocks as explicit parameters using the ampersand operator.
def process_data(data, &block)
if block_given?
data.map(&block)
else
data.dup
end
end
numbers = [1, 2, 3]
doubled = process_data(numbers) { |n| n * 2 } # => [2, 4, 6]
copied = process_data(numbers) # => [1, 2, 3]
Proc objects provide first-class closures that can be stored in variables, passed as arguments, and called later. The Proc.new
constructor accepts a block and creates a callable closure object.
def create_multiplier(factor)
Proc.new { |value| value * factor }
end
double = create_multiplier(2)
triple = create_multiplier(3)
puts double.call(5) # => 10
puts triple.call(4) # => 12
Lambdas behave similarly to procs but enforce argument count strictness and handle return statements differently. The lambda
method creates lambda objects, while the stabby lambda syntax ->
provides a more concise form.
# Traditional lambda syntax
greet_lambda = lambda { |name| "Hello, #{name}!" }
# Stabby lambda syntax
greet_stabby = ->(name) { "Hello, #{name}!" }
# Multi-line stabby lambda
calculate = ->(x, y) do
result = x * y + 10
"Calculation result: #{result}"
end
puts greet_lambda.call("Ruby") # => "Hello, Ruby!"
puts calculate.call(3, 4) # => "Calculation result: 22"
Variable capture creates references to the original variables, not copies. Multiple closures sharing the same captured variables see each other's modifications.
shared_state = []
collector1 = Proc.new { |item| shared_state << "1:#{item}" }
collector2 = Proc.new { |item| shared_state << "2:#{item}" }
collector1.call("apple")
collector2.call("banana")
puts shared_state # => ["1:apple", "2:banana"]
Advanced Usage
Closures enable sophisticated metaprogramming patterns through dynamic method creation, scope manipulation, and behavior injection. Class and module methods can generate closures that capture configuration or state for later execution.
class EventHandler
def initialize
@callbacks = {}
end
def on(event, &callback)
@callbacks[event] ||= []
@callbacks[event] << callback
end
def trigger(event, data = {})
return unless @callbacks[event]
@callbacks[event].each do |callback|
case callback.arity
when 0 then callback.call
when 1 then callback.call(data)
else callback.call(event, data)
end
end
end
end
handler = EventHandler.new
user_name = "Alice"
handler.on(:user_login) { |data| puts "#{user_name} logged in with ID #{data[:id]}" }
handler.on(:user_logout) { puts "#{user_name} logged out" }
handler.trigger(:user_login, { id: 123 }) # => "Alice logged in with ID 123"
user_name = "Bob"
handler.trigger(:user_logout) # => "Bob logged out"
Closure composition creates powerful functional programming patterns where closures combine to form new behaviors. Higher-order functions accept and return closures, enabling flexible behavior customization.
def compose(*functions)
functions.reduce do |f, g|
lambda { |*args| f.call(g.call(*args)) }
end
end
def partial(func, *partial_args)
lambda { |*remaining_args| func.call(*(partial_args + remaining_args)) }
end
add = ->(x, y) { x + y }
multiply = ->(x, y) { x * y }
square = ->(x) { x * x }
# Function composition
add_then_square = compose(square, partial(add, 10))
result = add_then_square.call(5) # => ((5 + 10) ** 2) = 225
Closures can capture and modify instance variables, class variables, and constants, creating stateful behaviors that persist across calls. This enables pattern implementations like memoization and caching.
class MemoizedCalculator
def initialize
@cache = {}
end
def memoize(name, &calculation)
define_singleton_method(name) do |*args|
cache_key = [name, args]
@cache[cache_key] ||= begin
puts "Computing #{name} with #{args.join(', ')}"
calculation.call(*args)
end
end
end
end
calc = MemoizedCalculator.new
calc.memoize(:fibonacci) do |n|
if n <= 1
n
else
calc.fibonacci(n - 1) + calc.fibonacci(n - 2)
end
end
puts calc.fibonacci(10) # Computes and caches intermediate values
puts calc.fibonacci(10) # Returns cached result immediately
Binding objects provide explicit access to closure execution contexts. The binding
method captures the current scope as a Binding object, which can evaluate code within that captured environment.
def create_scope_evaluator
local_var = "captured value"
{
binding: binding,
evaluator: lambda { |code| eval(code, binding) }
}
end
scope = create_scope_evaluator
puts scope[:evaluator].call("local_var") # => "captured value"
puts scope[:evaluator].call("local_var.upcase") # => "CAPTURED VALUE"
# Direct binding usage
result = eval("local_var.length", scope[:binding]) # => 14
Common Pitfalls
Variable capture timing creates subtle bugs when closures are created inside loops. Each iteration captures the same variable reference, causing all closures to share the final loop value rather than their individual iteration values.
# Problematic: all closures capture same variable
closures = []
(1..3).each do |i|
closures << lambda { puts "Value: #{i}" }
end
closures.each(&:call)
# Output: all print "Value: 3"
# Solution: capture value in new scope
closures = []
(1..3).each do |i|
closures << lambda { |captured_i = i| puts "Value: #{captured_i}" }.curry[]
end
# Better solution: use block parameter
closures = (1..3).map { |i| lambda { puts "Value: #{i}" } }
closures.each(&:call)
# Output: prints "Value: 1", "Value: 2", "Value: 3"
Argument handling differs significantly between procs and lambdas. Procs ignore extra arguments and assign nil
to missing parameters, while lambdas enforce exact argument counts and raise ArgumentError
for mismatches.
proc_example = Proc.new { |a, b| puts "a=#{a}, b=#{b}" }
lambda_example = lambda { |a, b| puts "a=#{a}, b=#{b}" }
# Procs are lenient
proc_example.call(1) # => "a=1, b="
proc_example.call(1, 2, 3) # => "a=1, b=2" (ignores 3)
# Lambdas are strict
lambda_example.call(1) # ArgumentError: wrong number of arguments
lambda_example.call(1, 2, 3) # ArgumentError: wrong number of arguments
Return statement behavior creates unexpected control flow issues. In procs, return
exits the enclosing method, while in lambdas, return
exits only the lambda itself.
def test_proc_return
my_proc = Proc.new { return "from proc" }
my_proc.call
"after proc call" # This line never executes
end
def test_lambda_return
my_lambda = lambda { return "from lambda" }
result = my_lambda.call
"after lambda call: #{result}"
end
puts test_proc_return # => "from proc"
puts test_lambda_return # => "after lambda call: from lambda"
Memory leaks occur when closures capture large objects or maintain references to objects that should be garbage collected. Long-lived closures can prevent proper memory cleanup.
# Problematic: closure keeps entire large object in memory
def create_processor(large_dataset)
# Closure captures entire large_dataset
lambda { |key| large_dataset[key] }
end
# Solution: extract only needed data
def create_processor(large_dataset)
needed_data = large_dataset.select { |k, v| important_key?(k) }
lambda { |key| needed_data[key] }
end
# Alternative: use weak references for temporary associations
require 'weakref'
def create_temporary_processor(large_object)
weak_ref = WeakRef.new(large_object)
lambda do |method_name|
obj = weak_ref.__getobj__ rescue nil
obj&.send(method_name)
end
end
Scope pollution happens when closures modify captured variables unexpectedly. Multiple closures sharing the same captured variables can interfere with each other's behavior.
# Problematic: shared mutable state
counter = 0
incrementers = 3.times.map do
lambda { counter += 1 }
end
# All incrementers modify the same counter
incrementers.each(&:call) # counter becomes 3
# Solution: each closure gets its own counter
incrementers = 3.times.map do |start_value|
current = start_value
lambda { current += 1 }
end
Reference
Closure Creation Methods
Method | Syntax | Argument Checking | Return Behavior | Use Case |
---|---|---|---|---|
Block | { } or do..end |
Lenient | Returns to calling method | Iteration, configuration |
Proc.new |
Proc.new { } |
Lenient | Returns from enclosing method | Stored callbacks, reusable logic |
proc |
proc { } |
Lenient | Returns from enclosing method | Legacy compatibility |
lambda |
lambda { } |
Strict | Returns from lambda only | Function objects, strict interfaces |
Stabby lambda | ->() { } |
Strict | Returns from lambda only | Concise function definitions |
Block Testing Methods
Method | Returns | Description |
---|---|---|
block_given? |
Boolean | Checks if current method received a block |
Proc#lambda? |
Boolean | Checks if Proc object is a lambda |
Proc#arity |
Integer | Returns number of parameters (-1 for variable args) |
Proc Object Methods
Method | Parameters | Returns | Description |
---|---|---|---|
#call(*args) |
Variable arguments | Object | Executes the closure with given arguments |
#[](*args) |
Variable arguments | Object | Alias for call method |
#===(*args) |
Variable arguments | Object | Used in case statements, calls the proc |
#yield(*args) |
Variable arguments | Object | Alternative calling syntax |
#curry(arity=nil) |
Optional integer | Proc | Returns curried version of the proc |
#parameters |
None | Array | Returns parameter information as [type, name] pairs |
#source_location |
None | Array or nil | Returns [filename, line_number] where proc was defined |
Binding Methods
Method | Parameters | Returns | Description |
---|---|---|---|
binding |
None | Binding | Captures current scope as Binding object |
Binding#eval(code) |
String | Object | Evaluates code in the binding's context |
Binding#local_variables |
None | Array | Returns local variable names in the binding |
Binding#local_variable_get(name) |
Symbol/String | Object | Gets local variable value |
Binding#local_variable_set(name, value) |
Symbol/String, Object | Object | Sets local variable value |
Argument Behavior Comparison
Closure Type | Too Few Args | Too Many Args | Default Values |
---|---|---|---|
Block | Assigns nil |
Ignores extras | Supported |
Proc | Assigns nil |
Ignores extras | Supported |
Lambda | ArgumentError |
ArgumentError |
Supported |
Variable Capture Rules
Variable Type | Captured | Shared | Scope |
---|---|---|---|
Local variables | Yes | By reference | Lexical |
Instance variables | Yes | By reference | Object scope |
Class variables | Yes | By reference | Class hierarchy |
Constants | Yes | By reference | Lexical with inheritance |
Global variables | Yes | Global | Application-wide |
Performance Characteristics
Operation | Block | Proc | Lambda |
---|---|---|---|
Creation overhead | Lowest | Medium | Medium |
Call overhead | Lowest | Medium | Highest |
Memory usage | Lowest | Medium | Medium |
Argument checking | None | None | Full validation |
Common Return Value Patterns
# Early return with lambda
validator = ->(data) { return false unless data.valid?; true }
# Multiple return points in proc
processor = Proc.new do |input|
return nil if input.empty?
return input.upcase if input.is_a?(String)
input.to_s
end
# Block with implicit return
[1, 2, 3].map { |n| n * 2 } # Returns transformed array