Perl 6: Seqs, Drugs, And Rock'n'Roll (Part 2)
Read this article on Perl6.Party
This is the second part in the series! Be sure you
read Part I first where we discuss
what Seqs are and how to .cache them.
Today, we'll take the Seq apart and see what's up in it; what drives it;
and how to make it do exactly what we want.
PART II: That Iterated Quickly
The main piece that makes a Seq do its thing is
an object that does the Iterator
role. It's this object that knows how to generate the next value, whenever
we try to pull a value from a Seq, or push all of its values somewhere, or
simply discard all of the remaining values.
Keep in mind that you never need to use Iterator's methods directly,
when making use of a Seq as a source of values. They are called
indirectly under the hood in various Perl 6 constructs. The use case for calling
those methods yourself is often the time when we're making an Iterator
that's fed by another Iterator, as we'll see.
Pull my finger...
In its most basic form, an Iterator object needs to provide only one
method: .pull-one
my $seq := Seq.new: class :: does Iterator {
method pull-one {
return $++ if $++ < 4;
IterationEnd
}
}.new;
.say for $seq;
# OUTPUT:
# 0
# 1
# 2
# 3
Above, we create a Seq using its
.new method that expects an
instantiated Iterator, for which
we use an anonymous class that does the Iterator role and provides a
single .pull-one method that uses a pair of
anonymous state variables to generate 4
numbers, one per call, and then returns
IterationEnd constant to signal the Iterator
does not have any more values to produce.
The Iterator protocol forbids attempting to fetch more values from an
Iterator once it generated the
IterationEnd value, so
your Iterator's methods may assume they'll never get called again past
that point.
Meet the rest of the gang
The Iterator role defines several more methods, all of which are
optional to implement, and most of which have some sort of default
implementation. The extra methods are there for optimization
purposes that let you take shortcuts depending on how the sequence is iterated
over.
Let's build a Seq that hashes a bunch of data using
Crypt::Bcryptmodule
(run zef install Crypt::Bcrypt to install it). We'll
start with the most basic Iterator that provides .pull-one method and
then we'll optimize it to perform better in different circumstances.
use Crypt::Bcrypt;
sub hash-it (*@stuff) {
Seq.new: class :: does Iterator {
has @.stuff;
method pull-one {
@!stuff ?? bcrypt-hash @!stuff.shift, :15rounds
!! IterationEnd
}
}.new: :@stuff
}
my $hashes := hash-it <foo bar ber>;
for $hashes {
say "Fetched value #{++$} {now - INIT now}";
say "\t$_";
}
# OUTPUT:
# Fetched value #1 2.26035863
# $2b$15$ZspycxXAHoiDpK99YuMWqeXUJX4XZ3cNNzTMwhfF8kEudqli.lSIa
# Fetched value #2 4.49311657
# $2b$15$GiqWNgaaVbHABT6yBh7aAec0r5Vwl4AUPYmDqPlac.pK4RPOUNv1K
# Fetched value #3 6.71103435
# $2b$15$zq0mf6Qv3Xv8oIDp686eYeTixCw1aF9/EqpV/bH2SohbbImXRSati
In the above program, we wrapped all the Seq making stuff inside
a sub called hash-it. We slurp all the positional
arguments given to that sub and instantiate a new Seq with an anonymous
class as the Iterator. We use attribute @!stuff to store the stuff we
need to hash. In the .pull-one method we check if we still have
@!stuff to hash; if we do, we shift
a value off @!stuff and hash it, using 15 rounds to make the hashing algo
take some time. Lastly, we added a say statement to measure
how long the program has been running for each iteration, using two now
calls, one of which is run with the
INIT phaser. From the output,
we see it takes about 2.2 seconds to hash a single string.
Skipping breakfast
Using a for loop, is not the only way to use the Seq returned by our
hashing routine. What if some user doesn't care about the first few hashes?
For example, they could write a piece of code like this:
my $hash = hash-it(<foo bar ber>).skip(2).head;
say "Made hash {now - INIT now}";
say bcrypt-match 'ber', $hash;
# OUTPUT:
# Made hash 6.6813790
# True
We've used Crypt::Bcryptmodule's bcrypt-match
routine to ensure the hash we got matches our
third input string and it does, but look at the timing in the output. It took
6.7s to produce that single hash!
In fact, things will look the worse the more items the user tries to skip.
If the user calls our hash-it with a ton of items and then tries to
.skip the first 1,000,000 elements to
get at the 1,000,001st hash, they'll be waiting for about
25 days for that single hash to be produced!!
The reason is our basic Iterator only knows how to .pull-one, so the
skip operation still generates the hashes, just to discard them. Since the
values our Iterator generates do not depend on previous values, we can
implement one of the optimizing methods to skip iterations cheaply:
use Crypt::Bcrypt;
sub hash-it (*@stuff) {
Seq.new: class :: does Iterator {
has @.stuff;
method pull-one {
@!stuff ?? bcrypt-hash @!stuff.shift, :15rounds
!! IterationEnd
}
method skip-one {
return False unless @!stuff;
@!stuff.shift;
True
}
}.new: :@stuff
}
my $hash = hash-it(<foo bar ber>).skip(2).head;
say "Made hash {now - INIT now}";
say bcrypt-match 'ber', $hash;
# OUTPUT:
# Made hash 2.2548012
# True
We added a .skip-one method to our Iterator that instead of hashing a
value, simply discards it. It needs to return a truthy value, if it was
able to skip a value (i.e. we had a value we'd otherwise generate in
.pull-one, but we
skipped it), or falsy value if there weren't any values to skip.
Now, the .skip method called on our Seq uses
our new .skip-one method to cheaply skip through 2 items and then uses
.pull-one to generate the third hash. Look at the timing now: 2.2s; the
time it takes to generate a single hash.
However, we can kick it up a notch. While we won't notice a difference with
our 3-item Seq, that user who was attempting to skip 1,000,000 items won't
get the 2.2s time to generate the 1,000,000th hash. They would also have to wait for 1,000,000 calls to .skip-one and @!stuff.shift. To
optimize skipping over a bunch of items, we can implement the
.skip-at-least method (for brevity, just our Iterator class is shown):
class :: does Iterator {
has @.stuff;
method pull-one {
@!stuff
?? bcrypt-hash( @!stuff.shift, :15rounds )
!! IterationEnd
}
method skip-one {
return False unless @!stuff;
@!stuff.shift;
True
}
method skip-at-least (Int \n) {
n == @!stuff.splice: 0, n
}
}
The .skip-at-least method takes an Int of items to skip. It should
skip as many as it can, and return a truthy value if it was able to skip
that many items, and falsy value if the number of skipped items was fewer.
Now, the user who skips 1,000,000 items will only have to suffer through
a single .splice call.
For the sake of completeness, there's another skipping method defined by
Iterator: .skip-at-least-pull-one. It follows the same semantics as
.skip-at-least, except with .pull-one semantics for return values. Its
default implemention involves just calling those two methods, short-circuiting
and returning IterationEnd if the .skip-at-least returned a falsy value,
and that default implementation is very likely good enough for all
Iterators. The method exists as
a convenience for Iterator users who call methods on Iterators and
(at the moment) it's not used in core Rakudo Perl 6 by any methods that can be
called on users' Seqs.
A so, so count...
There are two more optimization methods—.bool-only
and .count-only—that do not have a default implementation. The first one returns True or False, depending on
whether there are still items that can be generated by the Iterator (True
if yes). The
second one returns the number of items the Iterator can still produce.
Importantly these methods must be able to do that without
exhausting
the Iterator. In other words, after finding these methods implemented,
the user of our Iterator can call them and afterwards should still be
able to .pull-one all of the items, as if the methods were never called.
Let's make an Iterator that will take an Iterable and .rotate it
once per iteration of our Iterator until its tail becomes its head.
Basically, we want this:
.say for rotator 1, 2, 3, 4;
# OUTPUT:
# [2 3 4 1]
# [3 4 1 2]
# [4 1 2 3]
This iterator will serve our purpose to study the two Iterator methods.
For a less "made-up" example, try to find
implementations of iterators for combinations and permutations
routines in Perl 6 compiler's source code.
Here's a sub that creates our Seq with our shiny Iterator along
with some code that operates on it and some timings for different stages of
the program:
sub rotator (*@stuff) {
Seq.new: class :: does Iterator {
has int $!n;
has int $!steps = 1;
has @.stuff is required;
submethod TWEAK { $!n = @!stuff − 1 }
method pull-one {
if $!n-- > 0 {
LEAVE $!steps = 1;
[@!stuff .= rotate: $!steps]
}
else {
IterationEnd
}
}
method skip-one {
$!n > 0 or return False;
$!n--; $!steps++;
True
}
method skip-at-least (Int \n) {
if $!n > all 0, n {
$!steps += n;
$!n −= n;
True
}
else {
$!n = 0;
False
}
}
}.new: stuff => [@stuff]
}
my $rotations := rotator ^5000;
if $rotations {
say "Time after getting Bool: {now - INIT now}";
say "We got $rotations.elems() rotations!";
say "Time after getting count: {now - INIT now}";
say "Fetching last one...";
say "Last one's first 5 elements are: $rotations.tail.head(5)";
say "Time after getting last elem: {now - INIT now}";
}
# OUTPUT:
# Time after getting Bool: 0.0230339
# We got 4999 rotations!
# Time after getting count: 26.04481484
# Fetching last one...
# Last one's first 5 elements are: 4999 0 1 2 3
# Time after getting last elem: 26.0466234
First things first, let's take a look at what we're doing in our Iterator.
We take an Iterable (in the sub call on line 37, we use a Range object out of which we can milk 5000
elements in this case),
shallow-clone it (using [ ... ] operator) and keep that clone in
@!stuff attribute of our Iterator. During object instantiation, we
also save how many items @!stuff has in it into $!n attribute, inside the
TWEAK submethod.
For each .pull-one of the Iterator, we .rotate our @!stuff
attribute, storing the rotated result back in it, as well as making a shallow
clone of it, which is what we return for the iteration.
We also already implemented the .skip-one and .skip-at-least
optimization methods, where we use a private $!steps attribute to alter
how many steps the next .pull-one will .rotate our @!stuff by.
Whenever .pull-one is called, we simply reset $!steps to its default
value of 1 using the LEAVE phaser.
Let's check out how this thing performs! We store
our precious Seq in $rotations variable that we first check for
truthiness, to see if it has any elements in it at all; then we tell the
world how many rotations we can fish out of that Seq; lastly, we fetch
the last element of the Seq and (for screen space reasons) print the
first 5 elements of the last rotation.
All three steps—check .Bool, check .elems, and fetch last item with
.tail are timed, and the results aren't that pretty. While .Bool took
relatively quick to complete, the .elems call took ages (26s)! That's
actually not all of the damage. Recall from
PART I of this series that both .Bool and .elems cache the Seq unless special
methods are implemented in the Iterator. This means that each of those
rotations we made are still there in memory, using up space for nothing! What
are we to do? Let's try implementing those special methods
.Bool and .elems are looking for!
This only thing we need to change is to add two extra methods to our
Iterator
that determinine how many elements we can generate (.count-only)
and whether we have any elements to generate (.bool-only):
method count-only { $!n }
method bool-only { $!n > 0 }
For the sake of completeness, here is our previous example, with these two
methods added to our Iterator:
sub rotator (*@stuff) {
Seq.new: class :: does Iterator {
has int $!n;
has int $!steps = 1;
has @.stuff is required;
submethod TWEAK { $!n = @!stuff − 1 }
method count-only { $!n }
method bool-only { $!n > 0 }
method pull-one {
if $!n-- > 0 {
LEAVE $!steps = 1;
[@!stuff .= rotate: $!steps]
}
else {
IterationEnd
}
}
method skip-one {
$!n > 0 or return False;
$!n--; $!steps++;
True
}
method skip-at-least (\n) {
if $!n > all 0, n {
$!steps += n;
$!n −= n;
True
}
else {
$!n = 0;
False
}
}
}.new: stuff => [@stuff]
}
my $rotations := rotator ^5000;
if $rotations {
say "Time after getting Bool: {now - INIT now}";
say "We got $rotations.elems() rotations!";
say "Time after getting count: {now - INIT now}";
say "Fetching last one...";
say "Last one's first 5 elements are: $rotations.tail.head(5)";
say "Time after getting last elem: {now - INIT now}";
}
# OUTPUT:
# Time after getting Bool: 0.0087576
# We got 4999 rotations!
# Time after getting count: 0.00993624
# Fetching last one...
# Last one's first 5 elements are: 4999 0 1 2 3
# Time after getting last elem: 0.0149863
The code is nearly identical, but look at those sweet, sweet timings! Our
entire program runs about 1,733 times faster because our Seq can figure
out if and how many elements it has without having to iterate or
rotate anything.
The .tail call sees our optimization (side note: that's actually
very recent) and it too
doesn't have to iterate over anything and can just use our .skip-at-least
optimization to skip to the end. And last but not least, our Seq is
no longer being cached, so the only things kept around in memory are the
things we care about. It's a huge win-win-win for very little extra code.
But wait... there's more!
Push it real good...
The Seqs we looked at so far did heavy work: each generated value took
a relatively long time to generate. However, Seqs are quite versatile
and at times you'll find that generation of a value is cheaper than
calling .pull-one and storing that value somewhere. For cases like that,
there're a few more methods we can implement to make our Seq perform
better.
For the next example, we'll stick with the basics. Our Iterator will
generate
a sequence of positive
even numbers up to
the wanted limit. Here's what the call to the sub that makes our Seq looks
like:
say evens-up-to 20; # OUTPUT: (2 4 6 8 10 12 14 16 18)
And here's the all of the code for it. The particular operation we'll be
doing is storing all the values in an Array, by assigning to it:
sub evens-up-to {
Seq.new: class :: does Iterator {
has int $!n = 0;
has int $.limit is required;
method pull-one { ($!n += 2) < $!limit ?? $!n !! IterationEnd }
}.new: :$^limit
}
my @a = evens-up-to 1_700_000;
say now - INIT now; # OUTPUT: 1.00765440
For a limit of 1.7 million, the code takes around a second
to run. However, all we do in our Iterator is add some numbers together,
so a lot of the time is likely lost in .pull-oneing the values and adding
them to the Array, one by one.
In cases like this, implementing a custom .push-all method in our
Iterator can help. The method receives one argument that is a
reification
target. We're pretty close to bare "metal" now, so we can't do anything fancy
with the reification target object other than call .push method on it with a
single value to add to the target. The .push-all always returns
IterationEnd, since it
exhausts the Iterator, so we'll just pop that value right into the
return value of the method's Signature:
sub evens-up-to {
Seq.new: class :: does Iterator {
has int $!n = 0;
has int $.limit is required;
method pull-one {
($!n += 2) < $!limit ?? $!n !! IterationEnd
}
method push-all (\target --> IterationEnd) {
target.push: $!n while ($!n += 2) < $!limit;
}
}.new: :$^limit
}
my @a = evens-up-to 1_700_000;
say now - INIT now; # OUTPUT: 0.91364949
Our program is now 10% faster; not a lot. However, since we're doing all the
work in .push-all now, we no longer need to deal with state inside the
method's body, so we can shave
off a bit of time by using lexical variables instead of accessing object's
attributes all the time. We'll make them use native int types for
even more speed. Also, (at least currently), the += meta operator
is more expensive than a simple assignment and a regular +; since we're
trying to squeeze every last bit of juice here, let's take advantage of
that as well. So what we have now is this:
sub evens-up-to {
Seq.new: class :: does Iterator {
has int $!n = 0;
has int $.limit is required;
method pull-one {
($!n += 2) < $!limit ?? $!n !! IterationEnd
}
method push-all (\target --> IterationEnd) {
my int $limit = $!limit;
my int $n = $!n;
target.push: $n while ($n = $n + 2) < $limit;
$!n = $n;
}
}.new: :$^limit
}
my @a = evens-up-to 1_700_000;
say now - INIT now; # OUTPUT: 0.6688109
There we go. Now our program is 1.5 times faster than the original, thanks to
.push-all. The gain isn't as dramatic as we what saw with other methods,
but can come in quite handy when you need it.
There are a few more .push-* methods
you can implement to, for example, do something special when your Seq
is used in codes like...
for $your-seq -> $a, $b, $c { ... }
...where the Iterator would be asked to .push-exactly three items.
The idea behind them is similar to .push-all: you push stuff onto the
reification target. Their utility and performance gains are ever smaller,
useful only in particular situations, so I won't be covering them.
It's worth noting the .push-all can be used only with Iterators that
are not lazy, since... well... it expects you to push all the items. And
what exactly are lazy Iterators? I'm so glad you asked!
A quick brown fox jumped over the lazy Seq
Let's pare down our previous Seq that generates even numbers down to the
basics. Let's make it generate an infinite list of even numbers, using
an anonymous state variable:
sub evens {
Seq.new: class :: does Iterator {
method pull-one { $ += 2 }
}.new
}
put evens
Since the list is infinite, it'd take us an infinite time to fetch them all.
So what exactly happens when we run the code above? It... quite predictably
hangs when the put routine is called; it sits and patiently waits for
our infinite Seq to complete. The same issue occurs when trying to
assign our seq to a
@-sigiled variable:
my @evens = evens # hangs
Or even when trying to pass our Seq to a sub with a
slurpy parameter_Parameters):
sub meows (*@evens) { say 'Got some evens!' }
meows evens # hangs
That's quite an annoying problem. Fortunately, there's a very easy solution for it. But first, a minor detour to the land of naming clarification!
A rose by any other name would laze as sweet
In Perl 6 some things are or can be made "lazy". While it evokes the
concept of on-demand or "lazy" evaluation, which is ubiquitous in
Perl 6, things that are lazy in Perl 6
aren't just about that. If something is-lazy, it means it always
wants to be evaluated lazily, fetching only as many items as needed, even in
"mostly lazy" Perl 6 constructs that would otherwise eagerly consume even from sources that do on-demand generation.
For example, a sequence of lines read from a file would want to be lazy,
as reading them all in at once has the potential to use up all the RAM.
An infinite sequence would also want to be is-lazy because
an eager evaluation would cause it to hang, as the sequence never
completes.
So a thing that is-lazy in Perl 6 can be thought of as being infinite.
Sometimes it actually will be infinite, but even if it isn't, it being lazy
means it has similar consequences if used eagerly (too much CPU time used, too
much RAM, etc).
Now back to our infinite list of even numbers. It sounds like all we have to
do is make our Seq lazy and we do that by implementing .is-lazy method
on our Iterator that simply returns True:
sub evens {
Seq.new: class :: does Iterator {
method pull-one { $ += 2 }
method is-lazy (--> True) {}
}.new
}
sub meows (*@evens) { say 'Got some evens!' }
put evens; # OUTPUT: ...
my @evens = evens; # doesn't hang
meows evens; # OUTPUT: Got some evens!
The put routine now detects its dealing with something terribly long and
just outputs some dots. Assignment to Array no longer hangs (and will
instead reify on
demand). And the call to a slurpy doesn't hang either and will
also reify on
demand.
There's one more Iterator optimization method left that we should
discuss...
A Sinking Ship
Perl 6 has sink context, similar to "void" context in other languages, which means a value is being discarded:
42;
# OUTPUT:
# WARNINGS for ...:
# Useless use of constant integer 42 in sink context (line 1)
The constant 42 in the above program is in sink context—its value isn't
used by anything—and since it's nearly pointless to have it like that, the
compiler warns about it.
Not all sinkage is bad however and sometimes you may find that gorgeous
Seq on which you worked so hard is ruthlessly being sunk by the user! Let's take
a look at what happens when we sink one of our previous examples, the Seq
that generates up to limit even numbers:
sub evens-up-to {
Seq.new: class :: does Iterator {
has int $!n = 0;
has int $.limit is required;
method pull-one {
($!n += 2) < $!limit ?? $!n !! IterationEnd
}
}.new: :$^limit
}
evens-up-to 5_000_000; # sink our Seq
say now - INIT now; # OUTPUT: 5.87409072
Ouch! Iterating our Seq has no side-effects outside of the Iterator
that it uses, which means it took the program almost six seconds to do
absolutely nothing.
We can remedy the situation by implementing our own .sink-all method.
Its default implementation .pull-ones until the end of the Seq
(since Seqs may have useful side effects), which is not what we want
for our Seq. So let's implement a .sink-all that does nothing!
sub evens-up-to {
Seq.new: class :: does Iterator {
has int $!n = 0;
has int $.limit is required;
method pull-one {
($!n += 2) < $!limit ?? $!n !! IterationEnd
}
method sink-all(--> IterationEnd) {}
}.new: :$^limit
}
evens-up-to 5_000_000; # sink our Seq
say now - INIT now; # OUTPUT: 0.0038638
We added a single line of code and made our program 1,520 times faster—the perfect speed up for a program that does nothing!
However, doing nothing is not the only thing .sink-all is good for. Use
it for clean up that would usually be done at the end of iteration (e.g.
closing a file handle the Iterator was using). Or simply set the state
of the system to what it would be at the end of the iteration (e.g. .seek
a file handle to the end, for sunk Seq that produces lines from it).
Or, as an alternative idea, how about warning the user their code might
contain an error:
sub evens-up-to {
Seq.new: class :: does Iterator {
has int $!n = 0;
has int $.limit is required;
method pull-one {
($!n += 2) < $!limit ?? $!n !! IterationEnd
}
method sink-all(--> IterationEnd) {
warn "Oh noes! Looks like you sunk all the evens!\n"
~ 'Why did you make them in the first place?'
}
}.new: :$^limit
}
evens-up-to 5_000_000; # sink our Seq
# OUTPUT:
# Oh noes! Looks like you sunk all the evens!
# Why did you make them in the first place?
# ...
That concludes our discussion on optimizing your Iterators. Now, let's
talk about using Iterators others have made.
It's a marathon, not a sprint
With all the juicy knowledge about Iterators and Seqs we now
possess, we can probably see how this piece of code manages to work without
hanging, despite being given an infinite Range of numbers:
.say for ^∞ .grep(*.is-prime).map(* ~ ' is a prime number').head: 5;
# OUTPUT:
# 2 is a prime number
# 3 is a prime number
# 5 is a prime number
# 7 is a prime number
# 11 is a prime number
The infinite Range probably is-lazy. That .grep
probably .pull-ones until it finds a prime number. The .map
.pull-ones each of the .grep's values and modifies them, and
.head allows at most 5 values to be .pull-oned from it.
In short what we have here is a pipeline of Seqs and Iterators where
the Iterator of the next Seq is based on the Iterator of the
previous one. For our study purposes, let's cook up a Seq of our own that
combines all of the steps above:
sub first-five-primes (*@numbers) {
Seq.new: class :: does Iterator {
has $.iter;
has int $!produced = 0;
method pull-one {
$!produced++ == 5 and return IterationEnd;
loop {
my $value := $!iter.pull-one;
return IterationEnd if $value =:= IterationEnd;
return "$value is a prime number" if $value.is-prime;
}
}
}.new: iter => @numbers.iterator
}
.say for first-five-primes ^∞;
# OUTPUT:
# 2 is a prime number
# 3 is a prime number
# 5 is a prime number
# 7 is a prime number
# 11 is a prime number
Our sub slurps up_Parameters) its positional arguments and then calls
.iterator method on the @numbers Iterable. This method is available
on all Perl 6 objects and will let us interface with the object using
Iterator methods directly.
We save the @numbers's Iterator in one of the attributes of our
Iterator as well as create another attribute to keep track of how many
items we produced. In the .pull-one method, we first check whether
we already produced the 5 items we need to produce, and if not, we drop into
a loop that calls .pull-one on
the other Iterator, the one we got from @numbers Array.
We recently learned that if the Iterator does not have any more values for
us,
it will return the IterationEnd constant. A constant whose job is to signal the end of
iteration is finicky to deal with, as you can imagine. To detect it, we need to ensure we use the binding (:=), not
the assignment (=) operator, when storing the
value we get from .pull-one in a variable. This is because
pretty much only the container identity (=:=) operator will accept such a monstrosity, so we can't stuff the value
we .pull-one into just any container we please.
In our example program, if we do find that we received
IterationEnd from the source Iterator, we simply return
it to indicate we're done. If not, we repeat the process until we find a prime
number, which we then put into our desired string and that's what we return
from our .pull-one.
All the rest of the Iterator methods we've learned about can be called
on the source Iterator in a similar fashion as we called .pull-one
in our example.
Conclusion
Today, we've learned a whole ton of stuff! We now know that Seqs are
powered by Iterator objects and we can make custom iterators that generate
any variety of values we can dream about.
The most basic Iterator has only .pull-one method that generates
a single value and returns IterationEnd when it has no more values to produce. It's not
permitted to call .pull-one again, once it generated
IterationEnd and we
can write our .pull-one methods with the expectation that will never
happen.
There are plenty of optimization opportunities a custom Iterator can
take advantage of. If it can cheaply skip through items, it can implement
.skip-one or .skip-at-least methods. If it can know how many items
it'll produce, it can implement .bool-only and .count-only methods
that can avoid a ton of work and memory use when only certain values of a
Seq are needed. And for squeezing the very last bit of performance, you
can take advantage of .push-all and other .push-* methods that let you
push values onto the target directly.
When your Iterator .is-lazy, things will treat it with extra care and
won't try to fetch all of the items at once. And we can use the .sink-all
method to avoid work or warn the user of
potential mistakes in their code, when our Seq is being sunk.
Lastly, since we know how to make Iterators and what their methods do,
we can make use of Iterators coming from other sources and call methods
on them directly, manipulating them just how we want to.
We now have all the tools to work with Seq objects in Perl 6. In the
PART III of this series, we'll learn how to compactify all of that knowledge
and skillfully build Seqs with just a line or two of code, using the
sequence operator.
Stay tuned!
-Ofun
Leave a comment