Strange behaviour of eager take
Holli has accepted the answer that quoted two of Brad's comments. I deliberately kept that one terse.
But you're reading this.1 So I'll go in the opposite direction with this answer.
($bar,...)
is a list containing $bar
, not $bar
's value
The behavior shown in the question really is all about containers vs values, and how list literals store containers, not their values1:
my $bar = 0;
my $list = ($bar,);
say $list[0]; # 0
$bar = 1;
say $list[0]; # 1
The laziness vs eager aspect is just things doing what they're all supposed to do. Emphasizing this first point may be enough to inspire you to focus on containers vs values. And maybe that'll lead you to quickly understand what went wrong in Holli's code. But maybe not.1 So I'll continue.
What happens in the lazy case?
A lazy list waits until a value is demanded before attempting to produce it. Then, it just does the necessary work and pauses, yielding control, until it's asked to produce another value at some later time.
In Holli's code the for
loop demands values.
The first time around the for
loop, it demands a value from the lazy
expression. This turns around and demands a value from the gather
'd expression. The latter then computes until the take
, by which time it's created a list whose first element is the container $bar
. This list is the result of the take
.
Then the .print
prints that first list. At the moment of printing, $bar
still contains 0
. (The first increment of $bar
hasn't yet happened.)
The second time around the for
loop, the inner control structure enclosing the take
(the loop
) is re-entered. The first thing that happens is that $bar
gets incremented for the first time. Then the loop exit condition is checked (and fails), so the second time around the loop starts. Another list is created. Then it's take
d.
When the second list is printed, its first element, which is the $bar
container, prints as 1
, not 0
, because at that point, having been incremented, $bar
now contains 1
.
(If Holli had written code that held onto the first list, and printed that first list again now, after having just printed the second list, they'd have discovered that the first list also now printed with a 1
, no longer a 0
. Because all the take
d lists have the same $bar
container as their first element.)
And likewise the third list.
After the third list has been printed, the for
loop demands a fourth go at the gather
. This re-enters the loop
at the statement after the take
statement. $bar
gets incremented for the third time, to 3
, and then the last if $bar > 2;
condition triggers, exiting the loop (and thus the expression being gather
'd and ultimately the whole .print for ...
statement).
What happens in the eager case?
All the gather
ing is completed before any of the print
ing.
At the end of this, the for
construct has a sequence of three lists. It has not yet called any .print
calls. The third time around the loop
in the gather
has left $bar
containing 3
.
Next, .print
is called on each of the three lists. $bar
contains 3
so they all print with 3
as their first element.
Solving the problem by switching to an array
I think the idiomatic way to deal with this would be to switch from a list literal to an array literal:
[$bar, @bar[$bar]]
# instead of
($bar, @bar[$bar])
This works because, unlike a list literal, an array literal treats an element that's a container as an r-value2, i.e. it copies the value contained in the container out of that container, rather than storing the container itself.
It just so happens that the value is copied into another new Scalar
container. (That's because all elements of new non-native arrays are fresh Scalar
containers; this one of the main things that makes an array different from a list.) But the effect in this context is the same as if the value were copied directly into the array because it no longer matters that the value contained in $bar
is changing as things proceed.
The upshot is that the first element of the three arrays ends up containing, respectively, 0
, 1
, and 2
, the three values that were contained in $bar
at the time each array was instantiated.
Solving the problem by switching to an expression
As Holli noted, writing $bar + 0
also worked.
In fact any expression will do, so long as it isn't just $bar
on its own.
Of course, the expression needs to work, and return the right value. I think $bar.self
should work and return the right value no matter what value $bar
is bound to or assigned.
(Though it does read a little strangely; $bar.self
is not $bar
itself if $bar
is bound to a Scalar
container! Indeed, in an even more counter-intuitive twist, even $bar.VAR
, which uses .VAR
, a method which "Returns the underlying Scalar
object, if there is one.", still ends up being treated as an r-value instead!)
Does the doc need updating?
The above is an entirely logical consequence of:
What
Scalar
s are;What list literals do with
Scalar
s;What lazy vs eager processing means.
If the doc is weak, it's presumably in its explanation of one of the last two aspects. It looks like it's primarily the list literal aspect.
The doc's Syntax page has a section on various literals, including Array literals, but not list literals. The doc's Lists, sequences, and arrays does have a List literals section (and not one on Arrays) but it doesn't mention what they do with Scalar
s.
Presumably that warrants attention.
The Lists, sequences, and arrays page also has a Lazy lists section that could perhaps be updated.
Putting the above together it looks like the simplest doc fix might be to update the Lists, sequences, and arrays page.
Footnotes
1 In my first couple versions of this answer (1, 2, I tried to get Holli to reflect on the impact of containers vs values. But that failed for them and maybe hasn't worked for you either. If you're not familiar with Raku's containers, consider reading:
Containers, the official doc's "low-level explanation of Raku containers".
Containers in Perl 6, the third of Elizabeth Mattijsen's series of articles about Raku fundamentals for those familiar with Perl.
2 Some of the details in Wikipedia's discussion of "l-values and r-values" don't fit Raku but the general principle is the same.
In Raku most values are immutable.
my $v := 2;
$v = 3;
Cannot assign to an immutable value
To make variables, you know actually variable, there is a thing called a Scalar.
By default $
variables are actually bound to a new Scalar container.
(Which makes sense since they are called scalar variables.)
my $v;
say $v.VAR.^name; # «Scalar»
# this doesn't have a container:
my $b := 3;
say $v.VAR.^name; # «Int»
You can pass around this Scalar container.
my $v;
my $alias := $v;
$alias = 3;
say $v; # «3»
my $v;
sub foo ( $alias is rw ){ $alias = 3 }
foo($v);
say $v; # «3»
You can even pass it around in a list.
my $v;
my $list = ( $v, );
$list[0] = 3;
say $v; # «3»
This is the basis of the behaviour that you are seeing.
The thing is that you don't actually want to pass around the container, you want to pass around the value in the container.
So what you do is decontainerize it.
There are a few options for this.
$v.self
$v<>
$v[]
$v{}
Those last few only make sense on Array or Hash containers, but they also work on Scalar containers.
I would recommend using $v.self
or $v<>
for decontainerization.
(Technically $v<>
is short for $v{qw<>}
so it is for Hash containers, but there seems to be a consensus that $v<>
can be used generally for this purpose.)
When you do $v + 0
what you are doing is creating a new value object that is not in a Scalar container.
Since it is not in a container, the value itself gets passed instead of a container.
You don't notice this happening in the lazy case, but it is still happening.
It's just that you have printed the current value in the container before it gets changed out from underneath you.
my @bar = 'a', 'b', 'c';
sub foo( @b ) {
my $bar = 0;
gather loop {
print "*";
take ($bar, @b[$bar]);
$bar++;
last if $bar > 2;
}
};
my \sequence = foo( @bar ).cache; # cache so we can iterate more than once
# there doesn't seem to be a problem the first time through
.print for sequence; # «*0 a*1 b*2 c»
# but on the second time through the problem becomes apparent
.print for sequence; # «3 a3 b3 c»
sequence[0][0] = 42;
.print for sequence; # «42 a42 b42 c»
say sequence[0][0].VAR.name; # «$bar»
# see, we actually have the Scalar container for `$bar`
You would also notice this if you used something like .rotor
on the resulting sequence.
.put for foo( @bar ).rotor(2 => -1);
# **1 a 1 b
# *2 b 2 c
It's still just as lazy, but the sequence generator needs to create two values before you print it the first time. So you end up printing what is in $bar
after the second take
.
And in the second put
you print what was is in it after the third take
.
Specifically notice that the number associated with the b
changed from a 1
to a 2
.
(I used put
instead of print
to split up the results. It would have the same behaviour with print
.)
If you decontainerize, you get the value rather than the container.
So then it doesn't matter what happens to $bar
in the meantime.
my @bar = 'a', 'b', 'c';
sub foo( @b ) {
my $bar = 0;
gather loop {
print "*";
take ($bar.self, @b[$bar]); # <------
$bar++;
last if $bar > 2;
}
};
my \sequence = foo( @bar ).cache;
.print for sequence; # «*0 a*1 b*2 c»
.print for sequence; # «0 a1 b2 c»
# sequence[0][0] = 42; # Cannot modify an immutable List
# say sequence[0][0].VAR.name; # No such method 'name' for invocant of type 'Int'.
.put for foo( @bar ).rotor(2 => -1);
# **0 a 1 b
# *1 b 2 c