Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Making a Sequence from an Iterator

Tags:

raku

I'm trying to make an iterator, then build a sequence from it, but it doesn't act the way I think it should. What's up?

Here's my basic class:

class Foo {
    has $.x = 0;
    has $.max = 3;

    method val() {
        (++$!x > $!max) ?? () !! ($!x, "string $!x")
    }
}

my $foo = Foo.new;
say $foo.val.perl for ^4;

# (1, "string 1")
# (2, "string 2")
# (3, "string 3")
# ()

It just iterates until max, then returns (), works the way I think it should.

Then I build an Iterator from that, just a single pull-one() method.

class Foo-Iterator does Iterator {
    has Foo $.foo;

    method pull-one() {
        $!foo.val || IterationEnd
    }
}

my $iter = Foo-Iterator.new(foo => Foo.new);
$iter.pull-one.perl.say for ^4;

# (1, "string 1")
# (2, "string 2")
# (3, "string 3")
# IterationEnd

It still acts the way I expect it to.

If I access it with a Seq, it still works fine:

.perl.say for Seq.new: Foo-Iterator.new(foo => Foo.new);
# (1, "string 1")
# (2, "string 2")
# (3, "string 3")

That is still what I expect to see, the same thing the Iterator returned.

Finally, I store the Seq in an @ variable and print the results of that:

my @seq = Seq.new: Foo-Iterator.new(foo => Foo.new);
.perl.say for @seq;
# $(4, "string 1")
# $(4, "string 2")
# $(4, "string 3")

What's up with that? It is seems to be using the later value of the variable rather than the value it had at the time of the pull-one call() (which the string forces to be a value). Does a Seq return it as a container rather than a value? Is this laziness in action, where it doesn't pull until requested so it gets a later value?

If I make val() return +$!x instead of returning $!x, it seems to grab the value and give me what I want, I'm just trying to understand the behavior I see.

like image 511
Curt Tilmes Avatar asked Jan 16 '19 01:01

Curt Tilmes


1 Answers

I made precisely 4 changes to Foo: does Iterator, pull-one, IterationEnd, and I decont %!x with <>.

Your method was passing the container for $!x when you really want to pass the value inside of the container, so it needed $!x<>.
The reason you didn't notice it with the rest of the code is that the array assignment was the only one that was eager.

I did the rest of the changes because it already has state, and it only works once. Which is exactly how an Iterator should work. With your code it makes zero sense to add another object for an Iterator that basically only renames a method.

class Foo does Iterator {
    #     ^-----------^
    has $.x = 0;
    has $.max = 3;

    method pull-one() {
        #  ^------^

        (++$!x > $!max) ?? IterationEnd !! ($!x<>, "string $!x")
        #                  ^----------^        ^^
    }
}

Now to use it

my $seq = Seq.new( Foo.new );

for $seq<> { .say }
# (1 string 1)
# (2 string 2)
# (3 string 3)

my @seq = Seq.new( Foo.new );
for @seq { .say }
# (1 string 1)
# (2 string 2)
# (3 string 3)

Assuming that your example is entirely too simple, and there is a good reason for having a separate Iterator, why do you have a mutating val method?

class Foo-Iterator {…}

class Foo does Iterable {
    has $.max = 3;

    method val ( $index ) {
        ($index > $!max) ?? () !! ($index, "string $index")
    }

    method iterator (){
        # pass `self` for method calls or such
        # (could pass internal data additionally/instead)
        Foo-Iterator.new( :foo(self) )
    }
}

class Foo-Iterator does Iterator {
    # the mutating value
    has $!x = 0;

    # make it public only so we don't have
    # to mess with `new` or `BUILD`
    has $.foo is required;

    # only have the mutating logic in this object
    method pull-one() {
        $!foo.val( ++$!x ) || IterationEnd
    }
}

Now to use it.

# only one Foo object needed
my $foo = Foo.new;

# for Seq.new($foo.iterator) { .say }
for $foo<> { .say }
# (1 string 1)
# (2 string 2)
# (3 string 3)

for $foo<> { .say }
# (1 string 1)
# (2 string 2)
# (3 string 3)

my $iter-a = $foo.iterator;
my $iter-b = $foo.iterator;

say $iter-a.pull-one;
# (1 string 1)
say $iter-a.pull-one;
# (2 string 2)
say $iter-b.pull-one; # notice that $iter-b isn't tied to $iter-a
# (1 string 1)

my @seq = $foo<>;
for @seq { .say }
# (1 string 1)
# (2 string 2)
# (3 string 3)
like image 164
Brad Gilbert Avatar answered Jan 03 '23 12:01

Brad Gilbert