Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

*why* does list assignment flatten its left hand side?

I understand that list assignment flattens its left hand side:

my ($a, $b, $c);
($a, ($b, $c)) = (0, (1.0, 1.1), 2);
say "\$a: $a"; # OUTPUT: «$a: 0»
say "\$b: $b"; # OUTPUT: «$b: 1 1.1»    <-- $b is *not* 1
say "\$c: $c"; # OUTPUT: «$c: 2»        <-- $c is *not* 1.1

I also understand that we can use :($a, ($b, $c)) := (0, (1.0, 1.1)) to get the non-flattening behavior.

What I don't understand is why the left hand side is flattened during list assignment. And that's kind of two questions: First, how does this flattening behavior fit in with the rest of the language? Second, does auto-flattening allow any behavior that would be impossible if the left hand side were non-flattening?

On the first question, I know that Raku historically had a lot of auto-flattening behavior. Before the Great List Refactor, an expression like my @a = 1, (2, 3), 4 would auto-flatten its right hand side, resulting in the Array [1, 2, 3, 4]; similarly, map and many other iterating constructs would flatten their arguments. Post-GLR, though, Raku basically never flattens a list without being told to. In fact, I can't think of any other situation where Raku flattens without flat, .flat, |, or *@ being involved somehow. (@_ creates an implicit *@). Am I missing something, or is the LHS behavior in list assignment really inconsistent with post-GLR semantics? Is this behavior a historical oddity, or does it still make sense?

With respect to my second question, I suspect that the flattening behavior of list assignment may somehow help support for laziness. For example, I know that we can use list assignment to consume certain values from a lazy list without producing/calculating them all – whereas using := with a list will need to calculate all of the RHS values. But I'm not sure if/how auto-flattening the LHS is required to support this behavior.

I also wonder if the auto-flattening has something to do with the fact that = can be passed to meta operators – unlike :=, which generates a "too fiddly" error if used with a metaoperator. But I don't know how/if auto-flattening makes = less "fiddly".

[edit: I've found IRC references to the "(GLR-preserved) decision that list assignment is flattening" as early as early as 2015-05-02, so it's clear that this decision was intentional and well-justified. But, so far, I haven't found that justification and suspect that it may have been decided at in-person meetings. So I'm hopping someone knows.]

Finally, I also wonder how the LHS is flattened, at a conceptual level. (I don't mean in the Rakudo implementation specifically; I mean as a mental model). Here's how I'd been thinking about binding versus list assignment:

my ($a, :$b) := (4, :a(2)); # Conceptually similar to calling .Capture on the RHS
my ($c, $d, $e);
($c, ($d, $e) = (0, 1, 2);  # Conceptually similar to calling flat on the LHS

Except that actually calling .Capture on the RHS in line 1 works, whereas calling flat on the LHS in line 3 throws a Cannot modify an immutable Seq error – which I find very confusing, given that we flatten Seqs all the time. So is there a better mental model for thinking about this auto-flattening behavior?

Thanks in advance for any help. I'm trying to understand this better as part of my work to improve the related docs, so any insight you can provide would support that effort.

like image 351
codesections Avatar asked Sep 17 '21 15:09

codesections


1 Answers

Somehow, answering the questions parts in the opposite order felt more natural to me. :-)

Second, does auto-flattening allow any behavior that would be impossible if the left hand side were non-flattening?

It's relatively common to want to assign the first (or first few) items of a list into scalars and have the rest placed into an array. List assignment descending into iterables on the left is what makes this work:

my ($first, $second, @rest) = 1..5;
.say for $first, $second, @rest;'

The output being:

1
2
[3 4 5]

With binding, which respects structure, it would instead be more like:

my ($first, $second, *@rest) := |(1..5);

First, how does this flattening behavior fit in with the rest of the language?

In general, operations where structure would not have meaning flatten it away. For example:

# Process arguments
my $proc = Proc::Async.new($program, @some-args, @some-others);

# Promise combinators
await Promise.anyof(@downloads, @uploads);

# File names
unlink @temps, @previous-output;

# Hash construction
my @a = x => 1, y => 2;
my @b = z => 3;
dd hash @a, @b;  # {:x(1), :y(2), :z(3)}

List assignment could, of course, have been defined in a structure-respecting way instead. These things tend to happen for multiple reasons, but for one but the language already has binding for when you do want to do structured things, and for another my ($first, @rest) = @all is just a bit too common to send folks wanting it down the binding/slurpy power tool path.

like image 76
Jonathan Worthington Avatar answered Oct 17 '22 03:10

Jonathan Worthington