Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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?

like image 210
Julio Avatar asked Dec 15 '20 10:12

Julio


2 Answers

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" :-)

like image 200
Elizabeth Mattijsen Avatar answered Nov 15 '22 23:11

Elizabeth Mattijsen


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, but...

... was it?

==> 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)
say @foo[1].VAR.WHAT;       # (Scalar) <== In reality it 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`s do *not* make `Scalar`s
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] = ...;              # Fails no matter what `@foo2[1]` and `...` are.
@foo2[1] := ...;             # Fails no matter what `@foo2[1]` and `...` are.

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" 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" (automatically created and bound to that indexed position) 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.)

like image 39
raiph Avatar answered Nov 15 '22 22:11

raiph