http://www.perlmonks.org?node_id=122728


in reply to Let's get lazy

tilly once posted something that could be coerced to do this.

Update: It would be kind of the inside-out solution to this problem, you could get a function callback for every combination of values, but I think to get an actual iterator function using this method, similar to the iterator produced by my $iter = do { my $i; sub { $i++ } };, you'd need a co-routine. Unless tilly can wrangle it out of his code :-)

There does seem to be a Coroutine Module on CPAN, might be fun to look into :)

Update: Taking the initiative, I wrangled tilly's code myself (just couldn't wait for the book :)

use strict; use warnings; my $iterator = mk_iter( [1..2], ["a".."c"], [3..5] ); while (my @arr = $iterator->()) { print "@arr\n"; } sub mk_iter { my $range = shift; my $i = 0; my $end = @$range; my $iter = sub { return unless $i < $end; return $$range[$i++]; }; @_ ? ret_iter($iter, @_) : $iter; } sub ret_iter { my $iter = shift; my $range = shift; my $i = 0; my $end = @$range; my @arr; my $new_iter = sub { $i = 0 unless $i < $end; return unless $i or @arr = $iter->(); return @arr, $$range[$i++]; }; @_ ? ret_iter($new_iter, @_) : $new_iter; } ##################################### # Update # Here's a variation which is closer to what was # Originally asked for, i.e. all combinations from # 2-4 characters use strict; use warnings; use strict; use warnings; my $iterator = make_iter( 2,4,[qw(A C G N T)] ); while (my @arr = $iterator->()) { print "@arr\n"; } sub make_iter { my ($start, $end, $range) = @_; my $nxt_iter = sub { return }; my $iter = sub { my @data; unless (@data = $nxt_iter->()) { return unless $start <= $end; $nxt_iter = mk_iter( ($range) x $start++); return $nxt_iter->(); }; @data; } }

Replies are listed 'Best First'.
Laziness in a more consistent way
by tilly (Archbishop) on Nov 03, 2001 at 02:50 UTC
    Here is a more general solution to the problem. In this solution my iterators all indicate that they are done by returning an empty list, and when called next will restart. I didn't use i_grep, but I included it to show how you would do it.

    Note that despite the length, the code is more straightforward than the original recursive code. And if this was part of a longer program, you would be reusing the bulk of this code.

    use strict; my $iter = i_map( sub {print "@_\n"}, comb_iter( list_iter(1..2), list_iter('a'..'c'), list_iter(3..5) ) ); 1 while $iter->(); ################################################################### # The program proper ends here. These are utility functions that # # you could reuse # ################################################################### # Takes a list of iterators that are "restartable" # Returns a restartable iterator that iterates over all combinations # of outputs of the input iterators, creating a flat list of combinati +ons # of the inputs. (The output only makes sense in array context.) sub comb_iter { if (0 == @_) { return sub {()}; # Stupid case needed for generality. } elsif (1 == @_) { return shift; } else { my $outer_iter = shift; my $inner_iter = comb_iter(@_); my @last_outer; return sub { if (@last_outer) { my @ret = $inner_iter->(); if (@ret) { return (@last_outer, @ret); } else { @last_outer = $outer_iter->(); if (@last_outer) { return (@last_outer, $inner_iter->()); } else { return (); } } } else { @last_outer = $outer_iter->(); return (@last_outer, $inner_iter->()); } }; } } # Takes a function and an iterator, returns an iterator that uses that # function to filter the output. sub i_grep { my ($filter, $iter) = @_; my @last_ret = qw(just an initialization value); sub { while (@last_ret) { @last_ret = $iter->(); if ($filter->(@last_ret)) { return wantarray ? @last_ret : $last_ret[0]; } } return (); }; } # Takes a function and an iterator, returns an iterator that applies t +hat # function to the returns of the iterator. sub i_map { my ($filter, $iter) = @_; sub { my @ret = $iter->(); return @ret ? $filter->(@ret) : (); }; } # Takes a list and turns it into an iterator over that list sub list_iter { my @vals = @_; my $i = 0; sub { if ($i < @vals) { return $vals[$i++]; } else { $i = 0; return (); } }; }
    Note that the specific problem in the original question can now be solved as the author wanted using i_grep, or you can produce more efficient iterator as follows:
    my $genome_iter = i_map( sub {join '', @_}, join_iter( map { comb_iter( map { list_iter(qw(a c g n t)); } 1..$_ ) } 2..3 ) ); while (my $string = $genome_iter->()) { print "$string\n"; } # Takes a list of iterators, and returns an iterator that iterates # over each in turn sub join_iter { my @iter = @_; my $i = 0; return sub { while ($i < @iter) { my @ret = $iter[$i]->(); if (@ret) { return wantarray ? @ret : $ret[0]; } else { $i++; } } $i = 0; return (); }; }
    Alternately if you want to turn the output into a list you can just create an easy method:
    # Takes an iterator and returns a list of results sub iter2list { my $iter = shift; my @out; while (my @ret = $iter->()) { push @out, @ret; } return @out; }
    Note that most of the length here is because I am having to build my iterator interface from scratch. That is a lot of work! And some of the code looks more complex because what we are used to seeing in a few nested loops our minds balk at when you see it as a similar number of nested calls.
Re (tilly) 2: Let's get lazy
by tilly (Archbishop) on Nov 02, 2001 at 17:07 UTC
    A much better person to ask is Dominus, he is in the process of writing a book on exactly this subject.

    Chapter 4 in particular offers direct iterative solutions of the above problem. Once you know the techniques, they are straightforward to apply in any language with proper support for closures. And the techniques are essentially to create utility functions that take one iterator and create new ones out of it. For instance if you like doing stuff procedurally with map, grep, an easy way to produce a range etc, then you can create iterative versions of the same. (For instance a range would return all of the things in that range. An iterative map would take a function and an iterator and give you an iterator that is the result of applying that function to the output of the first iterator.) And then it becomes a mechanical process to write iterative versions of what you can dream up in a list-oriented manner.

    Alternately if you want the flavour of a co-routine solution to the problem, and don't want to wait for Perl 6, Ruby offers them now. So, I believe, does the very latest version of Python. (Ruby goes further and people there use them more often.) They don't offer the rest of Perl 6, but they give you the flavour of a Perlish scripting language with yield and (with Ruby or stackless Python) full continuations.