Reading — step 1 of 7
Learn
Ruby's closures (Procs, Lambdas, blocks) capture not just outer variables — they capture the entire binding (the local scope at definition time). Metaprogramming Ruby spends a chapter on binding tricks alone.
Proc vs Lambda — the core difference
p_proc = Proc.new { |x, y| x + (y || 0) }
p_proc.call(1) # 1 — missing arg becomes nil
p_proc.call(1, 2, 3) # 3 — extra arg dropped
l_lambda = lambda { |x, y| x + y }
# l_lambda.call(1) # ArgumentError — strict arity
l_lambda.call(1, 2) # 3
Lambdas are STRICT about arity. Procs are loose. The arrow syntax ->() makes lambdas:
add = ->(a, b) { a + b }
add.call(2, 3) # 5
add.(2, 3) # 5 — alternative syntax
add[2, 3] # 5 — bracket syntax
return semantics — the deeper difference
def proc_test
p = Proc.new { return 1 }
p.call # return jumps OUT of proc_test!
return 2 # never reached
end
puts proc_test # 1
def lambda_test
l = lambda { return 1 }
l.call # return only exits the lambda
return 2
end
puts lambda_test # 2
In a Proc, return returns from the ENCLOSING method. In a lambda, it returns from the lambda only. This is the most important practical difference.
Capturing bindings
Closures see local variables defined in their enclosing scope:
def counter
count = 0
-> { count += 1 } # closure captures `count` by reference
end
c = counter
c.call # 1
c.call # 2
c.call # 3
Each call to counter creates a fresh count and a closure capturing it. Different from Java's effectively-final captures — Ruby captures by reference, mutations persist.
Binding objects
binding (a method) returns a Binding object representing the current scope:
def trap
x = 10
binding
end
b = trap
b.eval("x") # 10 — accessing the captured local
b.eval("x = 999")
b.eval("x") # 999
Useful for debugging: drop a binding.irb (Ruby 2.5+) into your code to start an interactive REPL with all locals visible.
instance_eval — switching self
Run a block in the context of a different object:
class Person
def initialize(name)
@name = name
end
end
p = Person.new("Ada")
p.instance_eval { puts @name } # "Ada" — accesses @name as if inside p
p.instance_eval { @age = 36 } # adds an instance variable to p
Gives the block access to the object's privates. Used by every DSL — RSpec, Sinatra, Rake.
class_eval — adding methods at runtime
class User
attr_accessor :name
end
User.class_eval do
def shout
@name.upcase
end
end
User.new.tap { |u| u.name = "ada"; puts u.shout } # "ADA"
Reopens the class to add methods. Lower-level than define_method — use class_eval do def ... end for clean syntax.
Common mistakes
- Using a Proc when a lambda is needed — return semantics surprise.
- Captured variables in loops — closures capture by reference; old loop iteration variables are shared. Use a local re-binding per iteration.
- Mixing Proc.new and
proc—proc(lowercase) is a Proc.new. Just consistent style — pick one. instance_evalblocks losing local variables — block has access to the OBJECT's state; the OUTER scope's locals are still available, butselfswitches.- Trying to use
returnin a top-level Proc — LocalJumpError, since there's no enclosing method to return from.
Discussion
Ask a question, share an insight, or help someone who’s stuck.
Sign in to post a comment or reply.
Loading…