Iterator::Simple
2023-03-18 perl Iterator::Simple Iterator Higher-Order Perl Advent of CodeSince I read the Higher-Order Perl by Mark Jason Dominus I became big fan of iterator approach. As shown in few previous posts like Date range or BST Successor Search, I am usually using Iterator::Simple to build iterators.
The iterator itself is just anonymous function that returns next item, until it returns undef
when iteration is exhausted. There are many ways to build an iterator, but probably simplest is to write such function directly. Here is an example to build a range of numbers
use Iterator::Simple qw(iterator);
sub range {
my ($from, $to, $step) = @_;
$step //= 1;
my $i = $from - $step;
iterator {
$i += $step;
return if $i > $to;
return $i;
}
}
Usage of such iterator is easy, just create an instance using range
function and iterate using next
method it provides
my $up_to_ten = range(1,10);
while(defined(my $n = $up_to_ten->next)) {
say $n;
}
Other means to create an iterator is via iter
function. You can check with is_iterable
whether it can create iterator from what was specified. It supports file handles, array references, objects with next
method or __iter__
method. For example, here are iterators from opened file handle and testing iterator from array reference
use Iterator::Simple qw(iter);
use Path::Class qw(file);
my $input_file = shift;
my $lines = iter(file($input_file)->openr); # iterator from opened file handle
my $test = iter([ # iterator from array reference
'$ cd /',
'$ ls',
'dir a',
'14848514 b.txt',
'8504156 c.dat',
]);
Since the iterator is reference to anonymous function, it is very cheap to pass it around as a parameter into a function. The Iterator::Simple
module also provides number of helpers that can work with them.
imap
- transformation for each item
my $passphrases = imap { chomp; $_ } iter(\*DATA);
igrep
- select items that match a condition
my $filtered = igrep { $_ % 4 == 0 } $it;
ichain
- chains multiple iterators into oneienumerate
- creates iterator that produces array reference pairs of index and the itemizip
- puts together first items from all iterators, then second items, etcislice
,ihead
,iskip
- allows to skip/get specified number of itemslist
- turns iterator into array reference. Obviously works only for finite iterators
Last example comes from Dueling Generators of 2017 Advent Of Code. The generator
function creates infinite iterator of the values and matches
function implements the “judge” to count matching pairs. Note how easy it was for second part to add filtering of values via igrep
and rest of the program stayed the same.
use Function::Parameters;
use Iterator::Simple qw(iterator ihead list izip imap igrep);
use Data::Dump;
use Test::More;
# part 1
{
my $a = generator(init => 699, factor => 16807);
my $b = generator(init => 124, factor => 48271);
is matches(a => $a, b => $b, samples => 40_000_000), 600, 'Input for part 1 matches';
}
# part 2
{
my $a = igrep { $_ % 4 == 0 } generator(init => 699, factor => 16807);
my $b = igrep { $_ % 8 == 0 } generator(init => 124, factor => 48271);
is matches(a => $a, b => $b, samples => 5_000_000), 313, 'Input for part 2 matches';
}
done_testing;
fun matches(:$a, :$b, :$samples) {
my $judge = imap { ($_->[0]&0xFFFF) == ($_->[1]&0xFFFF) ? 'x' : '-' } izip $a, $b;
my $matches_in_first_few = igrep { $_ eq 'x' } ihead($samples, $judge);
my $count = 0;
$count++ while $matches_in_first_few->next;
return $count;
}
fun generator(:$init, :$factor) {
my $prev = $init;
return iterator {
$prev = ($prev * $factor) % 2147483647;
return $prev;
}
}