How to build lazy lists with defined generators and is there a "takeWhile" alternative?

389 views Asked by At

I am reading through perl6intro on lazy lists and it leaves me confused about certain things.

Take this example:

sub foo($x) {
  $x**2
}

my $alist = (1,2, &foo ... ^ * > 100);

will give me (1 2 4 16 256), it will square the same number until it exceeds 100. I want this to give me (1 4 9 16 25 .. ), so instead of squaring the same number, to advance a number x by 1 (or another given "step"), foo x, and so on.

Is it possible to achieve this in this specific case?

Another question I have on lazy lists is the following: In Haskell, there is a takeWhile function, does something similar exist in Perl6?

3

There are 3 answers

2
Brad Gilbert On BEST ANSWER

Here is how you could write a Perl 6 equivalent of Haskell's takewhile.

sub take-while ( &condition, Iterable \sequence ){
  my \iterator = sequence.iterator;

  my \generator = gather loop {
    my \value = iterator.pull-one;
    last if value =:= IterationEnd or !condition(value);
    take value;
  }

  # should propagate the laziness of the sequence
  sequence.is-lazy
  ?? generator.lazy
  !! generator
}

I should probably also show an implementation of dropwhile.

sub drop-while ( &condition, Iterable \sequence ){
  my \iterator = sequence.iterator;

  GATHER: my \generator = gather {

    # drop initial values
    loop {
      my \value = iterator.pull-one;

      # if the iterator is out of values, stop everything
      last GATHER if value =:= IterationEnd;

      unless condition(value) {
        # need to take this so it doesn't get lost
        take value;

        # continue onto next loop
        last;
      }
    }

    # take everything else
    loop {
      my \value = iterator.pull-one;
      last if value =:= IterationEnd;
      take value
    }
  }

  sequence.is-lazy
  ?? generator.lazy
  !! generator
}

These are only just-get-it-working examples.

It could be argued that these are worth adding as methods to lists/iterables.

You could (but probably shouldn't) implement these with the sequence generator syntax.

sub take-while ( &condition, Iterable \sequence ){
  my \iterator = sequence.iterator;
  my \generator = { iterator.pull-one } …^ { !condition $_ }
  sequence.is-lazy ?? generator.lazy !! generator
}
sub drop-while ( &condition, Iterable \sequence ){
  my \end-condition = sequence.is-lazy ?? * !! { False };
  my \iterator = sequence.iterator;

  my $first;
  loop {
    $first := iterator.pull-one;
    last if $first =:= IterationEnd;
    last unless condition($first);
  }

  # I could have shoved the loop above into a do block
  # and placed it where 「$first」 is below

  $first, { iterator.pull-one } … end-condition
}

If they were added to Perl 6/Rakudo, they would likely be implemented with Iterator classes.
( I might just go and add them. )


A direct implementation of what you are asking for is something like:

do {
  my $x = 0;
  { (++$x)² } …^ * > 100
}

Which can be done with state variables:

{ ( ++(state $x = 0) )² } …^ * > 100

And a state variable that isn't used outside of declaring it doesn't need a name.
( A scalar variable starts out as an undefined Any, which becomes 0 in a numeric context )

{ (++( $ ))² } …^ * > 100
{ (++$)² } …^ * > 100

If you need to initialize the anonymous state variable, you can use the defined-or operator // combined with the equal meta-operator =.

{ (++( $ //= 5))² } …^ * > 100

In some simple cases you don't have to tell the sequence generator how to calculate the next values.
In such cases the ending condition can also be simplified.

say 1,2,4 ...^ 100
# (1 2 4 8 16 32 64)

The only other time you can safely simplify the ending condition is if you know that it will stop on the value.

say 1, { $_ * 2 } ... 64;
# (1 2 4 8 16 32 64)

say 1, { $_ * 2 } ... 3;
# (1 2 4 8 16 32 64 128 256 512 ...)
0
smls On

I want this to give me (1 4 9 16 25 .. )

The easiest way to get that sequence, would be:

my @a = (1..*).map(* ** 2);  # using a Whatever-expression
my @a = (1..*).map(&foo);    # using your `foo` function

...or if you prefer to write it in a way that resembles a Haskell/Python list comprehension:

my @a = ($_ ** 2 for 1..*);  # using an in-line expression
my @a = (foo $_ for 1..*);   # using your `foo` function

While it is possible to go out of one's way to express this sequence via the ... operator (as Brad Gilbert's answer and raiph's answer demonstrate), it doesn't really make sense, as the purpose of that operator is to generate sequences where each element is derived from the previous element(s) using a consistent rule.

Use the best tool for each job:

  • If a sequence is easiest to express iteratively (e.g. Fibonacci sequence):
    Use the ... operator.

  • If a sequence is easiest to express as a closed formula (e.g. sequence of squares):
    Use map or for.

1
raiph On

I want this to give me (1 4 9 16 25 .. )

my @alist = {(++$)²} ... Inf;
say @alist[^10]; # (1 4 9 16 25 36 49 64 81 100)

The {…} is an arbitrary block of code. It is invoked for each value of a sequence when used as the LHS of the ... sequence operator.

The (…)² evaluates to the square of the expression inside the parens. (I could have written (…) ** 2 to mean the same thing.)

The ++$ returns 1, 2, 3, 4, 5, 6 … by combining a pre-increment ++ (add one) with a $ variable.

In Haskell, there is a takeWhile function, does something similar exist in Perl6?

Replace the Inf from the above sequence with the desired end condition:

my @alist = {(++$)²} ... * > 70;    # stop at step that goes past 70
say @alist; # [1 4 9 16 25 36 49 64 81]

my @alist = {(++$)²} ...^ * > 70;   # stop at step before step past 70
say @alist; # [1 4 9 16 25 36 49 64]

Note how the ... and ...^ variants of the sequence operator provide the two variations on the stop condition. I note in your original question you have ... ^ * > 70, not ...^ * > 70. Because the ^ in the latter is detached from the ... it has a different meaning. See Brad's comment.