DEV Community

Elizabeth Mattijsen

Posted on

Pushing the limits

This is the second part of the "A gaze of iterators!" series.

Pushing the limits

So let's again look at the list of methods the iterator on `42` has:

``````.say for 42.iterator.^methods'
# new
# pull-one
# push-exactly
# push-all
# skip-one
# skip-at-least
# count-only
# sink-all
# bool-only
# push-until-lazy
# push-at-least
# skip-at-least-pull-one
# is-lazy
# is-deterministic
# BUILDALL
``````

Hmmm... `.push-all` looks interesting. Could it really be that simple? Let's see!

``````my @array;
42.iterator.push-all(@array);
say @array;  # [42]
``````

And how about a list of values?

``````my @array;
<a b c>.iterator.push-all(@array);
say @array;  # [a b c]
``````

Looking at this, you could realize that the above is just a convoluted way to write:

``````my @array = <a b c>;
say @array;  # [a b c]
``````

And you'd be right again! And now you have a better idea of what goes on under the hood. Well, at least conceptually, because the actual implementation is of course at liberty to take short-cuts to improve efficiency.

Don't like that one

The next method on that list is `.skip-one`. Looks like it's mostly like `.pull-one`, so let's check:

``````my \$iter = <a b c>.iterator;
say \$iter.skip-one; # 1
say \$iter.pull-one; # b
say \$iter.pull-one; # c
say \$iter.skip-one; # 0
``````

So it looks like `.skip-one` really skips a value. But it also returns something? Indeed, it returns either `1` to indicate a successful skip (in this case "a" got skipped", and `0` for an unsuccessful skip (in this case because there is no value after "c").

Why not `True` and `False` you might ask? Well, these are all methods that work under the hood as efficiently as possible, and turning a native integer `1` into a boolean `True` would just be extra and unnecessary work.

What is it

Those two `.is-lazy` and `.is-deterministic` methods also look interesting:

``````say 42.iterator.is-lazy;           # False
say 42.iterator.is-deterministic;  # True
``````

The `.is-lazy` method indicates whether the iterator is lazy or not.

In hindsight, the term "lazy" was probably a bad choice. The most obvious thing about "lazy" iterators, is that you cannot calculate the number of elements it will produce. So the term "countable" (while reversing the meaning of the returned value) would probably have been better.

``````say (1..*).iterator.is-lazy;  # True
``````

is an example of a "lazy" iterator, of which you can not count the number of elements in a range of integers from 1 to infinity. If you try to do that with the `.elems` method, you will get an error:

``````say (1..*).elems;  # Cannot .elems a lazy list
``````

If it wouldn't produce the error, it would hang because it would be producing values "ad infinitum" literally! Before being able to tell you the number of elements.

The `.is-deterministic` method indicates whether the iterator, given a certain source, will always produce the same values in the same order. The Raku internals can optimize certain situations if it knows whether the produced values will always be the same.

``````say (1..10).iterator.is-deterministic;          # True
say (1..10).pick(*).iterator.is-deterministic;  # False
``````

Note the `.pick` method will produce the given values in a random order, so clearly not deterministic!

``````say (1..10).pick(*);  # (7 1 9 2 4 5 10 8 3 6)
say (1..10).pick(*);  # (4 6 8 9 2 10 7 5 1 3)
``````

That's weird

What does `.BUILDALL` do? Actually, nothing that should concern you. The ALLCAPS of the method really indicates that there is something special going on!

The `.BUILDALL` method is a method that is automatically generated for every class, and it contains the default logic to initialize an object of that class. There is no source code for it: the definition of a class determines how that method will be directly generated into executable bytecode.

Now, the other methods all have names that make sense, probably. But there is one method that appears to be different: `.sink-all`. Let's see what happens if we call that:

``````say <a b c>.iterator.sink-all;  # IterationEnd
``````

That's informational? Not! But what did it do? In this particular case, it will mark the iterator as completed. Not very useful.

But there are other (very common) cases where calling the `.sink-all` method is very useful. Remember that a `for` loop is really just a `.map` of which the results are discarded?

``````my \$seen = 0;
(1..10).map({++\$seen}).iterator.sink-all;
say \$seen;  # 10
``````

The `.sink-all` method is used internally for those iterators that are just executed for their side-effects. So the above is just a very complicated way to write:

``````my \$seen = 0;
++\$seen for 1..10;
say \$seen;  # 10
``````

The term "sink" is the Raku equivalent for what other programming languages call "void context". But more about that later in this series.

Conclusion

This concludes the second part of the series, in which most of the other methods that you can call on an iterator have been explained. Specifically the `.skip-one`, `.push-all`, `.is-lazy`, `.is-deterministic` and `.sink-all` methods. With a side-order of `.pick`.

Questions and comments are always welcome. You can also drop into the #raku-beginner channel on Libera.chat, or on Discord if you'd like to have more immediate feedback.

I hope you liked it! Thank you for reading all the way to the end.