Blocks and yields in Ruby
Yes, it is a bit puzzling at first.
In Ruby, methods can receive a code block in order to perform arbitrary segments of code.
When a method expects a block, you can invoke it by calling the yield
function.
Example:
Take Person
, a class with a name
attribute and a do_with_name
method. When the method is invoked it will pass the name
attribute to the block.
class Person def initialize( name ) @name = name end def do_with_name # expects a block yield( @name ) # invoke the block and pass the `@name` attribute endend
Now you can invoke this method and pass an arbitrary code block.
person = Person.new("Oscar")# Invoking the method passing a block to print the valueperson.do_with_name do |value| puts "Got: #{value}"end
Would print:
Got: Oscar
Notice the block receives as a parameter a variable called value
. When the code invokes yield
it passes as argument the value of @name
.
yield( @name )
The same method can be invoked with a different block.
For instance to reverse the name:
reversed_name = ""# Invoke the method passing a different blockperson.do_with_name do |value| reversed_name = value.reverseendputs reversed_name=> "racsO"
Other more interesting real life examples:
Filter elements in an array:
days = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"] # Select those which start with 'T' days.select do | item | item.match /^T/ end=> ["Tuesday", "Thursday"]
Or sort by name length:
days.sort do |x,y| x.size <=> y.size end=> ["Monday", "Friday", "Tuesday", "Thursday", "Wednesday"]
If the block is optional you can use:
yield(value) if block_given?
If is not optional, just invoke it.
You can try these examples on your computer with irb
(Interactive Ruby Shell)
Here are all the examples in a copy/paste ready form:
class Person def initialize( name ) @name = name end def do_with_name # expects a block yield( @name ) # invoke the block and pass the `@name` attribute endendperson = Person.new("Oscar")# Invoking the method passing a block to print the valueperson.do_with_name do |value| puts "Got: #{value}"endreversed_name = ""# Invoke the method passing a different blockperson.do_with_name do |value| reversed_name = value.reverseendputs reversed_name# Filter elements in an array: days = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"] # Select those which start with 'T' days.select do | item | item.match /^T/end# Sort by name length: days.sort do |x,y| x.size <=> y.sizeend
In Ruby, methods can check to see if they were called in such a way that a block was provided in addition to the normal arguments. Typically this is done using the block_given?
method but you can also refer to the block as an explicit Proc by prefixing an ampersand (&
) before the final argument name.
If a method is invoked with a block then the method can yield
control to the block (call the block) with some arguments, if needed. Consider this example method that demonstrates:
def foo(x) puts "OK: called as foo(#{x.inspect})" yield("A gift from foo!") if block_given?endfoo(10)# OK: called as foo(10)foo(123) {|y| puts "BLOCK: #{y} How nice =)"}# OK: called as foo(123)# BLOCK: A gift from foo! How nice =)
Or, using the special block argument syntax:
def bar(x, &block) puts "OK: called as bar(#{x.inspect})" block.call("A gift from bar!") if blockendbar(10)# OK: called as bar(10)bar(123) {|y| puts "BLOCK: #{y} How nice =)"}# OK: called as bar(123)# BLOCK: A gift from bar! How nice =)
It's quite possible that someone will provide a truly detailed answer here, but I've always found this post from Robert Sosinski to be a great explanation of the subtleties between blocks, procs & lambdas.
I should add that I believe the post I'm linking to is specific to ruby 1.8. Some things have changed in ruby 1.9, such as block variables being local to the block. In 1.8, you'd get something like the following:
>> a = "Hello"=> "Hello">> 1.times { |a| a = "Goodbye" }=> 1>> a=> "Goodbye"
Whereas 1.9 would give you:
>> a = "Hello"=> "Hello">> 1.times { |a| a = "Goodbye" }=> 1>> a=> "Hello"
I don't have 1.9 on this machine so the above might have an error in it.