Overview
A closure combines a function with references to variables from the environment where the function was defined. When a function is created inside another scope, it maintains access to variables in that outer scope, creating a persistent binding between the function and its creation environment. This mechanism exists across most modern programming languages and forms a fundamental building block for functional programming patterns, callbacks, event handlers, and data encapsulation techniques.
The term "closure" refers to the function "closing over" its surrounding state. The function carries this state with it wherever it goes, maintaining access to captured variables even when executed far from where it was defined. This behavior differs from normal function scope rules where local variables cease to exist after a function returns.
Ruby implements closures through blocks, Proc objects, and lambdas. Every Ruby block is a closure, capturing variables from the surrounding scope. This makes closures ubiquitous in Ruby code, appearing in iterators, callbacks, and functional programming constructs.
def create_counter
count = 0
Proc.new { count += 1 }
end
counter = create_counter
counter.call # => 1
counter.call # => 2
counter.call # => 3
In this example, the Proc maintains access to the count variable even after create_counter returns. Each call to the closure modifies the same captured variable, demonstrating how closures preserve state across invocations.
Closures enable patterns that would otherwise require explicit state management through objects or global variables. They provide a lighter-weight mechanism for encapsulating behavior with associated data, making them particularly valuable for callbacks, lazy evaluation, and creating function factories.
Key Principles
Closures operate through lexical scoping, where variable bindings are determined by the physical structure of the code rather than the runtime call stack. When a function references a variable, the language looks for that variable first in the function's local scope, then in successively outer scopes based on where the function was defined.
The captured variables in a closure are not copies but references to the actual variables in the enclosing scope. Multiple closures created in the same scope share access to the same variables. Modifications to captured variables affect all closures that reference them, and changes made through one closure are visible to others.
def create_shared_counter
count = 0
increment = Proc.new { count += 1 }
decrement = Proc.new { count -= 1 }
value = Proc.new { count }
[increment, decrement, value]
end
inc, dec, val = create_shared_counter
inc.call # => 1
inc.call # => 2
dec.call # => 1
val.call # => 1
The lifetime of captured variables extends beyond the normal scope boundaries. Variables that would typically be garbage collected after a function returns continue to exist as long as any closure referencing them remains reachable. This creates an implicit memory management relationship between closures and their captured state.
Closure semantics distinguish between capturing a variable's value versus capturing the variable itself. Languages that capture by reference (like Ruby) allow closures to observe and modify the current value of captured variables. This creates a live binding where changes propagate between the closure and its defining scope.
numbers = [1, 2, 3]
multiplier = Proc.new { |x| numbers.map { |n| n * x } }
multiplier.call(2) # => [2, 4, 6]
numbers << 4
multiplier.call(2) # => [2, 4, 6, 8]
The variable capture occurs at function creation time, not at call time. The closure binds to the variables visible when it was defined, establishing a permanent connection to those specific variables in memory. This timing determines which variables are available inside the closure and which version of each variable the closure sees.
Nested closures create layers of captured scope, where inner closures can access variables from all containing scopes. Each closure maintains its own capture list, but these lists overlap when closures are nested. An inner closure captures variables from both its immediate scope and any outer scopes, creating a chain of lexical environments.
Ruby Implementation
Ruby provides three primary mechanisms for creating closures: blocks, Proc objects, and lambdas. All three capture variables from the surrounding scope, but they differ in syntax, argument handling, and control flow behavior.
Blocks represent the most common form of closures in Ruby. Every block is a closure, capturing variables from the context where the block is defined. Blocks cannot be stored in variables directly but can be converted to Proc objects using the & operator or Proc.new.
def demonstrate_block_closure
multiplier = 5
[1, 2, 3].map { |n| n * multiplier }
end
demonstrate_block_closure # => [5, 10, 15]
The Proc class creates explicit closure objects that can be stored, passed around, and called later. Procs capture their surrounding scope when instantiated. Creating a Proc with Proc.new or proc produces an object with lenient argument handling and traditional return semantics.
def create_greeting(prefix)
Proc.new { |name| "#{prefix}, #{name}!" }
end
friendly = create_greeting("Hello")
formal = create_greeting("Greetings")
friendly.call("Alice") # => "Hello, Alice!"
formal.call("Dr. Smith") # => "Greetings, Dr. Smith!"
Lambdas, created with lambda or the stabby lambda syntax ->, are Procs with stricter semantics. Lambdas enforce argument count checking and treat return as returning from the lambda itself rather than the enclosing method. This makes lambdas behave more like regular methods.
strict = lambda { |x, y| x + y }
strict.call(1, 2) # => 3
strict.call(1) # ArgumentError: wrong number of arguments
relaxed = proc { |x, y| x + y }
relaxed.call(1) # => 1 (y becomes nil)
Method objects created with method(:name) also function as closures when the method references instance variables or local variables from an enclosing scope. However, method objects bind to the instance that defined them rather than capturing arbitrary scope.
class Counter
def initialize
@count = 0
end
def increment
@count += 1
end
def to_proc
method(:increment).to_proc
end
end
counter = Counter.new
incrementer = counter.method(:increment)
3.times { incrementer.call }
Ruby's closure implementation captures variables by reference, not by value. This means closures see the current value of captured variables, and modifications to those variables affect the closure's behavior. Multiple closures in the same scope share the same variable references.
def demonstrate_shared_reference
accumulator = 0
adders = 5.times.map do |i|
Proc.new { accumulator += i }
end
adders.each(&:call)
accumulator # => 10 (0+1+2+3+4)
end
Blocks can accept the calling context's self, while Procs and lambdas maintain the self from where they were defined. The instance_eval and instance_exec methods can execute closures in different contexts, though captured variables remain accessible.
class Context
def initialize(value)
@value = value
end
def use_closure(&block)
instance_eval(&block)
end
attr_reader :value
end
outer_var = "captured"
closure = proc { "#{@value} and #{outer_var}" }
Context.new("context").use_closure(&closure)
# => "context and captured"
Ruby's garbage collector tracks closure references to captured variables. Variables remain in memory as long as any closure referencing them exists. This can create memory leaks if closures are stored indefinitely while referencing large objects.
Practical Examples
Closures enable function factories that generate specialized functions based on configuration parameters. Each generated function captures its configuration, creating a family of related functions from a single factory.
def create_validator(min, max)
lambda do |value|
if value < min
"Value #{value} is below minimum #{min}"
elsif value > max
"Value #{value} exceeds maximum #{max}"
else
nil
end
end
end
age_validator = create_validator(0, 120)
temperature_validator = create_validator(-50, 50)
age_validator.call(25) # => nil (valid)
age_validator.call(150) # => "Value 150 exceeds maximum 120"
temperature_validator.call(30) # => nil (valid)
temperature_validator.call(75) # => "Value 75 exceeds maximum 50"
Event handlers and callbacks frequently use closures to maintain state between registration and execution. The closure captures context when the handler is registered, making that context available when the event fires.
class EventEmitter
def initialize
@handlers = []
end
def on_event(&handler)
@handlers << handler
end
def emit(data)
@handlers.each { |h| h.call(data) }
end
end
emitter = EventEmitter.new
received_messages = []
emitter.on_event { |msg| received_messages << msg }
emitter.on_event { |msg| puts "Received: #{msg}" }
emitter.emit("Hello")
emitter.emit("World")
received_messages # => ["Hello", "World"]
Closures implement partial application, where a function with multiple parameters is converted into a function with fewer parameters by fixing some arguments in advance.
def partial(fn, *bound_args)
lambda do |*args|
fn.call(*bound_args, *args)
end
end
multiply = lambda { |x, y, z| x * y * z }
double = partial(multiply, 2)
double.call(3, 4) # => 24 (2 * 3 * 4)
triple_by = partial(multiply, 3, 3)
triple_by.call(5) # => 45 (3 * 3 * 5)
Memoization caches function results using closures to maintain the cache. The cache persists across calls, avoiding redundant computation for previously seen inputs.
def memoize(fn)
cache = {}
lambda do |*args|
cache[args] ||= fn.call(*args)
end
end
fibonacci = lambda do |n|
return n if n <= 1
fibonacci.call(n - 1) + fibonacci.call(n - 2)
end
# Without memoization: fibonacci.call(35) takes seconds
memoized_fibonacci = memoize(fibonacci)
fibonacci = memoized_fibonacci # Replace with memoized version
memoized_fibonacci.call(35) # Fast after first call
Closures create private state that remains inaccessible except through specific methods. This provides encapsulation without defining a full class structure.
def create_secure_counter(initial = 0)
count = initial
allowed_users = []
{
increment: lambda { |user|
if allowed_users.include?(user)
count += 1
else
raise "Unauthorized"
end
},
value: lambda { count },
authorize: lambda { |user| allowed_users << user }
}
end
counter = create_secure_counter(10)
counter[:authorize].call("admin")
counter[:increment].call("admin") # => 11
counter[:value].call # => 11
counter[:increment].call("guest") # => raises "Unauthorized"
Resource management patterns use closures to ensure cleanup happens even if errors occur. The closure captures the resource and guarantees proper disposal.
def with_resource(resource_name)
resource = acquire_resource(resource_name)
begin
yield resource
ensure
release_resource(resource)
end
end
def acquire_resource(name)
puts "Acquiring #{name}"
{ name: name, data: "resource data" }
end
def release_resource(resource)
puts "Releasing #{resource[:name]}"
end
with_resource("database") do |db|
puts "Using #{db[:data]}"
end
# Prints:
# Acquiring database
# Using resource data
# Releasing database
Common Patterns
The command pattern encapsulates operations as closure objects, enabling queuing, logging, and undo functionality. Each command captures the state needed to execute its operation.
class Command
def self.create(&block)
new(block)
end
def initialize(block)
@block = block
@executed = false
end
def execute
return if @executed
@block.call
@executed = true
end
end
commands = []
value = 0
commands << Command.create { value += 10 }
commands << Command.create { value *= 2 }
commands << Command.create { value -= 5 }
commands.each(&:execute)
value # => 15 ((0 + 10) * 2 - 5)
The strategy pattern uses closures to vary algorithm behavior. Different closures implement different strategies, all conforming to the same interface.
class Sorter
def initialize(&strategy)
@strategy = strategy || ->(a, b) { a <=> b }
end
def sort(array)
array.sort(&@strategy)
end
end
by_length = Sorter.new { |a, b| a.length <=> b.length }
by_reverse = Sorter.new { |a, b| b <=> a }
words = ["apple", "pie", "banana", "car"]
by_length.sort(words) # => ["car", "pie", "apple", "banana"]
by_reverse.sort(words) # => ["pie", "car", "banana", "apple"]
The observer pattern registers closures as callbacks that execute when state changes. Multiple observers can monitor the same subject without tight coupling.
class Observable
def initialize
@observers = []
end
def add_observer(&observer)
@observers << observer
end
def notify(event)
@observers.each { |obs| obs.call(event) }
end
end
class Temperature
include Enumerable
def initialize
@observable = Observable.new
@value = 0
end
def value=(new_value)
old_value = @value
@value = new_value
@observable.notify(old: old_value, new: new_value)
end
def on_change(&block)
@observable.add_observer(&block)
end
attr_reader :value
end
temp = Temperature.new
log = []
temp.on_change { |e| log << "Changed: #{e[:old]} -> #{e[:new]}" }
temp.on_change { |e| puts "Alert!" if e[:new] > 100 }
temp.value = 50
temp.value = 105
log # => ["Changed: 0 -> 50", "Changed: 50 -> 105"]
Lazy evaluation defers computation until results are needed. Closures capture the computation and execute it on demand, potentially avoiding unnecessary work.
class LazyValue
def initialize(&block)
@block = block
@computed = false
end
def value
unless @computed
@value = @block.call
@computed = true
@block = nil # Allow garbage collection
end
@value
end
end
expensive = LazyValue.new do
puts "Computing..."
sleep 1
42
end
puts "Created lazy value"
puts "Accessing value..."
puts expensive.value # Prints "Computing..." then 42
puts expensive.value # Prints 42 immediately (cached)
Fluent interfaces chain operations by returning closures that capture accumulated state. Each method call returns a new closure with updated state.
class QueryBuilder
def self.build
new([])
end
def initialize(operations)
@operations = operations
end
def where(field, value)
op = lambda { |record| record[field] == value }
QueryBuilder.new(@operations + [op])
end
def and_where(field, value)
where(field, value)
end
def execute(records)
records.select do |record|
@operations.all? { |op| op.call(record) }
end
end
end
records = [
{ name: "Alice", age: 30, city: "NYC" },
{ name: "Bob", age: 25, city: "LA" },
{ name: "Charlie", age: 30, city: "NYC" }
]
results = QueryBuilder.build
.where(:age, 30)
.and_where(:city, "NYC")
.execute(records)
results # => [{name: "Alice", ...}, {name: "Charlie", ...}]
Design Considerations
Closures provide encapsulation without the overhead of defining classes. For simple stateful behavior, closures offer a lightweight alternative to object-oriented designs. However, closures become harder to understand and test as captured state grows complex. When encapsulated behavior requires more than a few variables or operations, classes provide better structure.
The choice between Procs and lambdas affects error handling and control flow. Lambdas enforce strict argument checking and contain return statements within themselves, making them behave like methods. Procs allow flexible argument counts and let return exit the enclosing method, which can cause unexpected behavior. Lambdas suit scenarios where closure behavior should be isolated and predictable. Procs work better when flexible argument handling is needed or when the closure should affect control flow in the calling method.
def proc_return_example
p = proc { return "from proc" }
p.call
"after proc" # Never reached
end
def lambda_return_example
l = lambda { return "from lambda" }
l.call
"after lambda" # This executes
end
proc_return_example # => "from proc"
lambda_return_example # => "after lambda"
Shared state between closures creates implicit dependencies that make code harder to reason about. When multiple closures modify the same captured variables, understanding program behavior requires tracking all closures that might affect that state. Immutable captured values eliminate these concerns but prevent closures from maintaining state across calls.
Closure lifetime determines memory management implications. Long-lived closures keep captured objects in memory indefinitely, potentially causing memory leaks. Short-lived closures that capture large objects should be allowed to fall out of scope quickly. Breaking circular references between closures and captured objects prevents garbage collection issues.
# Potential memory leak
class Cache
def initialize
@data = []
@cleanup = proc do
# This closure keeps @data alive forever
@data.clear
end
end
end
# Better approach
class Cache
def initialize
@data = []
end
def cleanup
# Method instead of stored closure
@data.clear
end
end
Closures capture variables by reference, creating unexpected behavior when the captured variable changes after closure creation. Loop variables present a common pitfall where multiple closures all reference the same variable, capturing its final value rather than the value from each iteration.
# Problem: all closures share the same variable
procs = []
for i in 1..3
procs << proc { i }
end
procs.map(&:call) # => [3, 3, 3]
# Solution: create new scope per iteration
procs = []
(1..3).each do |i|
procs << proc { i }
end
procs.map(&:call) # => [1, 2, 3]
Testing closures requires controlling captured state, which may not be accessible outside the closure. Injecting dependencies as closure parameters rather than capturing them from scope makes closures easier to test. When closures must capture state, testing often requires invoking the factory function with test values.
Performance considerations differ between closures and regular methods. Closures carry overhead from maintaining captured variable references. Method calls have less overhead but cannot capture state. For performance-critical code called frequently, methods outperform closures. For code called infrequently, closure convenience outweighs performance costs.
Common Pitfalls
Closures capturing mutable objects allow unexpected modifications from outside the closure. The closure has no control over who else holds references to captured objects or how they might change.
def create_logger(messages)
lambda { |msg| messages << msg }
end
log_store = []
logger = create_logger(log_store)
logger.call("First")
log_store << "Outside"
logger.call("Second")
log_store # => ["First", "Outside", "Second"]
The solution requires defensive copying when creating the closure or making captured collections immutable.
def create_safe_logger(messages)
safe_messages = messages.dup.freeze
lambda { |msg| safe_messages + [msg] }
end
Return statements inside Procs return from the enclosing method, not from the Proc itself. This causes unexpected exits from the surrounding context when the Proc is called from a different method.
def create_returner
proc { return "returned" }
end
def caller
p = create_returner
p.call
"after call" # Never reached
end
caller # => "returned"
Using lambdas instead of Procs prevents this issue, as lambda returns are local to the lambda.
Closures in loops often capture loop variables that continue changing after the closure is created. All closures end up referencing the variable's final value.
# Wrong: closures share the loop variable
callbacks = []
i = 0
while i < 3
callbacks << proc { puts i }
i += 1
end
callbacks.each(&:call) # Prints: 3, 3, 3
# Correct: each iteration creates new scope
callbacks = []
3.times do |i|
callbacks << proc { puts i }
end
callbacks.each(&:call) # Prints: 0, 1, 2
Closures capturing self can prevent garbage collection of the entire object graph. When an object creates a closure that references the object itself, a circular reference forms.
class Leaky
def initialize
@data = "large data" * 1000
# Closure captures self, preventing GC
@callback = proc { process(@data) }
end
def process(data)
# Some processing
end
end
Avoiding capturing self requires passing needed data as parameters or using weak references where supported.
Closures that modify captured variables create hidden side effects. Functions that produce side effects are harder to test, compose, and parallelize than pure functions.
# Side effect through closure
total = 0
calculator = proc { |x| total += x }
calculator.call(5)
calculator.call(10)
total # => 15 (modified externally)
# Better: pure function
calculator = proc { |total, x| total + x }
result1 = calculator.call(0, 5) # => 5
result2 = calculator.call(5, 10) # => 15
Nested closures create deep scope chains that make variable access slower and code harder to understand. Each level of nesting adds complexity to tracking which scope provides which variable.
def deeply_nested
a = 1
proc do
b = 2
proc do
c = 3
proc do
# Accessing a, b, c requires traversing scope chain
a + b + c
end
end
end
end
result = deeply_nested.call.call.call # => 6
Flattening nested closures or restructuring to reduce nesting depth improves clarity.
Closures capturing resources like file handles or database connections can delay resource cleanup. The resource remains open until the closure is garbage collected, potentially exhausting resource pools.
def create_file_reader(path)
file = File.open(path)
# File stays open as long as closure exists
lambda { file.read }
end
reader = create_file_reader("data.txt")
# File remains open indefinitely
Explicit resource management through ensure blocks or passing resources as parameters prevents these leaks.
Reference
Closure Types in Ruby
| Type | Creation | Argument Checking | Return Behavior | Use Case |
|---|---|---|---|---|
| Block | { } or do..end | None (assigned to block params) | Returns from block | Iterators, DSLs, temporary behavior |
| Proc | Proc.new, proc | Loose (extra args ignored) | Returns from enclosing method | Flexible callbacks, multiple arity |
| Lambda | lambda, -> | Strict (checks count) | Returns from lambda | Function-like behavior, strict calls |
| Method | method(:name) | Strict | Returns from method | Converting methods to callables |
Variable Capture Characteristics
| Aspect | Behavior | Implication |
|---|---|---|
| Capture timing | At closure creation | Variables bound when closure defined |
| Capture mechanism | By reference | Changes to variables visible in closure |
| Scope chain | Lexical (static) | Based on code structure, not call stack |
| Lifetime | Until closure GC'd | Variables kept alive by closures |
| Sharing | Multiple closures share variables | Modifications affect all closures |
| Self binding | Captured at creation | Self refers to original context |
Control Flow Differences
| Statement | Proc | Lambda | Block |
|---|---|---|---|
| return | Exits enclosing method | Exits lambda only | Exits enclosing method |
| break | Exits enclosing method | Invalid | Exits block normally |
| next | Exits Proc, returns value | Exits lambda, returns value | Continues to next iteration |
| redo | Restarts Proc | Restarts lambda | Restarts block |
Conversion Methods
| Method | Purpose | Example |
|---|---|---|
| to_proc | Converts object to Proc | :upcase.to_proc |
| & operator | Converts block to Proc in params | def method(&block) |
| & operator | Converts Proc to block in calls | array.map(&proc) |
| lambda | Creates lambda from block | lambda { code } |
| proc | Creates Proc from block | proc { code } |
Common Closure Patterns
| Pattern | Structure | Use Case |
|---|---|---|
| Function factory | Outer function returns closure | Creating configured functions |
| Partial application | Closure with pre-filled arguments | Specializing generic functions |
| Memoization | Cache in captured variable | Performance optimization |
| Private state | Closure captures inaccessible variable | Encapsulation without classes |
| Callback | Closure registered for later execution | Event handling, async operations |
| Iterator | Closure maintains iteration state | Custom iteration logic |
| Resource manager | Closure ensures cleanup | try-finally pattern |
| Strategy | Closure as algorithm variant | Runtime behavior selection |
Memory Management Guidelines
| Scenario | Risk | Mitigation |
|---|---|---|
| Long-lived closures | Memory leaks from captured objects | Nil out references when done |
| Large captured objects | Excessive memory use | Capture only needed data |
| Circular references | Objects can't be GC'd | Use weak references or break cycles |
| Captured resources | Resources held open | Explicit cleanup or pass as params |
| Nested closures | Deep reference chains | Flatten structure when possible |
Testing Strategies
| Approach | Technique | Benefit |
|---|---|---|
| Dependency injection | Pass state as parameters | Controllable test inputs |
| Factory testing | Test factory with known inputs | Verify closure captures correct state |
| Execution testing | Call closure and verify output | Confirm behavior correctness |
| State isolation | Create fresh closures per test | Prevent test interference |
| Mutation tracking | Verify captured state changes | Confirm side effects |