Search code examples
arrayslistrakurakudo

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


I'm struggling to understand why the zip-add Z+ operator does not work on some cases.

I have some 2-element lists that I'd like to sum.

These work as expected whether I use lists or arrays:

say (1, 2) Z+ (3, 4) # (4, 6)
say [1, 2] Z+ (3, 4) # (4, 6)
say [1, 2] Z+ [3, 4] # (4, 6)
say (1, 2) Z+ [3, 4] # (4, 6)

Now we will do the same thing but I'll change the right operand with a value stored elsewhere. In this case I have an array of lists:

my @foo = (1,1), (2,2);
say @foo.WHAT;              # (Array)
say @foo[1].WHAT;           # (List)
say @foo[1];                # (2,2)
say (3,3) Z+ @foo[1];       # (5)  ???

Which gives the unexpected (at least for me :)) result of (5).

There are a couple of ways to fix this.

First one is to force the got element to be a list:

my @foo = (1,1), (2,2);
say @foo.WHAT;              # (Array)
say @foo[1].WHAT;           # (List)   <== It was already a list, but...
say @foo[1];                # (2,2)
say (3,3) Z+ @foo[1].list;  # <== changed. (5,5)

And the other one is change the @foo definition to be a list instead of an array (either by is List or by binding := the value)

my @foo is List = (1,1), (2,2);   # <=== Changed
say @foo.WHAT;              # (Array)
say @foo[1].WHAT;           # (List)   <== It was already a list
say @foo[1];                # (2,2)
say (3,3) Z+ @foo[1];       # (5,5)

Why the first case didn't work?


Solution

  • 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.)