Blocks, Procs and Metaprogramming
Blocks and yield, Procs vs Lambdas, and an honest intro to method_missing and define_method.
Blocks
A block is a chunk of code you can pass to a method, using either { } (conventionally for one-liners) or do...end (conventionally for multi-line blocks):
[1, 2, 3].each { |n| puts n * 2 }
[1, 2, 3].each do |n|
doubled = n * 2
puts doubled
end
A block isn't a value or an object by itself — it's syntax attached to a method call, and only the method it's passed to decides whether (and how many times) to run it. This is different from a value you can store in a variable and pass around, which is exactly the gap Procs and Lambdas fill.
yield
Inside a method, yield invokes the block that was passed to it. This is how methods like each and map are implemented — they're ordinary Ruby methods that just happen to call yield:
def repeat(times)
i = 0
while i < times
yield i # runs whatever block the caller passed, with i as its argument
i += 1
end
end
repeat(3) { |i| puts "Iteration #{i}" }
# Iteration 0
# Iteration 1
# Iteration 2
block_given? lets a method check whether a block was actually passed, so you can support calling it with or without one:
def greet
if block_given?
yield
else
puts "Hello!"
end
end
greet # Hello!
greet { puts "Hi there!" } # Hi there!
Procs
A Proc turns a block into a real object you can store in a variable, pass around, and call later:
say_hello = Proc.new { |name| puts "Hello, #{name}!" }
# or the shorthand:
say_hello = proc { |name| puts "Hello, #{name}!" }
say_hello.call("Ali") # Hello, Ali!
say_hello.("Ali") # equivalent shorthand call syntax
Lambdas
A Lambda is a stricter, more method-like flavor of Proc:
multiply = lambda { |a, b| a * b }
# or the shorthand:
multiply = ->(a, b) { a * b }
puts multiply.call(3, 4) # 12
puts multiply.(3, 4) # 12
Procs vs. lambdas — the real differences
| Behavior | Proc | Lambda |
|---|---|---|
| Argument count checking | Lenient — extra args ignored, missing args become nil |
Strict — raises ArgumentError on a mismatch, just like a method |
return inside it |
Returns from the enclosing method immediately | Returns only from the lambda itself, like a normal method |
def test_proc
p = Proc.new { return 10 }
p.call
puts "This line never runs" # a Proc's `return` exits test_proc entirely
end
def test_lambda
l = lambda { return 10 }
l.call
puts "This line DOES run" # a lambda's `return` only exits the lambda
end
test_proc # prints nothing extra — returns out of test_proc at p.call
test_lambda # This line DOES run
This distinction trips up almost everyone the first time — a stray return inside a Proc used as a callback can silently exit far more code than intended.
A light, honest intro to metaprogramming
Ruby lets programs inspect and modify classes and methods at runtime — this is what powers much of Rails' "magic," like has_many :comments dynamically generating a full set of association methods. Two of the most common building blocks:
method_missing — intercepts calls to methods that don't actually exist on an object, letting you handle them dynamically instead of raising NoMethodError:
class DynamicProxy
def initialize(data)
@data = data
end
def method_missing(name, *args)
key = name.to_s
if @data.key?(key)
@data[key]
else
super # important — falls back to default behavior (raises NoMethodError) for truly unknown methods
end
end
def respond_to_missing?(name, include_private = false)
@data.key?(name.to_s) || super
end
end
user = DynamicProxy.new("name" => "Ali", "age" => 22)
puts user.name # Ali — no `name` method was ever explicitly defined
puts user.age # 22
Always pair method_missing with respond_to_missing? — otherwise .respond_to? and things like method(:name) will incorrectly report the dynamic method doesn't exist.
define_method — defines a real method on a class programmatically, which is both faster and generally safer than method_missing since the method genuinely exists afterward:
class Product
%w[name price description].each do |attribute|
define_method(attribute) do
instance_variable_get("@#{attribute}")
end
define_method("#{attribute}=") do |value|
instance_variable_set("@#{attribute}", value)
end
end
end
product = Product.new
product.name = "Widget"
puts product.name # Widget
This is effectively rebuilding attr_accessor from first principles — a good illustration of what attr_accessor itself does under the hood using metaprogramming.
Common mistakes
- Using a
Procas a callback without realizing itsreturnexits the enclosing method, not just the block — prefer a lambda when you specifically want method-like return behavior. - Overusing
method_missingwheredefine_methodwould be clearer, faster, and give better error messages and IDE/tooling support —method_missingshould be a last resort for genuinely dynamic method names. - Forgetting to call
superinmethod_missingfor unhandled cases — without it, truly invalid method calls fail silently or with a confusing error instead of the standardNoMethodError.
Interview questions
Q: What's the difference between a block, a Proc, and a Lambda?
A block is inline syntax passed to a method call and isn't itself an object. A Proc converts that same idea into a real, storable, passable object, but is lenient about argument count and its return exits the enclosing method. A Lambda is a stricter Proc — it checks argument count like a normal method call and its return only exits the lambda itself.
Q: When would you use yield versus accepting an explicit block parameter (&block)?
Use yield when a method simply needs to run the block it was given, with no need to store, pass along, or inspect the block as an object. Use an explicit &block parameter when you need to pass the block to another method, store it, or check block.arity/call it conditionally as an object.
Q: What's a practical risk of relying heavily on method_missing?
It makes an object's actual interface invisible to tools, documentation, and other developers reading the code — object.respond_to? and reflection can behave unexpectedly unless you also implement respond_to_missing?, and typos in method names can silently do the wrong thing instead of raising a clear error immediately.