12

I have difficulty understanding when and why the value held by a pushed Scalar container is affected after the push. I'll try to illustrate the issue that I ran into in a more complicated context in two stylized examples.

*Example 1 * In the first example, a scalar $i is pushed onto an array @b as part of a List. After the push, the value held by the scalar is explicitly updated in later iterations of the for loop using the $i++ instruction. These updates have an effect on the value in the array @b: at the end of the for loop, @b[0;0] is equal to 3, and no longer to 2.

my @b;
my $i=0;
for 1..3 -> $x {
  $i++;
  say 'Loose var $i: ', $i.VAR.WHICH, " ", $i.VAR.WHERE;
  if $x == 2 {
     @b.push(($i,1));
     say 'Pushed $i   : ', @b[0;0].VAR.WHICH, " ", @b[0;0].VAR.WHERE;
  }
}
say "Post for-loop";
say "Array       : ", @b;
say 'Pushed $i   : ', @b[0;0].VAR.WHICH, " ", @b[0;0].VAR.WHERE;

Output example 1:

Loose var $i: Scalar|94884317665520 139900170768608
Loose var $i: Scalar|94884317665520 139900170768648
Pushed $i   : Scalar|94884317665520 139900170768648
Loose var $i: Scalar|94884317665520 139900170768688
Post for-loop
Array       : [(3 1)]
Pushed $i   : Scalar|94884317665520 139900170768688

* Example 2 * In the second example, the scalar $i is the loop variable. Even though $i is updated after it has been pushed (now implicitly rather than explicitly), the value of $i in array @c does not change after the push; i.e. after the for loop, it is still 2, not 3.

my @c;
for 1..3 -> $i {
  say 'Loose var $i: ', $i.VAR.WHICH, " ", $i.VAR.WHERE;
  if $i == 2 {
     @c.push(($i,1));
     say 'Pushed $i   : ', @c[0;0].VAR.WHICH, " ", @c[0;0].VAR.WHERE;
  }
}
say "Post for-loop";
say "Array       : ", @c;
say 'Pushed $i   : ', @c[0;0].VAR.WHICH, " ", @c[0;0].VAR.WHERE;;

Output example 2:

Loose var $i: Scalar|94289037186864 139683885277408
Loose var $i: Scalar|94289037186864 139683885277448
Pushed $i   : Scalar|94289037186864 139683885277448
Loose var $i: Scalar|94289037186864 139683885277488
Post for-loop
Array       : [(2 1)]
Pushed $i   : Scalar|94289037186864 139683885277448

Question: Why is $i in @b in example 1 updated after the push, while $i in @c in example 2 is not?

edit: Following @timotimo's comment, I included the output of .WHERE in the examples. This shows the (WHICH/logical) scalar-identity of $i stays the same, while its memory address changes through the various loop iterations. But it does not explain why in example 2 the pushed scalar remains tied to the same WHICH-identity in combination with an old address ("448).

jjmerelo
  • 22,578
  • 8
  • 40
  • 86
ozzy
  • 785
  • 3
  • 12
  • 2
    i can give you the answer to why the WHICH seems to stay the same; look at the implementation: https://github.com/rakudo/rakudo/blob/master/src/core.c/Scalar.pm6#L8 - it only depends on the descriptor being used, which is a little object that holds things like the name of the variable, and the type constraint. if you use `.WHERE` instead of `.WHICH` you can see that the scalar is actually a different object each time around the loop. That happens because pointy blocks are "called", and the signature is "bound" on each call. – timotimo Oct 05 '19 at 16:30
  • @raiph During the loop, Example 1 shows the same pattern as Example 2: both have changing addresses reported by .WHERE, which is telling, I agree. But in itself it doesn’t explain why Example 2 comes to a different ending than Example 1. – ozzy Oct 06 '19 at 18:57

2 Answers2

5

A scalar value is just a container. You can think of them as a kind of smart pointer, rather than a primitive value.

If you do an assignment

$foo = "something"; #or
$bar++;

you are changing the scalars value, the container stays the same.

Consider

my @b; 
my $i=0; 
for 1..5 -> $x { 
  $i++; 
  @b.push(($i<>,1)); # decontainerize $i and use the bare value
} 
say @b;

and

my @b; 
my $i=0; 
for 1..5 -> $x { 
  $i := $i + 1;  # replaces the container with value / change value
  @b.push(($i,1)); 
} 
say @b;

Both of which work as expected. But: In both cases, the thing in the list is not mutable anymore, because there is no container.

@b[4;0] = 99; 

will therefore die. So just use the loop variable then, right?

No.

for 1..5 -> $x { 
  @b.push(($x,1)); # 
} 
@b[4;0] = 99; #dies

even if we iterate over a list of mutable things.

my $one = 1;
my $two = 2;
my $three = 3;
my $four = 4;
my $five = 5;

for ($one, $two, $three, $four, $five) -> $x { 
  @b.push(($x,1)); 
} 
@b[4;0] = 99; #dies

So there is no aliasing happening here, instead the loop variable is always the same container and gets values assigned that come from the other containers.

We can do this though.

for ($one, $two, $three, $four, $five) <-> $x { 
  @b.push(($x,1)); 
} 
@b[4;0] = 99; # works

for ($one, $two, $three, $four, $five) -> $x is rw { 
  @b.push(($x,1)); 
} 
@b[4;0] = 99; # works too

A way to make "the thing" mutable is using an intermediate variable.

for 1..5 -> $x { 
  my $j = $x;
  @b.push(($j,1)); # a new container 
} 
@b[4;0] = 99;

works fine. Or shorter and more in the original context

my @b; 
my $i=0; 
for 1..5 -> $x { 
  $i++; 
  @b.push((my $ = $i, 1)); # a new anonymous container
} 
@b[4;0] = 99;
say @b; # [(1 1) (2 1) (3 1) (4 1) (99 1)]

See also:

https://perl6advent.wordpress.com/2017/12/02/#theoneandonly https://docs.perl6.org/language/containers

jjmerelo
  • 22,578
  • 8
  • 40
  • 86
Holli
  • 5,072
  • 10
  • 27
  • 1
    Instead of `($x,1)`, you could also do `[$x,1]` which **would** create a new container (also for `1`, BTW) – Elizabeth Mattijsen Oct 05 '19 at 18:49
  • @ElizabethMattijsen But then it's the Array that does the "lifting" yes? – Holli Oct 05 '19 at 19:02
  • Not sure what you mean by "lifting", but if you containerizing the values on creation, then yes. – Elizabeth Mattijsen Oct 05 '19 at 19:51
  • @Holli Thanks for your reply. I'm not sure if it addresses the question though. Your answer focuses on the mutability of the container, which I think I understand. What I don't understand is why in the first example the pushed container $i - or better: its value - is updated after the push, while in the second example the value of the pushed container remains statically tied to the value at the moment of the push. The first example makes some sense to me (container is pointer to `Int` object --> `Int` gets replaced in for loop -> container points to new `Int`), but the second doesn't. – ozzy Oct 06 '19 at 07:20
  • @Holli I'll try to clarify the question. – ozzy Oct 06 '19 at 07:21
3

After playing with and thinking about my above question for some time, I'll wager an answer... It's pure conjecture on my part, so please feel free to say it's non-sense if it is, and if you happen to know, why...

In the first example, $i is defined outside of the lexical scope of the for loop. Consequently, $i exists independent of the loop and its iterations. When $i is referenced from inside the loop, there is only one $i that can be affected. It is this $i that gets pushed into @b, and has its contents modified afterwards in the loop.

In the second example, $i is defined inside the lexical scope of the for loop. As @timotimo pointed out, the pointed block get's called for each iteration, like a subroutine; $i is therefore freshly declared for each iteration, and scoped to the respective block. When $i is referenced inside the loop, the reference is to the block-iteration-specific $i, which would normally cease to exist when the respective loop iteration ends. But because at some point $i is pushed to @c, the reference to the block-iteration-specific $i holding value 2 cannot be deleted by the garbage collector after termination of the iteration. It will stay in existence..., but still be different from $i in later iterations.

ozzy
  • 785
  • 3
  • 12
  • @raiph Thanks. I’ll do that. Perhaps that someone with more insight than myself can (re)phrase the answer properly. I will anyhow not accept my own answer as correct until it’s confirmed (or improved) by those who know (rather than guess, like myself). – ozzy Oct 06 '19 at 13:01