Introduction
Blocks are everywhere in Ruby.
Every each, every map, every File.open(...) do |file| is a method
handing control to a chunk of code you passed in. It’s still one of the things
I love most about the language.
Plenty of people write Ruby for years leaning on blocks without ever pulling apart the three things underneath them: blocks, procs, and lambdas. They look different, and they differ in a few ways that genuinely matter, but they all do the same job, which is to hand a piece of behavior to a method. Get that distinction clear and the iterator methods you reach for every day stop looking like magic; you can read them, and write your own.
Let me walk you through them in the order I find clearest.
Blocks: the chunk you pass to a method
A block is an anonymous piece of code attached to a method call. It comes in two forms:
[1, 2, 3].map { |n| n * 2 } # => [2, 4, 6]
[1, 2, 3].map do |n|
n * 2
end
Braces for one-liners, do/end when the body spans several lines. The
choice is about readability; both forms build the same block.
A block isn’t an object you can store in a variable. It’s an implicit extra
argument that rides along with the method call. To actually use it, the
method reaches for yield.
yield: calling the block
Inside a method, yield runs the block that was passed in. The method’s own
code runs around it:
def with_transaction
puts "BEGIN"
yield
puts "COMMIT"
end
with_transaction { puts " UPDATE users ..." }
# BEGIN
# UPDATE users ...
# COMMIT
The method runs until yield, hands control to the block, then picks up
right after. That “run something before and after your code” shape is exactly
how transaction helpers, benchmarking, and File.open work.
If a method calls yield but no block was given, Ruby raises a
LocalJumpError. When the block is optional, guard it with block_given?:
def shout(text)
text = yield(text) if block_given?
"#{text.upcase}!"
end
shout("hello") # => "HELLO!"
shout("hello") { |t| t.reverse } # => "OLLEH!"
Arguments and return value
yield can pass arguments to the block, and the block hands a value back:
its last expression is what yield evaluates to.
Both directions show up if you reimplement select by hand:
def keep(list)
result = []
list.each { |item| result << item if yield(item) }
result
end
keep([1, 2, 3, 4, 5, 6]) { |n| n.even? } # => [2, 4, 6]
yield(item) sends each element into the block; the block returns true or
false; the method uses that to decide what to keep. Pass a different block
and the method filters by a different rule without changing a line. This is
what I love about blocks: one method, many behaviors. And it’s not a toy
example, this keep is more or less how Array#select itself is built.
Capturing the block: &block
Sometimes yield isn’t enough. You might want to hold onto the block, pass
it somewhere else, or store it for later. Prefix a parameter with & and
Ruby hands you the block as an object:
def repeat(times, &block)
times.times { |i| block.call(i) }
end
repeat(3) { |i| puts "attempt #{i}" }
# attempt 0
# attempt 1
# attempt 2
Now block is a real object. block.call(i) runs it (block.yield(i) does
the same thing). Because it’s an object, you can keep it in an instance
variable, forward it to another method, or inspect it, none of which bare
yield allows.
That object has a type: Proc.
Procs
A proc is a block captured as an object you can name and reuse:
double = proc { |n| n * 2 }
double.call(21) # => 42
double.(21) # => 42
double[21] # => 42
The three call syntaxes are interchangeable; pick whichever reads best.
The & from the previous section works both ways. Prefixing a proc with &
in a method call turns it back into a block, so you can feed a stored proc
to any method that expects one:
double = proc { |n| n * 2 }
[1, 2, 3].map(&double) # => [2, 4, 6]
This is also what the familiar &:symbol shorthand really is. &:capitalize
converts the symbol :capitalize into a proc that calls capitalize on each
element:
%w[alice bob carol].map(&:capitalize) # => ["Alice", "Bob", "Carol"]
Lambdas
A lambda is a proc with two stricter rules. You write one with the ->
syntax:
square = ->(n) { n * n }
square.call(5) # => 25
The first difference is arity. A lambda checks its argument count like a
real method. A plain proc doesn’t: it pads missing arguments with nil and
ignores extra ones.
add = ->(a, b) { a + b }
add.call(1) # ArgumentError: wrong number of arguments (given 1, expected 2)
loose = proc { |a, b| [a, b] }
loose.call(1) # => [1, nil]
The second difference shows up with return, and it’s the classic trap I’ve
watched bite plenty of solid developers:
def with_lambda
l = -> { return 10 }
l.call
20
end
with_lambda # => 20
def with_proc
p = proc { return 10 }
p.call
20
end
with_proc # => 10
return in a lambda returns from the lambda itself, so the method carries on.
return in a proc returns from the enclosing method, which is why
with_proc never reaches its last line. When you store a callback and call it
later, that jump out of the surrounding method is rarely what you want, so
reach for a lambda: it behaves like the small method it looks like.
They’re all closures
Blocks, procs, and lambdas share one more trait: they remember the environment where they were defined. They capture surrounding variables and keep them alive.
def counter
count = 0
-> { count += 1 }
end
tick = counter
tick.call # => 1
tick.call # => 2
tick.call # => 3
counter returns after its first line, yet count lives on inside the
lambda. Each call sees and updates the same captured variable. That’s a
closure, and it’s how you build counters, generators, and small bits of
private state without a class.
Putting it together: a tiny memoizer
Here’s the example I reach for when I want all four ideas on screen at once. It’s a memoizer: give it a computation and it hands back a callable that caches its results.
def memoize(&computation)
cache = {}
->(arg) { cache[arg] ||= computation.call(arg) }
end
price_with_tax = memoize { |amount| amount * 1.20 }
price_with_tax.call(100) # => 120.0
price_with_tax.call(100) # cached, no recompute
Every idea from this post is in those three lines. &computation captures the
incoming block as a proc. The returned lambda closes over both cache and
computation, so they survive after memoize returns. And cache[arg] ||=
runs the computation once per argument, then serves the stored value ever
after. Swap the block and you have a memoizer for any pure function. Three
lines, and it leans on all four concepts at once, that’s the kind of thing
that keeps me coming back to Ruby.
Wrapping up
Three forms of the same idea:
- a block is anonymous code passed to a method; the method runs it with
yield(and checks for it withblock_given?). - a proc is that block captured as an object you can name, store, and
pass around;
&converts between the two. - a lambda is a proc that behaves like a method: strict about arguments,
and its
returnonly exits itself. - all three are closures: they carry their surrounding scope with them.
My rule of thumb: reach for a block when a method needs a one-off piece of behavior, and reach for a proc or a lambda when you need to store that behavior, pass it around, or reuse it. Once these three feel natural, a big chunk of the standard library stops being something you memorize and becomes something you can read, because it’s plain Ruby built on the same blocks you pass every day. That shift, from using these tools to composing with them, is the part of Ruby I still enjoy most.
Have comments or want to discuss this topic?
Send an email to ~bounga/bounga.org-discuss@lists.sr.ht