Saturday 7 March 2009

Ruby blocks

A block is a chunk of code. What is great in Ruby is the way they can be passed as an argument to a method. For example, that is what is happening here:
(0..10).each { |x|
p x
}

The (0..10) is a Range object, with a method, each. The method is passed the block (everything between the curly braces). Blocks can be defined with do and end, rather than curly braces (but note that the precedence is different).
(0..10).each do |x|
p x
end


Implicitly passing blocks
Here is an example of passing a block to a custom method, test. Note that test does not mention the block at all, so the block is considered to have been passed implicitly. Where the block is used is with the yield statement.
def test
p yield(5, 'house')
p yield(100, 'mansion')
end

test { |i, s|
puts "You are in the block #{i} #{s}"
"Returning #{i} #{s}"
}

# Output:

# You are in the block 5 house
# "Returning 5 house"
# You are in the block 100 mansion
# "Returning 100 mansion"

So what is going on? The test method is invoked, and passed a block of code. Inside the test method, Ruby iterates though each line until it reaches a yield statement, and when it does, it runs the code block passed to the method. The yield statement is kind of like a method call, in that you can pass it arguments (and it will insist on the right number of arguments), and it can return a value too. You can think of yield as an alias for your block, so in the above example, yield is sent a number and a string, and returns a new string.

Naturally the block will be run every time Ruby encounters a yield statement, which is twice in the above example.

Iterating with blocks
Okay, now it gets a bit hairy, and web pages that actually address this become correspondingly rare...

How do you get the method that is receiving a block to iterate over an Array or Hash? In this example, a method, test, is added to the class Array, then an Array object is ceated and test is invoked.
class Array
def test
total = 0
each { |x|
p yield(5 + x)
total += x
}
total
end
end

ma = [12, 34, 8]
p ma.test { |y|
p y
y - 5
}

# Output:

# 17
# 12
# 39
# 34
# 13
# 8
# 54

The first point to note is the each statement and its associated block. This is what allows us to iterate over the Array (or Hash). The each statement sets up a variable, x. This will take the value of each member of the array in turn, as normal. On the next line there is the yield statement. This invokes the block that was received, sending it the current value from the array, plus five.

The block is set up to accept a single value, and to call it y, which it then prints. It then returns this value minus five.

Back with the yield statement, and the test method prints the returned value. Then it adds the current value from the array, x, to the variable total. Once the loop finishes (all the array members have been done), total is used as a return value for the test method.

Back outside the test method, the returned value is printed.

Here are more useful examples for the Array class.

class Array
# Allows you to loop over an array, accessing
# both the index and the value
def each_pair
each_index { |i| yield i, fetch(i) }
end

# As the each method, but skips the first element
def each_not_first
each_index { |i| yield fetch(i) unless i == 0 }
end

# Returns a total over each element in the array
# where the value for an element is determined
# by the given block.
def total &prc
val = 0
each { |e| val += prc.call(e) }
val
end

# Returns an element that best fits the criteria
# given by the block.
# Note that I have used yield here, it seems to work
# better if you have more than one parameter;
# "warning: multiple values for a block parameter..."
def find_best &prc
best = first
each_not_first { |e| best = e if yield(best, e) }
best
end
end

# Example array
ary = [
{:name => 'one', :value => 56},
{:name => 'two', :value => 79},
{:name => 'three', :value => -5},
]

# This uses each_pair to print both the index,
# and the name of the item.
ary.each_pair { |index, item| "#{index}: #{item[:name]}" }

# This example uses total to add up the
# values of each element.
p ary.total { |e| e[:value] }

# This one uses find to get the element with the
# highest value.
p ary.find { |x, y| x[:value] < y[:value] }


Explicitly passing blocks
If you want to be able handle the block other than through yield, you need to pass it explicitly. All this involves is listing it in the arguments. Note that the block must be last in the list, and has to be preceded by an ampersand (but the ampersand should not be present when used later in your code). When you do this, the block is converted to a Proc object (which I discussed here; note that you cannot pass a Proc object in lieu of a block).

def test &prc
puts "The block is of the #{prc.class} class"
puts prc.call('This') unless prc.nil?
end

test { |s| "#{s} does nothing" }

test

# Output:

# The block is of the Proc class
# This does nothing
# The block is of the NilClass class

Apparently, this is significantly slower than using implicit passing. The block is invoked with call; arguments to that are passed to the block.

Note that you cannot set a default value for a block in the arguments of method, however, as shown above, if no block is given the variable will be set to nil.

Many languages have a 'with' statement (Visual BASIC and Pascal). This example adds that functionality to Ruby, and also compares explicit and implicit passing:
# Define a complicated data struction
data = { :ary => %w(one two three four) }

# Define a method, 'use', for all objects
# This version uses explicit passing
class Object
def use &prc
prc.call self
end
end

# This version uses implicit passing
class Object
def use
yield self
end
end

# invoke 'use' on a specific member of the data structure
data[:ary][2].use do |x|
# Do stuff with x, rather than data[:ary][2]
p "The number is #{x}"
end


See also:
http://blog.sidu.in/2007/11/ruby-blocks-gotchas.html
http://www.programimi.com/2007/10/11/ruby-on-rails-code-blocks-and-iteration/

Struggling with Ruby: Contents Page

No comments: