This is a chapter in Perl New Features, a book from Perl School that you can buy on LeanPub or Amazon. Your support helps me to produce more content.
This feature was promoted to a stable version in v5.40.
Perl v5.36 adds experimental support that allows a foreach
(or for
) to loop iterate over multiple values at the same time by specifying multiple control variables. This is incredibly cool:
use v5.36; use experimental qw(for_list); my @animals = qw( Buster Mimi Ginger Nikki ); foreach my( $s, $t ) ( @animals ) { say "$s ^^^ $t"; }
The output shows two iterations of the loop, each which grabbed two values from the list:
Buster ^^^ Mimi Ginger ^^^ Nikki
Add another parameter; the list now doesn’t divide evenly between the parameters, so any parameter that can’t match with a list item gets undef
, just like normal list assignment:
use v5.36; use experimental qw(for_list); foreach my( $s, $t, $u ) ( @animals ) { say "$s ^^^ $t ^^^ $u"; }
Since use v5.36
also turns on warnings, you get those “uninitialized” warnings for free when you use those undef
values:
Buster ^^^ Mimi ^^^ Ginger Nikki ^^^ ^^^ Use of uninitialized value ... Use of uninitialized value ...
Another interesting use combines the new builtin::indexed
feature that gets you the index and value at the same time:
use v5.36; use experimental qw(for_list builtin); use builtin qw(indexed); my @animals = qw( Buster Mimi Ginger Nikki ); foreach my( $i, $value ) ( indexed(@animals) ) { say "$i: $value"; }
That’s a bit nicer than going through the indices to access the value in an additional statement:
foreach my $i ( 0 .. $#animals ) { my $value = $animals[$i]; say "$i: $value"; }
No placeholders (yet)
So far, this new syntax doesn’t have a way to skip values. In a normal list assignment, you discard a value coming from the right hand list with a literal undef
:
my( $s, undef, $t ) = @animals
Try that in the for
list and you get a syntax error:
foreach my( $s, undef, $u ) ( @animals ) { # ERROR! say "$s ^^^ $u"; }
Hash keys and values
I’m tempted to use this for hashes, although each
inside a while
is still probably better since it doesn’t have to build the entire input list in one go:
use experimental qw(for_list); my %animals = ( cats => [ qw( Buster Mimi Ginger ) ], dogs => [ qw( Nikki ) ], ); foreach my( $k, $v ) ( %animals ) { say "$k ^^^ @$v"; }
Since those hash values are array refs, it would be helpful if this feature could use the refaliasing
and declared_refs
features (Mix assignment and reference aliasing with declared_refs):
use experimental qw(for_list); use experimental qw(refaliasing declared_refs); my %animals = ( cats => [ qw( Buster Mimi Ginger ) ], dogs => [ qw( Nikki ) ], ); foreach my( $k, \@v ) ( %animals ) { say "$k ^^^ @v"; }
Sadly, the parser doesn’t expect the reference operator inside that for
list:
syntax error ... near ", \"
Doing
Prior to builtin multiple iteration, the best way to do the same thing was probably the List::MoreUtils (not part of core) module. The natatime
function, which I wished was named n_at_a_time
, grabs the number of elements that you specify and returns them as a list. Since it returns a list instead of an array reference, it’s easier to use it with a while
:
use List::MoreUtils qw(natatime); my @x = ('a' .. 'g'); my $iterator = natatime 3, @x; while( my @vals = $iterator->() ) { print "@vals\n"; }
Another approach uses splice
. The easiest thing might be to do it destructively since that requires no index fiddling:
my @x = 'a' .. 'g'; my @temp = @x; while( my @vals = splice @temp, 0, 3, () ) { print "@vals\n"; }
Here’s an example from the L
sub nary_print { my $n = shift; while (my @next_n = splice @_, 0, $n) { say join q{ -- }, @next_n; } } nary_print(3, qw(a b c d e f g h)); # prints: # a -- b -- c # d -- e -- f # g -- h
Playing with the array indices can get this done, but it comes with a lot of baggage. First, an array slice doesn’t return an empty list, so you can’t use that as a condition in the while
as in the previous examples. Since it fills in the missing elements with undef
, outputting the values possibly comes with warnings. Even if you want to accept those annoyances, you still have to manage the end of array condition ($#X
) yourself:
my @x = 'a' .. 'g'; my $start = 0; my $n = 3; while( $start <= $#x ) { no warnings qw(uninitialized); my @vals = @x[$start, $start + $n - 1]; print "@vals\n"; $start += $n; }
So yeah, having a multiple iterator feature built into Perl is a huge win.
Summary
The experimental for_list
feature lets you take multiple elements of the list in each iteration. This doesn't yet handle many of the list assignment features that would make this as useful as people will want it to be.