12

If I understand the Raku docs correctly, the elements of Arrays are always containerized, i.e. Scalars. However, the deepmap method seems to create (inner) Arrays with uncontainerized elements:

my @a = [1, [2, 3]];
my @b = @a.deepmap: *.clone;
say @b[0].VAR.^name;     # Scalar, this is OK
say @b[1].^name;         # Array, as expected
say @b[1][0].VAR.^name;  # Int, why?
@b[0] = 4;               # this works
@b[1][0] = 5;            # error: Cannot assign to an immutable value

Why does this happen?

For context, I originally wanted to use .deepmap: *.clone to create a deep copy, but I needed the copy to be mutable. I solved the problem by using @a.deepmap: { my $ = .clone }, but I am still curious why this happens.

Nikola Benes
  • 2,372
  • 1
  • 20
  • 33

1 Answers1

10

If I understand the Raku docs correctly, the elements of Arrays are always containerized, i.e. Scalars.

That's almost correct, but not quite – Array initialization (i.e., with [1, 2]) containerizes the values, but that doesn't mean that elements are always containerized. For example, you can explicitly bind a value to a position in an array.

Or, as you've discovered, you can wind up with a non-containerized value when creating an Array in an unusual way. Let's take a look at what deepmap is doing here:

my @a = [1, [2, 3]];

@a.deepmap({.say; $_});  # OUTPUT: «1␤2␤3␤»
say @a.raku;             # OUTPUT: «[1 [2 3]]»

What's going on? Well, deepmap is recursively descending into the structure and calling the function on each leaf element (that's why it prints 2 instead of [2 3] on the second iteration). And then it binds the result to the slot it was iterating over.

So, with .clone, deepmap goes down to the leaf (e.g., 2) and calls .clone on that value, gets 2 (an Int) and binds that to the position in the Array.

It appears that what you wanted to happen was for .clone to be called on [2 3], rather than 2. If so, you could do that for lists with one level of nesting with (like above) with .map(*.clone); for more complex nesting, you can use duckmap or tests inside a map expression (or, as you discovered, call .clone on the leaf values and add a Scalar manually.)

codesections
  • 8,900
  • 16
  • 50
  • 2
    The docs say: “An Array is a List which forces all its elements to be scalar containers”. It seems the authors use the word “force” in a very unusual way… Also, the docs say that “binding Array slots directly to values is strongly discouraged”. It surprises me then that a standard method does something that is strongly discouraged. – Nikola Benes Dec 19 '21 at 20:42
  • What I still don't understand, though, is why the outermost array elements are not bound but assigned. Why is `@b[0]` a Scalar? – Nikola Benes Dec 19 '21 at 20:46
  • 3
    Hmm, yeah that "forces" language in the Array docs is a bit unfortunate and would be worth changing. Re: "Why is @b[0] a Scalar" – it's because of the `@` sigil. Note that `@a.deepmap(*.clone)[0].VAR.^name` returns `Int` (without a `Scalar`). But when you assign the `deepmap`'s result into `@b`, the you get the default behavior of `@`: copy each element of the RHS and assign it to a Scalar bound to the corresponding position on the LHS. (Note that you don't get a Scalar for `$b[0]` (or for `@b[0]` if you declare `@b` with `my @b is List =`).) – codesections Dec 19 '21 at 21:30
  • 1
    I feel this is actually worthy of making a Rakudo issue for – Elizabeth Mattijsen Dec 19 '21 at 22:02
  • 1
    @NikolaBenes Just as "forced" is "unfortunate" wording, so too "strongly discouraged". – raiph Dec 19 '21 at 23:40