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 with block_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 return only 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