How does lazyness of the slice index affects the slicing of an array/list? [RAKU]

119 views Asked by At

When we slice an array with an index that exceeds the boundaries of the array we get as the result the undefined (Any)

When we pass the same slice index as a lazy list then we get as result the existing values of the array/list (and NOT any more than that):

my @a = ^5;

say @a[^10];        # (0 1 2 3 4 (Any) (Any) (Any) (Any) (Any))
say @a[lazy ^10];   # (0 1 2 3 4)

It is clear that lazyness of the slice index affects the result.

Trying to undestand the way things are and as a proof of concept I programmed my simple version of the slice mechanism:

my @a = ^5;

my @s1 = ^10;
my @s2 = lazy ^10;

sub postcircumfix:<-[ ]-> (@container, @index) {
    my $iter = @index.iterator;

    gather {
        loop {
            my $item := $iter.pull-one;

            if $item =:= IterationEnd {
                last;
            }

            with @container[$item] {
                take @container[$item]
            } else {
                @index.is-lazy ?? { last } !! take @container[$item];
            }
        }
    }
}

say @a-[@s1]-;   # (0 1 2 3 4 (Any) (Any) (Any) (Any) (Any))
say @a-[@s2]-;   # (0 1 2 3 4)

But I am wondering if my naive algorithm depicts the way that things are computed under the hood !

1

There are 1 answers

2
user0721090601 On BEST ANSWER

The source for how things are done under the hood can be found in array_slice.pm6.

Specifically, you can see the following at L73:

    if is-pos-lazy {
        # With lazy indices, we truncate at the first one that fails to exists.
        my \rest-seq = Seq.new(pos-iter).flatmap: -> Int() $i {
            nqp::unless(
              $eagerize($i),
              last,
              $i
            )
        };
        my \todo := nqp::create(List::Reifier);
        nqp::bindattr(todo, List::Reifier, '$!reified', eager-indices);
        nqp::bindattr(todo, List::Reifier, '$!current-iter', rest-seq.iterator);
        nqp::bindattr(todo, List::Reifier, '$!reification-target', eager-indices);
        nqp::bindattr(pos-list, List, '$!todo', todo);
    }
    else {
        pos-iter.push-all: target;
    }

So, as you've surmised, it does indeed stop after a list item doesn't exist. This is no doubt becaue many lazy lists are infinite, and iterators don't provide a way to know if they are infinite or not (the generator may be non-determinative).

If you really want to enable such a thing, you could, for instance, write your own slicer that handles lazy lists where an element may not be available, but you have to take care to ensure that things are only eagerly evaluated if you know they're finite:

multi sub postcircumfix:<-[ ]-> (@a, @b) {
  lazy gather {
    take @a[$_] for @b;
  }
}

my @a = ^5;
my @b = lazy gather { 
  for ^10 -> $i { 
    # So we can track when elements are evaluated
    say "Generated \@b[$i]"; 
    take $i;
  } 
};

say "Are we lazy? ", @a-[@b]-;
say "Let's get eager: ", @a-[@b]-.eager;
say "Going beyond indices: ", @a-[@b]-[11]

The output of this is

Are we lazy? (...)
Generated @b[0]
Generated @b[1]
Generated @b[2]
Generated @b[3]
Generated @b[4]
Generated @b[5]
Generated @b[6]
Generated @b[7]
Generated @b[8]
Generated @b[9]
Let's get eager: (0 1 2 3 4 (Any) (Any) (Any) (Any) (Any))
Going beyond indices: Nil