Raku list addition operator `Z+` 'fails' unless one of the lists is forced

Starting from:

my @foo = (1,1), (2,2);


say (3,3) Z+ @foo[1][*];   # (5 5)  


say (3,3) Z+ @foo[1][];    # (5 5)  


say (3,3) Z+ @foo[1]<>;    # (5 5)  


say (3,3) Z+ @foo[1]:v;    # (5 5)  

If you remove the + from the Z, and you use dd instead of say, it may become clearer:

dd (3,3) Z @foo[1];  # ((3, $(2, 2)),).Seq

So in this case, you get a list with 3 and (2,2). Note the $ before the (2,2): that means it's itemized: to be considered a single item.

Now with Z+, instead of creating a list, you're going to add the values.

When you write:

say 3 + (42,666);    # 5

you get 5 because you're adding the number of elements in the list to 3. That's why you're winding up with 5 in your example as well, not because the values in the list are 2.

In the other cases the Z operators sees non-itemized lists and so will iterate over its elements as you expected.

In case of doubt, make sure that you use dd instead of say in debugging: it will give you the nitty gritty of an expression, and not just a "gist" :-)

Another way of looking at things...

my @foo = (1,1), (2,2);
say @foo.WHAT;              # (Array)
say @foo[1].WHAT;           # (List)   <== It was already a list, right?

==> No, it wasn't.

This is the primary key to your question in two respects:

  • First, as Liz notes, when trying to understand what's going on when you encounter a surprise, use dd, not say, because dd focuses on the underlying reality.

  • Second, it's important to understand the role of Scalars in Raku, and how that sharply distinguishes Arrays from Lists.

Another way to see the underlying reality, and the role of Scalars, is to expand your examples a little:

my @foo = (1,1), (2,2);
say @foo.WHAT;              # (Array)  <== Top level elements "autovivify" as `Scalar`s
say @foo[1].VAR.WHAT;       # (Scalar) <== The element was a `Scalar`, not a `List`
say @foo[1].WHAT;           # (List)   <== The `Scalar` returns the value it contains
@foo[1] = 42;               # Works.   <== The `Scalar` supports mutability

my @foo2 is List = (1,1), (2,2);
say @foo2.WHAT;              # (List)  <== `List` elements *don't* "autovivify"
say @foo2[1].VAR.WHAT;       # (List)  <== `VAR` on a non-`Scalar` is a no op
say @foo2[1].WHAT;           # (List)  <== This time `@foo2[1]` IS a *`List`*
@foo2[1] = ...;              # Attempt to assign to `List` bound to `@foo2[1]` fails
@foo2[1] := ...;             # Attempt to bind to `@foo2[1]` element fails

I'll draw attention to several aspects of the above:

  • A Scalar generally keeps quiet about itself

    A Scalar returns the value it contains in an r-value context, unless you explicitly seek it out with .VAR.

  • Scalar containers can be read/write or readonly

    Until I wrote this answer, I had not cleanly integrated this aspect into my understanding of Raku's use of Scalars. Perhaps it's obvious to others but I feel it's worth mentioning here because the Scalar indicated by the $(...) display from dd and .raku is a readonly one -- you can't assign to it.

  • An Array "autovivifies" (automatically creates and binds) a read/write Scalar for each of its elements

    If a value is assigned to an indexed position (say @foo[42]) of a (non-native) Array, then if that element does not currently :exist (ie @foo[42]:exists is False), then a fresh read/write Scalar is "autovivified" as the first step in processing the assignment.

  • A List never autovivifies a Scalar for any of its elements

    When a value is "assigned" (actually bound, even if the word "assigned" is used) to an indexed position in a List, no autovivification ever occurs. A List can include Scalars, including read/write ones, but the only way that can happen is if an existing read/write Scalar is "assigned" to an element (indexed position), eg my @foo := (42, $ = 99); @foo[1] = 100; say @foo; # (42 100).

And now we can understand your code that yields (5):

my @foo = (1,1), (2,2);     # `@foo` is bound to a fresh non-native `Array`
say @foo[1].VAR.WHAT;       # (Scalar) -- @foo[1] is an autovivified `Scalar`
say @foo[1];                # (2,2) -- `say` shows value contained by `Scalar`

say (3,3) Z+ @foo[1];       # (5) --- because it's same as follows:

say +$(2,2);                # 2 -- number of elements in a two element list †
say (3,3) Z+ 2;             # (5) -- `Z` stops if either side exhausted

We're applying a coercive numeric operation (+) to a list (Positional value), not to its elements. A list, coerced to a number, is its "length" (count of elements). (Certainly for a non-sparse one. I'm not sure about sparse ones.)