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