Blocks, yield and closures

yield and block_given?

Every method can be called with a block, and yield runs it. The method does not declare the block; it asks whether one was given and calls it:

def twice return :no_block unless block_given? yield yield end p twice { puts "hi" } p twice

ruby twice_demo.rb

hi
hi
nil
:no_block

yield returns the value of the block, so a method can collect it, compare it or pass it on. block_given? is the guard that keeps a method usable without a block.

The block as a named object

&block as the last parameter captures the block into a Proc, which is what a method needs when it wants to store the block or pass it to another call:

def run(&block) block.call end p run { :done }

ruby -e 'def run(&block) = block.call; p run { :done }'

:done

block.call and yield do the same thing here. The captured form is the one to use when the block is called later, stored in an object, or handed to map.

A block is a closure

A block keeps the local variables that were in scope where it was written, even after the method that defined them has returned:

ruby -e 'def counter; n = 0; -> { n += 1 }; end; c = counter; p c.call, c.call'

1
2

The lambda holds n, so the two calls see the same counter. That is the mechanism behind a memoised value, a lazy default, and the each block that reads a variable from outside the loop.

A with_timing helper

The pattern that uses everything above in four lines: take a block, run it, print how long it took. It is the helper a script reaches for before a profiler:

def with_timing started = Time.now yield puts Time.now - started end with_timing { sleep 0.1 }

ruby -e 'def t; s = Time.now; yield; puts Time.now - s; end; t { sleep 0.1 }'

0.10011083

The number is seconds as a float, close to the one-tenth the block asked for. Wrap the block around the suspicious step rather than the whole script; the section that takes the time is the one to read.

What to watch for

yield without a block and without a guard raises LocalJumpError. Either give the method a default block (def twice; yield; end cannot) or check block_given? first.

A block is not a method argument: return inside a block returns from the method that yielded, which surprises a reader who expects it to leave the block only. A lambda returns from itself, which is why a stored callback is usually a lambda.

Next: Procs, lambdas and method objects

Article author: Arthur Isaev