Ruby Lambdas — Arrow Syntax Lambda Arity and Closure Behavior Explained
In this tutorial, you will learn about Ruby Lambdas. We cover key concepts, practical examples, and best practices to help you master this topic.
Ruby lambdas created with -> or lambda keyword are function-like closures that enforce argument arity (unlike Procs) and use return to exit only the lambda, not the enclosing method.
What You'll Learn
- Creating lambdas with -> and lambda keywords
- How lambda arity differs from Proc arity
- Return behavior differences between lambdas and procs
- Practical use cases for lambdas
Why It Matters
Lambdas provide stricter, more predictable behavior than procs. Durga Antivirus Pro uses lambdas for validation rules, transformation pipelines, and configuration defaults. Understanding when to use lambdas vs procs prevents subtle bugs in your Ruby code.
Real-World Use
Rails uses lambdas for scopes, validations options, and conditional callbacks: scope :active, -> { where(active: true) }, validates :email, if: -> { email.present? }. Lambdas are also preferred for callable objects that need argument validation.
flowchart LR
A["Lambdas"] --> B["Creation"]
B --> C["Arity Rules"]
C --> D["Return Behavior"]
D --> E["Closures"]
E --> F["Mixins"]
A:::current --> B
style A fill:#2563eb,stroke:#2563eb,color:#fff
style B fill:#dbeafe,stroke:#2563eb,color:#1e40af
style C fill:#dbeafe,stroke:#2563eb,color:#1e40af
style D fill:#dbeafe,stroke:#2563eb,color:#1e40af
style E fill:#dbeafe,stroke:#2563eb,color:#1e40af
style F fill:#f1f5f9,stroke:#94a3b8,color:#64748b
Creating Lambdas
# Arrow syntax (Ruby 1.9+)
greet = ->(name) { "Hello, #{name}!" }
# lambda keyword
greet = lambda { |name| "Hello, #{name}!" }
# Multi-line
greet = lambda do |name|
"Hello, #{name}!"
end
Calling Lambdas
greet = ->(name) { "Hello, #{name}!" }
puts greet.call("Alice") # Hello, Alice!
puts greet["Bob"] # Hello, Bob!
puts greet.("Charlie") # Hello, Charlie!
puts greet === "Dave" # Hello, Dave!
Lambda Arity (Argument Strictness)
Lambdas enforce argument count — procs don't:
# Lambda — strict about arguments
strict = ->(a, b) { a + b }
puts strict.call(1, 2) # 3
# strict.call(1) # ArgumentError (wrong number)
# strict.call(1, 2, 3) # ArgumentError (wrong number)
# Proc — flexible about arguments
flexible = Proc.new { |a, b| a + b }
puts flexible.call(1, 2) # 3
puts flexible.call(1) # 1 (b is nil → nil.to_i = 0)
puts flexible.call(1, 2, 3) # 3 (extra args ignored)
Arity Method
strict = ->(a, b) { a + b }
flexible = Proc.new { |a, b| a + b }
puts strict.arity # 2
puts flexible.arity # 2
# Optional arguments
optional = ->(a, b = 0) { a + b }
puts optional.arity # -2 (at least 1 required)
Return Behavior
This is the most important difference:
def test_lambda
my_lambda = -> { return "from lambda" }
result = my_lambda.call
"from method (#{result})"
end
def test_proc
my_proc = Proc.new { return "from proc" }
result = my_proc.call
"from method (#{result})" # Never reached
end
puts test_lambda # from method (from lambda)
puts test_proc # from proc
Lambda return exits the lambda and returns to the method. Proc return exits the entire method.
Practical Implications
# Lambda — safe to use in method
def transform_list(items)
transformer = ->(item) { return item.upcase if item.is_a?(String) }
items.map(&transformer)
end
puts transform_list(["hello", 42, "world"]).inspect
# ["HELLO", 42, "WORLD"]
# Proc — dangerous in method
def transform_list_bad(items)
transformer = Proc.new { |item| return item.upcase if item.is_a?(String) }
items.map(&transformer)
end
puts transform_list_bad(["hello", 42, "world"]) # "HELLO" — exits early!
Lambdas as First-Class Objects
# Store in data structures
operations = {
add: ->(a, b) { a + b },
subtract: ->(a, b) { a - b },
multiply: ->(a, b) { a * b },
divide: ->(a, b) { a / b }
}
puts operations[:add].call(10, 5) # 15
puts operations[:divide].call(10, 5) # 2
# Pass to methods
def apply_operation(a, b, operation)
operation.call(a, b)
end
puts apply_operation(10, 5, ->(x, y) { x ** y }) # 100000
Lambdas and Closures
Like blocks and procs, lambdas capture their surrounding scope:
def create_multiplier(factor)
->(value) { value * factor }
end
double = create_multiplier(2)
triple = create_multiplier(3)
puts double.call(5) # 10
puts triple.call(5) # 15
Lambdas for Configuration
class ConfigValidator
attr_reader :validators
def initialize
@validators = {}
end
def add_validation(field, &validator)
@validators[field] = validator
end
def validate(data)
errors = []
@validators.each do |field, validator|
unless validator.call(data[field])
errors << "#{field} is invalid"
end
end
errors
end
end
config = ConfigValidator.new
config.add_validation(:email) { |v| v =~ /\A[\w.]+@\w+\.\w+\z/ }
config.add_validation(:age) { |v| v.is_a?(Integer) && v > 0 }
puts config.validate({ email: "test@example.com", age: 25 }).inspect
# []
puts config.validate({ email: "invalid", age: -1 }).inspect
# ["email is invalid", "age is invalid"]
Lambda vs Proc vs Block Decision Guide
# Use a block when:
[1, 2, 3].map { |n| n * 2 }
# Use a lambda when:
# - You need strict argument checking
# - You need predictable return behavior
# - You need to store the callable for later
validator = ->(email) { email.include?("@") }
# Use a proc when:
# - You need flexible argument handling
# - You need to return from the enclosing method
# - You're working with Symbol#to_proc patterns
Conversion Between Types
# Lambda to Proc? They're already the same class
puts ->{}.class # Proc
puts Proc.new{}.class # Proc
# Check if a Proc is a lambda
puts ->{}.lambda? # true
puts Proc.new{}.lambda? # false
# Block to lambda
my_lambda = ->(x) { x * 2 }
block = my_lambda.to_proc # Still a lambda
Lambdas in Rails
Rails uses lambdas extensively for configuration:
# Scopes
class User < ApplicationRecord
scope :active, -> { where(active: true) }
scope :recent, -> { where("created_at > ?", 7.days.ago) }
end
# Conditional validations
class Order < ApplicationRecord
validates :card_number, presence: true, if: -> { payment_type == "credit_card" }
end
Common Mistakes
1. Assuming Lambdas and Procs Are Interchangeable
def test
p = Proc.new { |a, b| a + b }
l = ->(a, b) { a + b }
p.call(1) # Works (b = nil)
# l.call(1) # ArgumentError!
p.call(1, 2, 3) # Works (ignores extra)
# l.call(1, 2, 3) # ArgumentError!
end
2. Proc Return Breaking the Method
def each_with_report(array)
array.each do |item|
Proc.new { return "BAD" if item.nil? }.call
end
"OK"
end
puts each_with_report([1, nil, 2]) # "BAD" — exits method!
3. Forgetting .call or [] Syntax
my_lambda = -> { "hello" }
# puts my_lambda # Prints proc info, doesn't call
puts my_lambda.call # "hello"
puts my_lambda[] # "hello"
4. Overusing Lambdas When a Method Would Do
# Unnecessary
double = ->(n) { n * 2 }
result = [1, 2, 3].map(&double)
# Cleaner inline
result = [1, 2, 3].map { |n| n * 2 }
5. Not Understanding Closure Overhead
Each lambda creates a closure. Creating thousands inside loops can be slower than using methods. Use lambdas for flexibility, not micro-optimization.
Practice Questions
1. How do lambdas differ from procs in argument handling?
Lambdas enforce arity — they raise ArgumentError if called with wrong number of arguments. Procs are flexible — missing arguments become nil, extra arguments are ignored.
2. How does return behave differently in lambdas vs procs?
In a lambda, return exits the lambda and returns control to the calling method. In a Proc, return exits the entire enclosing method. Lambdas behave like methods; procs behave like inline code.
3. How do you check if a Proc object is a lambda?
Use .lambda? method. It returns true for lambdas and false for regular procs, even though both are instances of the Proc class.
4. What's the syntax for creating a lambda with parameters?
Arrow syntax: ->(param1, param2) { expression }. Keyword syntax: lambda { |param1, param2| expression }.
Challenge: Write a compose method that takes two lambdas and returns a new lambda that applies the second after the first (functional composition).
Solution
def compose(f, g)
->(*args) { f.call(g.call(*args)) }
end
double = ->(x) { x * 2 }
square = ->(x) { x ** 2 }
# square first, then double
double_after_square = compose(double, square)
puts double_after_square.call(3) # 18 (3*3=9, 9*2=18)
# double first, then square
square_after_double = compose(square, double)
puts square_after_double.call(3) # 36 (3*2=6, 6*6=36)
FAQ
{{< faq question="Are lambdas and procs the same class?" >}}
Yes. Both are instances of Proc. The difference is in behavior, which is tracked internally by Ruby. Use .lambda? to distinguish them. Lambdas are "typed" internally as lambda procs.
{{< /faq >}}
{{< faq question="When should I use a lambda vs a method?" >}} Use a lambda when you need a callable that can be passed around, stored in data structures, or created dynamically. Use a method when the logic is static, named, and called from multiple places in your code. {{< /faq >}}
{{< faq question="Can I convert a method to a lambda?" >}}
Yes. Use method(:method_name).to_proc to convert a method to a proc/lambda. The result behaves like a lambda (strict arity, proper return).
{{< /faq >}}
{{< faq question="Are lambdas slower than methods?" >}} Slightly. Method calls are optimized in Ruby. Lambda calls involve proc overhead. For most applications the difference is negligible. Use lambdas for flexibility, not in tight loops calling millions of times. {{< /faq >}}
{{< faq question="Can lambdas have default arguments?" >}}
Yes. ->(a, b = 0) { a + b } works just like regular method default arguments. The arity reflects the required arguments.
{{< /faq >}}
Try It Yourself
# lambdas_demo.rb
puts "=== Lambda vs Proc comparison ==="
def demonstrate_return
l = -> { return "lambda return" }
p = Proc.new { return "proc return" }
puts "Before lambda call"
result_l = l.call
puts "After lambda call: #{result_l}"
puts "Before proc call"
result_p = p.call # This exits the method!
puts "After proc call: #{result_p}" # Never reached
end
puts demonstrate_return
Expected output:
=== Lambda vs Proc comparison ===
Before lambda call
After lambda call: lambda return
Before proc call
proc return
What's Next
Now that you understand lambdas, learn about mixins — Ruby's mechanism for sharing behavior across classes using modules and the Comparable/Enumerable interfaces.
| Topic | Description | Link |
|---|---|---|
| Ruby Mixins | Comparable, Enumerable | {{< ref "14-mixins" >}} |
| Ruby Duck Typing | respond_to?, method_missing | {{< ref "15-duck-typing" >}} |
| Python Functions | Compare Python lambda functions | Python |
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro