2009-02-22

Enumerator

Do you remember the Fiber? If not, better have a look there before reading on. I will show another use of fibres, this time we won't see them, but they are there, in the guts of the
Enumerator
.

One can use enumerators in either of the two main ways:

Virtual array
If you have read the post about fibres, you are already familiar with the virtual arrays. An enumerator is an easier and a bit more automated way to create a virtual array.

Let's say we'd like to observe a HOTPO sequence starting at any chosen number. As we know, the sequence might be infinite, so it wouldn't be very wise to create an array holding all the elements. But we can create an enumerator to iterate over them, like this:
def hotpo(v)
Enumerator::new\
{ |y|
loop\
{
y<<v
break if v==1
v=(v&1>0) ? 3*v+1 : v/2
}
}
end

hotpo(27).each{|x| print x," "}

The function takes the first value of the sequence as an argument, and returns an enumerator. We create an enumerator of this type by passing it a block and putting the sequentially computed values in the block argument. So: the
y
is the virtual array itself, say good evening.

The loop is not infinite, at least not in any of the known cases, because no infinite HOTPO sequence has been found. But if we remove the break, we could see that the program doesn't hung, even though it has the infinite loop in it! Well, it does hung, but it still produces the output, so it's not more hung than an open word processor.

This behaviour is very similar to this presented in the post about fibres, and that's because the enumerator uses them. If you have understood the fibres well, you could try to implement this kind of enumerator yourself - just as a small exercise for the reader.

View of an enumerable
The other way of using enumerators (the more standard way, I'd say) is to create them from existing enumerable objects. The idea of an enumerator is to allow only very limited access to the underlying enumerable.

For example,
an_array.each
(without passing a block) is an enumerable which can be regarded as a safe read-only view of the array. It does not allow the user to call any other method of the array but
each
and its derivate methods. So you can call
an_array.each.each{...}
or
an_array.each.map{...}
, but the call to
an_array.each_map!{...}
, even though legal, will not modify the underlying object. But still
an_array.map!.each{...}
is able to do so.

The general idea of using chained calls of enumerating methods is:
- If the first method is
each
, then any non-modifying method can be used as the second call.
- If the first method is something else, the valid second methods are
each
which simply forwards the call to the first method, and
with_index
, which does the same but also passes the element index to the block.

So the following, even though perfectly legal, makes no sense:
an_array.select.map{...}
. One of the methods should be neutral, and the neutral methods are
each
and
each_with_index
(or just
with_index
). So apart from making a save view, the main advantage of using enumerators is the possibility to call
an_array.map.with_index{|e,i| ...}
or even
an_array.select.with_index{|e,i| ...}
.

Note that the methods
all?
and
any?
do not return useful when called with no block, so you cannot make these checks with index.

No comments: