Ruby Lambda and Proc: The Complete Guide to Closures
By RubyLearning
Understanding Ruby lambda and Ruby proc is essential for writing expressive, functional-style Ruby code. Blocks, procs, and lambdas are collectively known as Ruby closures — callable objects that capture the surrounding variable scope and can be stored, passed around, and executed later. This guide covers everything from foundation concepts to advanced patterns like currying and the strategy pattern.
Blocks in Ruby: The Foundation
Before diving into procs and lambdas, you need to understand blocks. A block is a chunk of code enclosed between do...end or curly braces { } that you pass to a method. Blocks are not objects themselves, but they form the foundation for procs and lambdas.
# Block with curly braces (single-line convention)
[1, 2, 3].each { |n| puts n * 2 }
# Block with do...end (multi-line convention)
[1, 2, 3].each do |n|
squared = n * n
puts "#{n} squared is #{squared}"
end
Every Ruby method can implicitly accept a block. The method uses the yield keyword to execute the block that was passed to it.
def greet(name)
puts "Hello, #{name}!"
yield if block_given?
puts "Goodbye, #{name}!"
end
greet("Alice") { puts "How are you today?" }
# Output:
# Hello, Alice!
# How are you today?
# Goodbye, Alice!
The block_given? method checks whether a block was passed to the current method, preventing a LocalJumpError when no block is provided.
What Is a Proc in Ruby?
A Ruby proc (short for procedure) is a block that has been turned into an object. Because procs are objects, you can store them in variables, pass them to methods, and call them later. Procs are instances of the Proc class.
# Creating a proc with Proc.new
my_proc = Proc.new { |name| puts "Hello, #{name}!" }
my_proc.call("Alice") # => Hello, Alice!
# Creating a proc with the proc kernel method
my_proc2 = proc { |name| puts "Hi, #{name}!" }
my_proc2.call("Bob") # => Hi, Bob!
Procs are lenient about the number of arguments they receive. If you pass too few arguments, the missing ones are set to nil. If you pass too many, the extras are silently ignored.
flexible_proc = Proc.new { |a, b, c| puts "a=#{a.inspect}, b=#{b.inspect}, c=#{c.inspect}" }
flexible_proc.call(1) # => a=1, b=nil, c=nil
flexible_proc.call(1, 2, 3) # => a=1, b=2, c=3
flexible_proc.call(1, 2, 3, 4) # => a=1, b=2, c=3 (extra argument ignored) What Is a Lambda in Ruby?
A Ruby lambda is a special type of proc that behaves more like a method. Lambdas are also instances of the Proc class, but they have stricter argument checking and different return behavior.
# Creating a lambda with the lambda keyword
my_lambda = lambda { |name| puts "Hello, #{name}!" }
my_lambda.call("Alice") # => Hello, Alice!
# Creating a lambda with the stabby lambda syntax (-> {})
my_lambda2 = ->(name) { puts "Hi, #{name}!" }
my_lambda2.call("Bob") # => Hi, Bob!
# Check if a Proc is a lambda
puts my_lambda.lambda? # => true
puts Proc.new {}.lambda? # => false
The stabby lambda syntax ->(args) { body } was introduced in Ruby 1.9 and is now the preferred way to create lambdas in modern Ruby code. It is concise and visually distinct from regular procs.
All Four Ways to Create Procs and Lambdas
Ruby gives you four different syntax options for creating callable objects. Here they are side by side:
# 1. Proc.new - explicit Proc constructor
p1 = Proc.new { |x| x * 2 }
# 2. proc {} - Kernel method shorthand for Proc.new
p2 = proc { |x| x * 2 }
# 3. lambda {} - creates a lambda (strict proc)
l1 = lambda { |x| x * 2 }
# 4. -> {} - stabby lambda syntax (preferred for lambdas)
l2 = ->(x) { x * 2 }
# All four are callable
puts p1.call(5) # => 10
puts p2.call(5) # => 10
puts l1.call(5) # => 10
puts l2.call(5) # => 10
# Alternative invocation syntax
puts p1.(5) # => 10 (shorthand for .call)
puts p1[5] # => 10 (bracket syntax) Key Differences Between Proc and Lambda
While both procs and lambdas are Proc objects, the Ruby lambda vs proc distinction matters in two critical ways: arity (argument) checking and return behavior. Getting these wrong leads to some of the most confusing bugs in Ruby.
Difference 1: Arity Checking
Lambdas enforce the correct number of arguments, just like methods. Procs are flexible and forgiving.
# Lambda: strict arity
strict = ->(a, b) { a + b }
strict.call(1, 2) # => 3
# strict.call(1) # => ArgumentError: wrong number of arguments (given 1, expected 2)
# strict.call(1,2,3) # => ArgumentError: wrong number of arguments (given 3, expected 2)
# Proc: flexible arity
flexible = Proc.new { |a, b| "#{a.inspect} and #{b.inspect}" }
flexible.call(1, 2) # => "1 and 2"
flexible.call(1) # => "1 and nil" (no error)
flexible.call(1, 2, 3) # => "1 and 2" (no error, extra ignored) Difference 2: Return Behavior
This is the most important difference between Ruby blocks, procs, and lambdas. A return inside a lambda exits only the lambda. A return inside a proc exits the enclosing method.
# Lambda return: exits only the lambda
def lambda_test
l = -> { return "from lambda" }
result = l.call
"Method returns: #{result}" # This line DOES execute
end
puts lambda_test # => "Method returns: from lambda"
# Proc return: exits the enclosing method
def proc_test
p = Proc.new { return "from proc" }
p.call
"This never executes" # This line does NOT execute
end
puts proc_test # => "from proc" Closures and Variable Binding
Both procs and lambdas are Ruby closures — they capture and retain access to the variables from the scope where they were defined. This is called variable binding, and it is one of the most powerful features of functional programming in Ruby.
def counter_factory(start)
count = start
incrementer = -> { count += 1; count }
decrementer = -> { count -= 1; count }
resetter = -> { count = start; count }
[incrementer, decrementer, resetter]
end
inc, dec, reset = counter_factory(10)
puts inc.call # => 11
puts inc.call # => 12
puts inc.call # => 13
puts dec.call # => 12
puts reset.call # => 10
Notice how all three lambdas share the same count variable. When one lambda modifies the value, the others see the change. The closure captures the variable itself, not a copy of its value.
# Closures capture the variable, not the value
x = 10
snapshot = -> { puts "x is #{x}" }
x = 20
snapshot.call # => "x is 20" (not 10!)
# Common gotcha with loops
lambdas = []
5.times do |i|
lambdas << -> { i }
end
puts lambdas.map(&:call).inspect # => [0, 1, 2, 3, 4]
# Each iteration creates a new scope for i, so this works as expected Passing Blocks to Methods: yield and &block
Ruby provides two mechanisms for methods to work with blocks: the implicit yield keyword and the explicit &block parameter.
# Implicit block with yield
def with_logging
puts "[LOG] Starting operation"
result = yield
puts "[LOG] Completed with result: #{result}"
result
end
with_logging { 2 + 2 }
# [LOG] Starting operation
# [LOG] Completed with result: 4
# Explicit block parameter with &block
def with_retry(attempts: 3, &block)
attempts.times do |i|
begin
return block.call
rescue StandardError => e
puts "Attempt #{i + 1} failed: #{e.message}"
end
end
raise "All #{attempts} attempts failed"
end
with_retry(attempts: 3) { some_risky_operation }
The & operator converts between blocks and procs in both directions. When used in a method parameter, it converts the passed block into a Proc object. When used in a method call, it converts a Proc (or any object responding to to_proc) into a block.
# Converting a proc/lambda to a block with &
square = ->(x) { x ** 2 }
puts [1, 2, 3, 4].map(&square).inspect # => [1, 4, 9, 16]
# Symbol#to_proc - the famous &:method_name shorthand
puts ["hello", "world"].map(&:upcase).inspect # => ["HELLO", "WORLD"]
# This is equivalent to:
puts ["hello", "world"].map { |s| s.upcase }.inspect Method Objects and method(:name)
Ruby also lets you wrap existing methods in callable objects using method(:name). This returns a Method object that behaves similarly to a lambda.
def double(x)
x * 2
end
m = method(:double)
puts m.call(5) # => 10
puts m.class # => Method
# Method objects work with & just like procs and lambdas
puts [1, 2, 3].map(&method(:double)).inspect # => [2, 4, 6]
# Instance methods can be captured too
str = "hello"
upper = str.method(:upcase)
puts upper.call # => "HELLO"
# Convert a method to a lambda
double_lambda = method(:double).to_proc
puts double_lambda.lambda? # => true Method objects are especially useful when you want to pass an existing method where a block or callable is expected, without wrapping it in a new lambda.
Practical Use Cases
Callbacks and Event Handlers
Lambdas are ideal for implementing callback patterns. You can register multiple handlers and invoke them when events occur.
class EventEmitter
def initialize
@listeners = Hash.new { |h, k| h[k] = [] }
end
def on(event, &handler)
@listeners[event] << handler
end
def emit(event, *args)
@listeners[event].each { |handler| handler.call(*args) }
end
end
emitter = EventEmitter.new
emitter.on(:user_created) { |user| puts "Welcome email sent to #{user}" }
emitter.on(:user_created) { |user| puts "Analytics tracked for #{user}" }
emitter.on(:error) { |msg| puts "ERROR: #{msg}" }
emitter.emit(:user_created, "alice@example.com")
# Welcome email sent to alice@example.com
# Analytics tracked for alice@example.com Strategy Pattern
The strategy pattern lets you swap algorithms at runtime. In Ruby, lambdas make this pattern lightweight — no need for full class hierarchies. If you are comparing different approaches to problems like this across programming languages, tools like whocodesbest.com can help you evaluate how different AI coding models handle design patterns in Ruby versus other languages.
class Sorter
STRATEGIES = {
alphabetical: ->(items) { items.sort },
reverse: ->(items) { items.sort.reverse },
by_length: ->(items) { items.sort_by(&:length) },
shuffle: ->(items) { items.shuffle }
}.freeze
def initialize(strategy = :alphabetical)
@strategy = STRATEGIES.fetch(strategy)
end
def sort(items)
@strategy.call(items)
end
end
words = ["banana", "apple", "cherry", "date"]
puts Sorter.new(:alphabetical).sort(words).inspect
# => ["apple", "banana", "cherry", "date"]
puts Sorter.new(:by_length).sort(words).inspect
# => ["date", "apple", "banana", "cherry"] Functional Programming Patterns
Lambdas enable functional programming techniques in Ruby such as composing functions, building pipelines, and creating higher-order functions.
# Function composition with >> and << (Ruby 2.6+)
double = ->(x) { x * 2 }
add_one = ->(x) { x + 1 }
double_then_add = double >> add_one # (x * 2) + 1
add_then_double = double << add_one # (x + 1) * 2
puts double_then_add.call(5) # => 11
puts add_then_double.call(5) # => 12
# Building a pipeline
pipeline = [
->(s) { s.strip },
->(s) { s.downcase },
->(s) { s.gsub(/[^a-z0-9\s]/, '') },
->(s) { s.gsub(/\s+/, '-') }
].reduce(:>>)
puts pipeline.call(" Hello, World! ") # => "hello-world" Currying and Partial Application
Currying transforms a multi-argument lambda into a chain of single-argument lambdas. Partial application fixes some arguments to produce a new callable with fewer parameters. Ruby supports both through the curry method.
# Currying: transform multi-arg into chain of single-arg
multiply = ->(a, b) { a * b }
curried = multiply.curry
puts curried.call(3).call(4) # => 12
puts curried.(3).(4) # => 12
# Partial application: fix some arguments
double = curried.(2)
triple = curried.(3)
puts double.(5) # => 10
puts triple.(5) # => 15
# Practical example: configurable formatters
format_number = ->(prefix, precision, number) {
"#{prefix}#{number.round(precision)}"
}
usd = format_number.curry.("$", 2)
eur = format_number.curry.("\u20AC", 2)
pct = format_number.curry.("", 1)
puts usd.(19.995) # => "$20.0"
puts eur.(42.1234) # => "\u20AC42.12"
puts pct.(87.654) # => "87.7"
Currying with arity lets you specify the number of arguments before the curried lambda invokes:
# curry with arity
adder = Proc.new { |*nums| nums.sum }
curried_adder = adder.curry(3)
puts curried_adder.(1).(2).(3) # => 6
# Useful for building validators
validate = ->(rule, message, value) {
rule.call(value) ? nil : message
}
check_presence = validate.curry.(
->(v) { !v.nil? && !v.empty? },
"cannot be blank"
)
check_min_length = validate.curry.(
->(v) { v.length >= 3 },
"must be at least 3 characters"
)
puts check_presence.call("").inspect # => "cannot be blank"
puts check_presence.call("hello").inspect # => nil
puts check_min_length.call("hi").inspect # => "must be at least 3 characters" Real-World Examples
Authorization with Proc-based Policies
class Policy
def initialize(&rule)
@rule = rule
end
def allowed?(user, resource)
@rule.call(user, resource)
end
end
admin_policy = Policy.new { |user, _| user.role == :admin }
owner_policy = Policy.new { |user, resource| resource.owner_id == user.id }
combined = Policy.new { |user, resource|
admin_policy.allowed?(user, resource) || owner_policy.allowed?(user, resource)
}
# Usage in a controller-like context
# if combined.allowed?(current_user, @document)
# # permit action
# end Middleware / Filter Chain
class FilterChain
def initialize
@filters = []
end
def add(&filter)
@filters << filter
self
end
def apply(data)
@filters.reduce(data) { |result, filter| filter.call(result) }
end
end
chain = FilterChain.new
.add { |text| text.strip }
.add { |text| text.downcase }
.add { |text| text.gsub(/\bfoo\b/, "bar") }
.add { |text| text.squeeze(" ") }
puts chain.apply(" The FOO jumped over the Foo. ")
# => "the bar jumped over the bar." Memoization with Lambdas
def memoize(fn)
cache = {}
->(* args) {
cache[args] ||= fn.call(*args)
}
end
slow_square = ->(x) {
sleep(1) # Simulate expensive computation
x ** 2
}
fast_square = memoize(slow_square)
puts fast_square.(4) # Takes 1 second, returns 16
puts fast_square.(4) # Instant, returns 16 from cache
puts fast_square.(5) # Takes 1 second, returns 25 Quick Reference: Proc vs Lambda
| Feature | Proc | Lambda |
|---|---|---|
| Creation | Proc.new { } or proc { } | lambda { } or -> { } |
| Arity checking | Lenient (ignores extra, nils for missing) | Strict (raises ArgumentError) |
| Return behavior | Returns from enclosing method | Returns from the lambda only |
.lambda? | false | true |
| Class | Proc | Proc (same class) |
| Best for | Block-like behavior, DSLs | Method-like behavior, callbacks |
Summary
Ruby blocks, procs, and lambdas give you a flexible toolkit for writing clean, composable code. Blocks are the simplest form — anonymous chunks of code you pass to methods. Procs turn blocks into objects with lenient argument handling and method-exiting returns. Lambdas are strict procs that behave like anonymous methods with proper arity checking and local returns.
As a practical rule: reach for lambdas as your default choice for stored callable objects. Use procs when you specifically need their lenient argument behavior or method-level return semantics. Use blocks when you just need to pass a quick chunk of code to a method. And remember that all three are closures — they capture the variables from their defining scope, which makes them powerful tools for encapsulation and functional programming in Ruby.