A Ruby iterator is simply a method that can invoke a block of code.
At first sight, a block in Ruby looks just like a block in C, Java,
or Perl. Unfortunately, in this case looks are deceiving---a Ruby
block
is a way of grouping statements, but not in the
conventional way.
First, a block may appear only in the source adjacent to a method
call; the block is written starting on the same line as the method's
last parameter. Second, the code in the block is not executed at the
time it is encountered. Instead, Ruby remembers the context in which
the block appears (the local variables, the current object, and so
on), and then enters the method. This is where the magic starts.
Within the method, the block may be invoked, almost as if it were a
method itself, using the
yield
statement.
Whenever a
yield
is executed, it invokes the code in the block. When the block
exits, control picks back up immediately after the
yield
.
[Programming-language buffs will be pleased to
know that the keyword yield
was chosen to echo the yield
function in Liskov's language CLU, a language that is over 20
years old and yet contains features that still haven't been widely
exploited by the CLU-less.] Let's start with a trivial example.
def threeTimes
yield
yield
yield
end
threeTimes { puts "Hello" }
|
produces:
The block (the code between the braces) is associated with the call to
the method
threeTimes
. Within this method,
yield
is
called three times in a row. Each time, it invokes the code in the
block, and a cheery greeting is printed. What makes blocks interesting,
however, is that you can pass parameters to them and receive values
back from them. For example, we could write a simple function that
returns members of the Fibonacci series up to a certain
value.
[The basic Fibonacci series is a sequence of integers,
starting with two 1's, in which each subsequent term is the sum
of the two preceding terms. The series is sometimes used in sorting
algorithms and in analyzing natural phenomena.]
def fibUpTo(max)
i1, i2 = 1, 1 # parallel assignment
while i1 <= max
yield i1
i1, i2 = i2, i1+i2
end
end
fibUpTo(1000) { |f| print f, " " }
|
produces:
1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987
|
In this example, the
yield
statement has a parameter.
This value
is passed to the associated block. In the definition of the block, the
argument list appears between vertical bars. In this instance, the
variable
f
receives the value passed to the
yield
, so the
block prints successive members of the series. (This example also
shows parallel assignment in action. We'll come back to this
on page 75.) Although it is common to pass just one
value to a block, this is not a requirement; a block may have any
number of arguments. What happens if a block has a different number
of parameters than are given to the yield? By a staggering
coincidence, the rules we discuss under parallel assignment come into
play (with a slight twist: multiple parameters passed to a
yield
are converted to an array if the block has just one argument).
Parameters to a block may be existing local variables; if so, the new value of the variable will be
retained after the block completes. This may lead to unexpected
behavior, but there is also a performance gain to be had by using
variables that already exist.
[For more information on this
and other ``gotchas,'' see the list beginning
on page 127; more performance information begins
on page 128.]
A block may also return a value to the method. The value of the last
expression evaluated in the block is passed back to the method as the
value of the
yield
. This is how the
find
method used by class
Array
works.
[The find
method is actually defined
in module Enumerable
, which is mixed into class Array
.] Its
implementation would look something like the following.
class Array
|
def find
|
for i in 0...size
|
value = self[i]
|
return value if yield(value)
|
end
|
return nil
|
end
|
end
|
|
[1, 3, 5, 7, 9].find {|v| v*v > 30 }
|
� |
7
|
This passes successive elements of the array to the associated block. If
the block returns
true
, the method returns the corresponding
element. If no element matches, the method returns
nil
. The example shows
the benefit of this approach to iterators. The
Array
class does
what it does best, accessing array elements, leaving the application
code to concentrate on its particular requirement (in this case,
finding an entry that meets some mathematical criteria).
Some iterators are common to many types of Ruby collections. We've
looked at
find
already. Two others are
each
and
collect
.
each
is probably the simplest iterator---all it does is yield
successive elements of its collection.
[ 1, 3, 5 ].each { |i| puts i }
|
produces:
The
each
iterator has a special place in Ruby;
on page
85 we'll describe how it's used as the basis of the
language's
for
loop, and starting on page 102 we'll see how
defining an
each
method can add a whole lot more
functionality to your class for free.
Another common iterator is
collect
, which takes each element
from the collection and passes it to the block. The results returned
by the block are
used to construct a new array. For instance:
["H", "A", "L"].collect { |x| x.succ }
|
� |
["I", "B", "M"]
|