Overview
Lambda functions in Ruby represent callable objects that capture lexical scope and enforce argument checking. Ruby provides multiple syntaxes for creating lambdas: the lambda
keyword, the ->
stabby lambda syntax, and conversion from blocks using &
operators.
Lambdas differ from regular procs in two critical ways: strict argument checking and return behavior. When a lambda receives the wrong number of arguments, Ruby raises an ArgumentError
. When a lambda executes a return
statement, control returns to the caller rather than the enclosing method.
Ruby implements lambdas as Proc
objects with internal flags that modify their behavior. The Proc#lambda?
method returns true
for lambda objects and false
for regular procs created with Proc.new
or proc
.
# Three ways to create lambdas
lambda_proc = lambda { |x| x * 2 }
stabby_lambda = ->(x) { x * 2 }
converted_lambda = proc { |x| x * 2 }.to_proc
lambda_proc.lambda? # => true
stabby_lambda.lambda? # => true
converted_lambda.lambda? # => false
Lambdas capture variables from their defining scope, creating closures that maintain access to local variables, instance variables, and constants even after the original scope ends.
def create_counter(initial = 0)
count = initial
->{
count += 1
}
end
counter = create_counter(10)
counter.call # => 11
counter.call # => 12
The primary use cases for lambdas include callback systems, functional programming patterns, configuration objects, and delayed execution scenarios where strict argument validation is required.
Basic Usage
Creating lambdas follows consistent patterns across Ruby's different syntaxes. The lambda
keyword provides the most explicit approach, while the ->
syntax offers concurrency for simple cases.
# Basic lambda creation
multiply = lambda { |a, b| a * b }
add = ->(a, b) { a + b }
greet = lambda { |name| "Hello, #{name}!" }
multiply.call(3, 4) # => 12
add.call(5, 7) # => 12
greet.call("Ruby") # => "Hello, Ruby!"
Lambda argument syntax supports default values, splat operators, and keyword arguments. The stabby lambda syntax requires parentheses around parameters, while the lambda
keyword uses block parameter syntax.
# Default arguments
with_default = ->(name = "World") { "Hello, #{name}" }
with_default.call # => "Hello, World"
with_default.call("Ruby") # => "Hello, Ruby"
# Splat arguments
sum_all = ->(*numbers) { numbers.sum }
sum_all.call(1, 2, 3, 4) # => 10
# Keyword arguments
format_name = ->(first:, last:, middle: nil) {
[first, middle, last].compact.join(" ")
}
format_name.call(first: "Jane", last: "Doe") # => "Jane Doe"
Calling lambdas uses the call
method, []
operator, or ===
operator. The call
method provides the clearest intent, while []
offers array-like syntax.
square = ->(x) { x ** 2 }
square.call(5) # => 25
square[5] # => 25
square === 5 # => 25
Converting blocks to lambdas uses the &
operator in method parameters. This pattern appears frequently in Ruby's enumerable methods and callback systems.
numbers = [1, 2, 3, 4, 5]
# Block to lambda conversion
double = ->(x) { x * 2 }
doubled = numbers.map(&double) # => [2, 4, 6, 8, 10]
# Method to lambda conversion
class Calculator
def self.square(x)
x ** 2
end
end
squared = numbers.map(&Calculator.method(:square)) # => [1, 4, 9, 16, 25]
Lambdas integrate with Ruby's enumerable methods through block conversion, enabling functional programming patterns within object-oriented code.
users = [
{ name: "Alice", age: 30 },
{ name: "Bob", age: 25 },
{ name: "Carol", age: 35 }
]
by_age = ->(user) { user[:age] }
over_30 = ->(user) { user[:age] > 30 }
sorted_users = users.sort_by(&by_age)
older_users = users.select(&over_30)
ages = users.map(&by_age)
Advanced Usage
Lambda composition creates complex behavior from simple components. Ruby's lambda objects support chaining through custom methods and functional programming patterns.
# Lambda composition
add_one = ->(x) { x + 1 }
multiply_two = ->(x) { x * 2 }
square = ->(x) { x ** 2 }
# Manual composition
composed = ->(x) { square.call(multiply_two.call(add_one.call(x))) }
composed.call(3) # => 64
# Composition helper
def compose(*lambdas)
->(x) { lambdas.reverse.reduce(x) { |acc, f| f.call(acc) } }
end
pipeline = compose(add_one, multiply_two, square)
pipeline.call(3) # => 64
Currying transforms multi-argument lambdas into sequences of single-argument lambdas. Ruby's Proc#curry
method implements automatic currying with flexible argument application.
# Manual currying
add = ->(a, b) { a + b }
add_curried = ->(a) { ->(b) { a + b } }
add_five = add_curried.call(5)
add_five.call(10) # => 15
# Automatic currying
multiply = ->(a, b, c) { a * b * c }
curried_multiply = multiply.curry
double = curried_multiply.call(2)
double_and_triple = double.call(3)
double_and_triple.call(4) # => 24
# Partial application with curry
multiply.curry.call(2, 3).call(4) # => 24
Lambdas serve as configuration objects in library design, providing flexible APIs that accept both simple values and complex behavior.
class DataProcessor
def initialize(&processor)
@processor = processor || ->(data) { data }
end
def process(data)
@processor.call(data)
end
end
# Simple configuration
upcase_processor = DataProcessor.new { |text| text.upcase }
upcase_processor.process("hello") # => "HELLO"
# Complex configuration
json_processor = DataProcessor.new do |data|
require 'json'
JSON.parse(data).transform_values(&:upcase)
end
Memoization with lambdas creates cached computation patterns. The lambda captures a cache variable from its defining scope, maintaining state across calls.
def memoized_fibonacci
cache = {}
fib = lambda do |n|
return n if n <= 1
cache[n] ||= fib.call(n - 1) + fib.call(n - 2)
end
end
fibonacci = memoized_fibonacci
fibonacci.call(10) # => 55
fibonacci.call(50) # => 12586269025 (computed quickly due to caching)
Lambda factories generate specialized functions based on parameters. This pattern creates domain-specific languages and flexible APIs.
def validator_factory(type, **options)
case type
when :length
min, max = options.values_at(:min, :max)
->(value) { value.length.between?(min || 0, max || Float::INFINITY) }
when :format
pattern = options[:pattern]
->(value) { pattern.match?(value) }
when :custom
options[:validator]
end
end
email_validator = validator_factory(:format, pattern: /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i)
name_validator = validator_factory(:length, min: 2, max: 50)
email_validator.call("user@example.com") # => true
name_validator.call("Jo") # => true
Common Pitfalls
Lambda argument checking differs significantly from proc argument checking. Lambdas raise ArgumentError
for mismatched arguments, while procs adjust arguments silently.
lambda_func = lambda { |a, b| a + b }
proc_func = proc { |a, b| a + b }
# Lambda strict checking
begin
lambda_func.call(1)
rescue ArgumentError => e
puts e.message # => wrong number of arguments (given 1, expected 2)
end
# Proc flexible checking
proc_func.call(1) # => 1 (b becomes nil)
proc_func.call(1, 2, 3) # => 3 (extra arguments ignored)
Return statements behave differently in lambdas versus procs. Lambda returns exit the lambda itself, while proc returns attempt to exit the enclosing method.
def test_lambda_return
lambda_func = lambda { return "from lambda" }
result = lambda_func.call
"after lambda: #{result}"
end
def test_proc_return
proc_func = proc { return "from proc" }
result = proc_func.call
"after proc: #{result}" # This line never executes
end
test_lambda_return # => "after lambda: from lambda"
test_proc_return # => "from proc"
Variable capture timing creates subtle bugs when lambdas reference mutable objects. The lambda captures the variable reference, not the value, leading to unexpected behavior in loops.
# Incorrect: captures final loop variable value
lambdas = []
(1..3).each do |i|
lambdas << lambda { puts i }
end
lambdas.each(&:call) # prints 3, 3, 3
# Correct: captures individual values
lambdas = []
(1..3).each do |i|
lambdas << lambda { |j| puts j }.curry.call(i)
end
# Alternative: parameter binding
lambdas = (1..3).map { |i| lambda { puts i } }
lambdas.each(&:call) # prints 1, 2, 3
Method references converted to lambdas retain their original context, but lambda context cannot be changed after creation. This affects method calls and instance variable access.
class Counter
def initialize
@count = 0
end
def increment
@count += 1
end
def to_lambda
lambda { increment }
end
end
counter1 = Counter.new
counter2 = Counter.new
# Lambda captures original instance
lambda_func = counter1.to_lambda
lambda_func.call # Increments counter1.@count
Block argument destructuring works differently with lambdas than with procs. Lambdas require exact argument matching, while procs perform automatic array destructuring.
pairs = [[1, 2], [3, 4], [5, 6]]
# Proc destructures automatically
pairs.each { |a, b| puts "#{a} + #{b} = #{a + b}" }
# Lambda requires explicit destructuring
sum_lambda = lambda { |(a, b)| a + b }
pairs.map(&sum_lambda) # => [3, 7, 11]
# Without destructuring, lambda receives array
no_destructure = lambda { |pair| pair.sum }
pairs.map(&no_destructure) # => [3, 7, 11]
Lambda equality comparisons check object identity, not behavioral equivalence. Two lambdas with identical code are not equal unless they reference the same object.
# Different lambda objects
lambda1 = lambda { |x| x * 2 }
lambda2 = lambda { |x| x * 2 }
lambda1 == lambda2 # => false
# Same lambda object
shared_lambda = lambda { |x| x * 2 }
reference1 = shared_lambda
reference2 = shared_lambda
reference1 == reference2 # => true
Reference
Creation Syntax
Syntax | Example | Notes |
---|---|---|
lambda { block } |
lambda { |x| x * 2 } |
Explicit lambda creation |
-> { block } |
-> { |x| x * 2 } |
Stabby lambda syntax |
->() { block } |
->() { puts "hello" } |
No parameters |
->(a, b) { block } |
->(a, b) { a + b } |
Multiple parameters |
&:symbol |
numbers.map(&:to_s) |
Symbol to proc conversion |
&method(:name) |
array.map(&method(:puts)) |
Method to proc conversion |
Core Methods
Method | Parameters | Returns | Description |
---|---|---|---|
#call(*args) |
Variable arguments | Object | Executes lambda with arguments |
#[](*args) |
Variable arguments | Object | Alternative call syntax |
#===(*args) |
Variable arguments | Object | Case equality call |
#arity |
None | Integer | Number of required parameters |
#lambda? |
None | Boolean | Returns true for lambda objects |
#curry |
Optional arity (Integer) | Proc | Returns curried version |
#parameters |
None | Array | Parameter information array |
#source_location |
None | Array | File and line number |
#binding |
None | Binding | Binding object of lambda's scope |
Argument Patterns
Pattern | Syntax | Example | Behavior |
---|---|---|---|
Required | |a, b| |
->(a, b) { a + b } |
Must provide exact number |
Optional | |a = 1| |
->(a = 1) { a * 2 } |
Uses default if not provided |
Splat | |*args| |
->(*args) { args.sum } |
Collects remaining arguments |
Keyword | |a:| |
->(a:) { a.upcase } |
Required keyword argument |
Optional keyword | |a: 'default'| |
->(a: 'hi') { a } |
Optional with default |
Double splat | |**opts| |
->(**opts) { opts } |
Collects keyword arguments |
Block | |&block| |
->&block) { block.call } |
Captures passed block |
Comparison: Lambdas vs Procs
Behavior | Lambda | Proc |
---|---|---|
Argument checking | Strict (raises ArgumentError) | Flexible (adjusts silently) |
Return behavior | Returns from lambda | Returns from enclosing method |
Creation | lambda {} , -> {} |
proc {} , Proc.new {} |
.lambda? |
true |
false |
Missing arguments | Raises error | Sets to nil |
Extra arguments | Raises error | Ignores extras |
Error Types
Error | Condition | Example |
---|---|---|
ArgumentError |
Wrong argument count | lambda { |a| }.call() |
LocalJumpError |
Invalid return context | Return in top-level proc |
NoMethodError |
Calling undefined method | lambda.undefined_method |
TypeError |
Invalid curry argument | lambda.curry("invalid") |
Performance Characteristics
Operation | Time Complexity | Memory Usage | Notes |
---|---|---|---|
Creation | O(1) | Low | Lightweight objects |
Call | O(1) + block time | Minimal overhead | Direct method dispatch |
Curry | O(1) | Additional closure | Creates wrapper proc |
Binding capture | O(n) variables | Proportional to scope | Captures local variables |
Common Use Cases
Pattern | Implementation | Best For |
---|---|---|
Callback | on_success: ->(result) { ... } |
Event handling |
Filter | users.select(&active_filter) |
Data processing |
Transform | data.map(&transformer) |
Data transformation |
Validator | validates :email, with: email_lambda |
Input validation |
Factory | create_processor(type, **options) |
Dynamic behavior |
Memoization | Cache in captured variable | Expensive computations |
Configuration | Accept lambda for custom logic | Library design |