Tuesday, June 5, 2007

Ruby Yield

Yield is one of the most powerful concepts implemented in the Ruby programming language. Yield lets you branch from within a method and execute some external code, then return to the original code in the first method.

What's so special about yield in Ruby? You could get the same branch and return flow by just executing a method call in java, or a function call in C.

Heck, COBOL's PERFORM verb implements branch and return behavior. So what's up with yield?

Well, the behavior of what you branch to is set in stone with all those other techniques, but is undetermined until coded in Ruby. Yield is kind of like a place holder that says "I'm going to branch off and do something here, but I don't know what I'm going to do yet".

Maybe some examples will help make it more clear. Here is a Ruby method with some yields coded in it, then the code to invoke the method and the block of code to invoke when the yield is encountered:


def block1
puts "Start of block1..."
yield
yield
puts "End of block1."
end

block1 {puts "yielding..."}

Running this code would produce the following output:


Start of block1...
yielding...
yielding...
End of block1.

So block1 executes it's first line and prints "Start of block1...", then it executes the yields which branch to the block of code outside block1 but associated with it at run time, then comes back and prints "End of block1."

OK that's, fine but isn't that functionally the same as the following code:


def block2
puts "Start of block2..."
put_it("in put_it called method...")
put_it("still in put_it called method...")
puts "End of block2."
end

def put_it(text)
puts text
end

block2

Running this second set of code produces the following:


Start of block2...
in put_it called method...
still in put_it called method...
End of block2.

For the most part, these are pretty much the same. However without having to redefine any methods I could immediately execute the block1 method with a different block associated with it. The yield would branch and execute this newly associated logic:


block1 {require 'time'
puts Time.now.httpdate}

This produces the output:


Start of block1...
Tue, 05 Jun 2007 19:20:35 GMT
Tue, 05 Jun 2007 19:20:35 GMT
End of block1.

The hard coded function call in block2 forever ties the branching logic to the put_it function. To get different behavior you would need to either change the function call in block2 to call a different function, or change the behavior of the function put_it. This second way is brittle because it could break code elsewhere that calls put_it. The first is cumbersome and possibly brittle.

Ruby's yield is a wonderful construct.

What about when you don't want to branch, but want the other functionality in the block1 code to be executed? Can you just run the code without the associated block?

Let's see what happens. Running block1 without an attached block for the yield to associate to:


block1

This produces the result:


Start of block1...
LocalJumpError: no block given
from :3:in 'block1'
from :7
from :0

We get a 'no block given' error at line 3 in method 'block1'. Line 3 is our first yield in the block1 method.

What to do?

We can wrap the yield in an if statement and check for the presence of a block using the block_given? conditional. It returns true if a block was given with the method call, or false if not.

For this example I'll use a slightly more idiomatic way of checking for the presence of a block in ruby:


def block3
puts "Start of block3..."
yield if block_given?
puts "End of block3."
end

now if we run the code with a block:


block3 { puts "yielding" }

or without:


block3

It works both times producing:


Start of block3...
yielding...
End of block3.

and


Start of block3...
End of block3.


This will make your methods less brittle and more agile.

Ruby's yield is worth getting to know. You can learn more here and here. Many of Ruby's built in methods contain yields that allow you to associate blocks with them producing added functionality at run time.

4 comments:

El Raichu said...

personally, i never use yield. i've always preferred calling the block directly. i can toss around the block just like any other variable, and it defaults to nil if no block is given.


---start---


def some_method *args, &block

case block.arity

when -1 then block[ *args ]
when 0 then block[]
when 1 then block[ args.first ]

end unless block.nil?

end


---end---

sheesh... code formatting is awful... go livejournal!

Sean Lynch said...

Thanks el raichu!

I like using case statements to handle situations that might vary at run time. Its easier to handle multiple situations and still take care of the defaults.

I'll try to incorporate your style in my current project. I can already see why you prefer it to yield.

---
Yes formatting is bad. At least I can play with the stylesheet on the blog.

Tony Ennis said...

Ha, some necromancy going on here!

What happens when you have this:

def x
yield
b()
end

def b
yield
end

Basically how do I pass a yield block into b from x?

The method proposed by El Raichu seems to work out better.

Anonymous said...

@Stormcrow

def function1(&block)
yield
function2 &block
end

def function2(&block)
yield
end

function1 { puts 'yielding...' }

# yielding...
# yielding...
# => nil