p5-list-mapmulti / lib / List / MapMulti.pm

package List::MapMulti;

use 5.006;
use strict;
use warnings;
no warnings qw/once void/;

BEGIN
{
	$List::MapMulti::AUTHORITY = 'cpan:TOBYINK';
	$List::MapMulti::VERSION   = '0.002';
	
	# use this module if it's installed.
	# don't panic if it's unavailable.
	eval {
		require autovivification;
		autovivification->unimport('warn');
	};
}

use Carp qw/carp croak/;

our (@EXPORT, @EXPORT_OK, %EXPORT_TAGS);
use base qw/Exporter/;
BEGIN {
	@EXPORT      = qw/mapm/;
	@EXPORT_OK   = (@EXPORT, qw/map_multi iterator_multi/);
	%EXPORT_TAGS = (
		'all'      => \@EXPORT_OK,
		'standard' => \@EXPORT,
		'default'  => \@EXPORT,
		'nothing'  => [],
		);
}

sub iterator_multi
{
	join(q{::}, __PACKAGE__, 'Iterator')->new(@_);
}

sub map_multi (&@)
{
	my ($code, @arrays) = @_;
	my @results;
	
	if (@arrays)
	{
		my $iter = iterator_multi(@arrays);
		local $_ = $iter;
		
		while (my @values = $iter->())
		{
			#$_ = \@values;
			push @results, $code->(@values);
		}
	}
	
	wantarray ? @results : scalar(@results);
}

sub mapm (&@); *mapm = \&map_multi;

package List::MapMulti::Iterator;

use strict;
use warnings;
no warnings qw/once void/;

use Carp qw/carp croak/;

use overload
	'&{}' => sub { my $self = shift; sub { $self->next } },
	'@{}' => sub { my $self = shift; [ $self->current ] },
	;

BEGIN
{
	$List::MapMulti::Iterator::AUTHORITY = 'cpan:TOBYINK';
	$List::MapMulti::Iterator::VERSION   = '0.002';
	
	autovivification->unimport('warn');
}

sub new
{
	my ($class, @arrays) = @_;
	
	_array_check(\@arrays);
	
	bless {
		arrays          => \@arrays,
		lengths         => [ map { ;scalar @$_ } @arrays ],
		next_indices    => [ map { ;0 } @arrays ],
		current_indices => undef,
		last            => 0,
	}, $class;
}

sub _array_check
{
	my ($arrays) = @_;
	my $callsub  = [caller(1)]->[3];

	if (warnings::enabled('misc'))
	{
		carp "no arrayrefs were passed to $callsub"
			unless @$arrays;
	}
	
	croak "non-arrayref passed to $callsub"
		if grep { ref ne 'ARRAY' } @$arrays;
}

sub _increment_indices
{
	my ($indices, $lengths) = @_;
	my $inc = $#$indices;
	
	while (1)
	{
		if ($inc < 0)
		{
			@$indices = ();
			return;
		}
		
		$indices->[$inc]++;
		if ($indices->[$inc] >= $lengths->[$inc])
		{
			$indices->[$inc] = 0;
			$inc--;
		}
		else
		{
			return $indices;
		}
	}
}

sub next
{
	my $self = shift;
	
	return if $self->{last};
	
	$self->{current_indices} = [ $self->next_indices ];
	
	my @values = map
		{ $self->{arrays}[$_][$self->{current_indices}[$_]] }
		0 .. $#{$self->{arrays}};
		
	$self->{last} = !_increment_indices($self->{next_indices}, $self->{lengths});
	
	return @values;
}

sub next_indices
{
	my $self = shift;
	
	if (@_)
	{
		$self->{next_indices} = [@_[ 0 .. $#{$self->{arrays}} ]];
	}
	
	@{ $self->{next_indices} };
}

sub current
{
	my $self = shift;
	
	if (@_)
	{
		my @ix = $self->current_indices;
		for my $i (0 .. $#_)
		{
			$self->{arrays}[$i][ $ix[$i] ] = $_[$i];
		}
	}
	
	my @values = map
		{ $self->{arrays}[$_][$self->{current_indices}[$_]] }
		0 .. $#{$self->{arrays}};
}

sub current_indices
{
	@{ (shift)->{current_indices} };
}

__PACKAGE__
__END__

=head1 NAME

List::MapMulti - map through multiple arrays at once

=head1 SYNOPSIS

 use feature qw/say/;
 use List::MapMulti qw/mapm/;
 
 my @numbers = (2..10, qw/Jack Queen King Ace/);
 my @suits   = qw/Clubs Diamonds Hearts Spades/;
 my @cards   = mapm { "$_[0] of $_[1]" } \@numbers, \@suits;
 
 say scalar(@cards);     # says '52'
 say $cards[0];          # says '2 of Clubs'
 say $cards[1];          # says '2 of Diamonds'
 say $cards[-1];         # says 'Ace of Spades'

=head1 DESCRIPTION

List::MapMulti provides shortcuts for looping through several lists in a
nested fashion. Think about all the times you've needed to do something
like:

 foreach my $x (@exes) {
   foreach my $y (@whys) {
     # do something with $x and $y
   }
 }

There are two different solutions available to you: C<map_multi> (which
has an alias C<mapm>) and C<iterator_multi>.

The only thing this module exports by default is C<mapm>.

=head2 C<< map_multi { BLOCK } \@list1, \@list2 ... >>

=begin private

=item C<mapm> - for pod::coverage

=end private

(Or C<mapm>!)

Calls the codeblock with every possible combination of values from each
list. If you imagine it as calling within a set of nested loops, then the
final list is the innermost loop; and the first loop is the outermost
loop.

Note that within the codeblock, the items from each list are available
as C<< $_[0] >>, C<< $_[1] >>, etc. The C<< $_ >> variable is set to a
List::MapMulti::Iterator object which is used internally by C<map_multi>.

C<mapm> is exported by default, but C<map_multi> needs to be requested
explicitly.

=head2 C<< iterator_multi(\@list1, \@list2, ...) >>

This allows constructions like this:

 my $iterator = iterator_multi(\@numbers, \@suits);
 while (my ($number, $suit) = $iterator->())
 {
   say "$number of $suit";
 }

Although C<map_multi> is arguably a nicer syntax, the iterator provides
you with an important advantage: you don't have to iterate through every
possible combination. You can control flow using, say, C<next>, C<last>
or C<redo>.

=head2 List::MapMulti::Iterator

This is advanced fu that you probably don't need to know about.

While iterators act like coderefs (you get the next set of values via
C<< $iterator->() >>), internally they are blessed objects that overload
C<< &{} >>. As they are objects, they are able to provide some methods.

These are the methods they provide:

=head3 C<< new(\@list1, \@list2, ...) >>

Constructor. The C<iterator_multi> function is just a shortcut for this.

=head3 C<< next >>

Calling C<< $iterator->next >> is exactly equivalent to calling
C<< $iterator->() >>.

=head3 C<< current >>

Returns the same thing as the previous call to C<< next >> (unless
the original arrays have changed since then).

This can also be used as a setter, in which case it writes back to
the appropriate slots in the original arrays. 

=head3 C<< next_indices >>

Returns the array indices that will be used to read from the
original arrays next time C<next> is called. Again, this can be
used as a setter.

=head3 C<< current_indices >>

Returns the array indices that was used to read from the original
arrays last time C<next> was called.

=head1 BUGS

Please report any bugs to
L<http://rt.cpan.org/Dist/Display.html?Queue=List-MapMulti>.

=head1 SEE ALSO

L<List::Util>,
L<List::MoreUtils>,
L<List::Pairwise>.

=head1 AUTHOR

Toby Inkster E<lt>tobyink@cpan.orgE<gt>.

=head1 COPYRIGHT AND LICENCE

This software is copyright (c) 2012 by Toby Inkster.

This is free software; you can redistribute it and/or modify it under
the same terms as the Perl 5 programming language system itself.

=head1 DISCLAIMER OF WARRANTIES

THIS PACKAGE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR IMPLIED
WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF
MERCHANTIBILITY AND FITNESS FOR A PARTICULAR PURPOSE.
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.