When you see yield anywhere in a Ruby block, you should think “execute the block.” Try out the following in irb:
| >> def do_it |
| >> yield |
| >> end |
| => :do_it |
This is a pretty plain-looking piece of code that just executes any block you give it:
| >> do_it { puts "I'm doing it" } |
| I'm doing it |
| => nil |
Outputting a string doesn’t return any value. In other words, this block is executed merely for its side effects. Now, let’s make a block that returns a value and pass it into the do_it method:
| >> do_it { [1,2,3] << 4 } |
| => [1, 2, 3, 4] |
What happens when we don’t pass in a block to do_it?
| >> do_it |
| LocalJumpError: no block given (yield) |
| from (irb):28:in `do_it' |
irb helpfully informs you that the method was not given a block to execute. You might want to pass arguments into a block. For example, say you want a method that passes in two arguments to a block:
| >> def do_it(x, y) |
| >> yield(x, y) |
| >> end |
| => :do_it |
Now, we can pass in and execute any block that takes two arguments:
| >> do_it(2, 3) { |x, y| x + y } |
| => 5 |
| |
| >> do_it("Ohai", "Benevolent Dictator") do |greeting, title| |
| "#{greeting}, #{title}!!!" |
| end |
| => "Ohai, Benevolent Dictator!!!" |
There’s a tiny gotcha to yield’s argument-passing behavior. It is more tolerant of missing and extra arguments than you might expect. Missing arguments will be set to nil, and extra arguments will be silently discarded.
Let’s modify the method and give it fewer arguments. In this definition of do_it, yield is only given a single argument:
| >> def do_it(x) |
| >> yield x |
| >> end |
| => :do_it |
Observe what happens when the method receives a block that expects two arguments:
| >> do_it(42) { |num, line| "#{num}: #{line}" } |
| => "42: " |
If you find this behavior slightly strange, you can think of yield acting a little like a parallel assignment, in that nils are assigned to missing arguments:
| >> a, b = 1 # => 1 |
| >> b # => nil |
As previously noted, missing arguments are assigned nil, which explains the lack of an error. What happens if the yield is given more arguments than expected? Once again, think about the parallel assignment analogy:
| >> a, b = 1,2,3 |
| => [1, 2, 3] |
| >> a |
| => 1 |
| >> b |
| => 2 |
In this case, 3 is discarded. Redefine do_it once more:
| >> def do_it |
| >> yield "this", "is", "ignored!" |
| >> end |
| => :do_it |
Now, pass in a block that takes in no arguments:
| >> do_it { puts "Ohai!" } |
| => Ohai! |
Once again, Ruby executes the code without a hitch. This is also consistent with the parallel assignment behavior as previously demonstrated. Keep in mind this argument passing behavior; otherwise, you might waste precious time figuring out why Ruby doesn’t throw an exception when you think it would, especially when writing unit tests.
Now let’s look at the relationship between blocks and closures.