CrackedRuby logo

CrackedRuby

Lambda Functions

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