Raku / doc

🦋 Raku documentation
https://docs.raku.org/
Artistic License 2.0
289 stars 291 forks source link

Clarify uses of sigils and why “identical values” behave differently in differently-sigilled variables. #3950

Open g964 opened 3 years ago

g964 commented 3 years ago

Problem or new feature

# 1
say zip((1, 2), (3, 4)); # gives ((1, 3) (2, 4)): correct

#2
my $a = (1, 2); my $b = (3, 4);
say "a: $a ; b: $b";
say $a.^name; # List: correct

# 3
say zip($a, $b); # gives (((1 2) (3 4))): wrong?

why "1" and "3" give different results? I am lost here...

k3ut0i commented 3 years ago

While $a.^name says that $a is a List, when you store the list in a variable with scalar sigil, you are indicating that the list be treated as one value. So zip will not iterate into it.

https://docs.raku.org/language/101-basics#Scalar

treyharris commented 3 years ago

To amplify @k3ut0i’s comment, let’s look at some code that shows moving in either direction:

my @a = (1,2);
my @b = (3,4);
say "{@a.^name, @b.^name}: {@a.raku, @b.raku}";
say "zip(@a, @b): ", zip(@a, @b);
say "zip(\$@a, \$@b): ", zip($@a, $@b);
say();

my $a = (1, 2);
my $b = (3,4);
say "{$a.^name, $b.^name}: {$a.raku, $b.raku}";
say "zip(\$a, \$b): ", zip($a, $b); 
say "zip(@$a, @$b): ", zip(@$a, @$b);

Output:

Array Array: [1, 2] [3, 4]
zip(@a, @b): ((1 3) (2 4))
zip($@a, $@b): (([1 2] [3 4]))

List List: $(1, 2) $(3, 4)
zip($a, $b): (((1 2) (3 4)))
zip(@1 2, @3 4): ((1 3) (2 4))

Using the @ operator (actually, list contextualizer) on the scalar variables $a and $b makes them zip the same as using an @-sigil variable directly. Similarly, using the $ operator (or item contextualizer) makes zip on @a and @b act like it does on your $a and $b.

This is cute because it’s symmetrical, but I think a bit more commonly you’ll see .list used as the more explicit list contextualizer:

say "zip(\$a.list, \$b.list): ", zip($a.list, $b.list); 

Output:

zip($a.list, $b.list): ((1 3) (2 4))

What about the docs?

I’m not sure this issue should be closed as a case of language help—nor even, “maybe we need to add another example”…

As a docs matter, this specific issue (misunderstanding of listy things in $-sigil variables) has — for almost two decades now! — continued to present one of the most nettlesome Raku learning curves for a specific group: that category of programmers who are not dyed-in-the-wool Perl internals-savvy programmers (who think about Perl variables and sigils more deeply than anyone in their right mind should do — but we’re not in our right minds), nor those who have no Perl experience at all—but who have enough Perl experience to have familiarity with the sigils and some of their more esoteric uses without really delving into the whole value-vs.-variable-vs.-container internals. I’m curious if this describes you.

(It isn’t exclusive to that group; a couple other groups that I’ve found tend to have this issue as well are Lisp programmers—who think of lists very differently—and Eiffel or other design-by-contract paradigm programmers—who tend to assume that when things can be constructed the same, the methods called on them should work exactly the same.)

The way sigils work in Raku is often shorthanded something like: “unlike in Perl, they’re integral to the variable now, so you never have to worry about when to swap the sigil when slicing/indexing, and you can assign a list to a $-sigilled variable now without losing information, no problem!” But we don’t get into the issues that present when you really commit to this by just using $ for all your variables, and why you shouldn’t do that (other than as a stylistic matter). I wonder if we shouldn’t have some more text about why, as a design matter, one would use variables of all three (data container) sigils at all—rather than exclusively $, which in Raku can do the job?

treyharris commented 3 years ago

On the basis of what I wrote above in the second section, I’m labeling this as a trap.

g964 commented 3 years ago

Thanks to all!

codesections commented 3 years ago

@treyharris You bring up some really good points.

Here's my perspective as someone who came to Raku without Perl experience but who still remembers being tripped up by that distinction: I wonder if the problem is that we tend to have examples like this

my @array = (1, 2, 3) # Arrays are declared with @ and are mutable
my $list = (1, 2, 3)  # Lists are declared with $ and are immutable

That's not really wrong, but it confuses/combines two issues. (And I'm not sure where I came across examples like that – it was a while ago.) It might be better to say something like:

my @array = (1, 2, 3);   # Assigning a List to a @-sigiled variable creates an Array, which is mutable
my @list := (1, 2, 3);   # Binding a List to a @-sigiled variable creates a List, which is immutable
my $s-array = [1, 2, 3]; # Assigning an Array to a $-sigiled variable also creates an Array, but in scalar context
my $s-list = (1, 2, 3);  # Assigning a List to a $-sigiled variable also creates an List, but in scalar context

That might be too many concepts to introduce at once, but my point is that $ is sometimes presented as the way you make list-y things immutable, which isn't right and may not be helpful.

pmichaud commented 3 years ago

My take:

my @a = (1, 2, 3); # declare an array (mutable) and initialize it with elements from the (immutable) List my $l = (1, 2, 3); # declare a scalar variable and initialize it with an immutable List

Note that the $l variable here is itself mutable -- you can change the value of the variable (e.g. "$l = 4") but not the elements of the List it's currently referencing (e.g. you can't do "$l[0] = 4" because Lists are immutable).

Your other examples are then something like

my @a = (1, 2, 3); # declare an array (mutable) and initialize it with elements from the (immutable) List my @l := (1, 2, 3); # declare an positional variable and bind it to an immutable List my $sa = [1, 2, 3]; # declare a scalar variable and initialize it with a mutable Array my $sl = (1, 2, 3); # declare a scalar variable and initialize it with an immutable List

Part of the point of this is that the elements of $sa can be changed, while the elements of $sl cannot.

Pm

On Thu, Sep 02, 2021 at 01:21:31PM -0700, Daniel Sockwell wrote:

@treyharris You bring up some really good points.

Here's my perspective as someone who came to Raku without Perl experience but who still remembers being tripped up by that distinction: I wonder if the problem is that we tend to have examples like this

my @array = (1, 2, 3) # Arrays are declared with @ and are mutable
my $list = (1, 2, 3)  # Lists are declared with $ and are immutable

That's not really wrong, but it confuses/combines two issues. (And I'm not sure where I came across examples like that – it was a while ago.) It might be better to say something like:

my @array = (1, 2, 3);   # Assigning a List to a @-sigiled variable creates an Array, which is mutable
my @list := (1, 2, 3);   # Binding a List to a @-sigiled variable creates a List, which is immutable
my $s-array = [1, 2, 3]; # Assigning an Array to a $-sigiled variable also creates an Array, but in scalar context
my $s-list = (1, 2, 3);  # Assigning a List to a $-sigiled variable also creates an List, but in scalar context

That might be too many concepts to introduce at once, but my point is that $ is sometimes presented as the way you make list-y things immutable, which isn't right and may not be helpful.

-- You are receiving this because you are subscribed to this thread. Reply to this email directly or view it on GitHub: https://github.com/Raku/doc/issues/3950#issuecomment-912026252

raiph commented 3 years ago

This is the same issue that arises in English.

The issue is rooted in the following ambiguity and its resolution: how many things are there in a list of five things?

The fact is, both these answers are demonstrably correct:

The reality is it's ambiguous. So how do you decide?

English provides some tools to help disambiguate. And so does Raku. Personally I think Raku does a stellar job, and that we're discussing a notable Raku strength, a million miles from a trap.


Many PLs do not distinguish variables from values. This includes ones like:

But what if both these approaches had merit, and can be combined to good effect? What if variables drove variable aspects of behaviour, and values drove value aspects? This is Raku's approach.


One particularly nice thing about this approach is dealing with the fundamental ambiguity in the nature of plural things, such as a list.

Raku's variable side of things sorts out this distinction via sigils:


“they’re integral to the variable now, so you never have to worry about when to swap the sigil when slicing/indexing, and you can assign a list to a $-sigilled variable now without losing information, no problem!”

I think that's correct.

But we don’t get into the issues that present when you really commit to this by just using $ for all your variables, and why you shouldn’t do that (other than as a stylistic matter). I wonder if we shouldn’t have some more text about why, as a design matter, one would use variables of all three sigils at all—rather than exclusively $, which in Raku can do the job?

I think the solution is to focus on the specific example of "a list of five things", and the ambiguity I discussed, and various ways to resolve that ambiguity:

In addition, the coder can further control behaviour, even if a sigil has already been chosen. If it's $foo, one can write @$foo to convey a plural view of the value. Conversely, if it's @foo, one can write $@foo to convey a singular view.

And so on.

treyharris commented 3 years ago

@raiph Very nice—that seems like a good start. Fleshed out with some examples of

  1. Each of the types of disambiguation strategies you isolated found in the core library to show how they work with various combinations (permutations, in some cases—don’t forget multis!) of sigils,
  2. How one would go about doing these for one’s own subs and methods,
  3. How Signatures and Captures work to shape the prior item (I can see it making sense to discuss this prior to the previous item or next, depending on which is more concrete to explain first)
  4. Recommendations for how to choose a sigil — your “five items…” section above gives some ideas, but they’re more like how the language sees them; we need to turn the perspective around to that of an interface designer to consider when to use which.

The final item is obviously strongly related to the prior two: while there are cases like wraps where it can be convenient to immediately change the context of a captured argument to something different than the signature’s parameter, that’s rare, so it mostly tracks. For a lot of body code, already having learnt to declare and call correctly will transfer directly to what sigil is correct after a scoped my definition.

But the big area of confusion where Signatures and Captures, splatting and eagerness, proto whatevers etc., don’t help much is another couple of places I see frequent confusion: iterators and set operations (particularly when member-checking or smart matching), where context can get subtle and weird indeed if you start off on the wrong sigil footing, but can be as intuitive as a paintbrush if you start off right.

I’m happy to collaborate on this if you want to bounce some ideas back and forth.

patrickbkr commented 3 years ago

I really value discussion of intent of sigils. I.e. What are the abstract concepts they convey?

I'd think it is a good complement to in addition give a technical explanation of what happens under the hood with sigiled variables and assignment and binding. I actually don't have reliable knowledge, but from the above examples I think the story roughly goes as follows. I will evade the word "variable" as I feel it confuses things for people having a preconception about what a variable is.

No sigil - no container

my Int \x = 2; creates a symbol containing a value. x := 4; binds the symbol to the value 4. (The above somehow fails without the type constraint. Unsure why.) It would be nice to give an example of putting a literal Scalar object into a sigilless variable. But I found no way to do so. (Assigning another variable doesn't count.)

$ sigil - Scalar container

my $s; creates a symbol bound to (auto initialized to) a scalar container with nothing in it. $s = 6; puts a value in the container (probably by calling some method on the container), the symbol itself is untouched. $s := 4; binds the symbol to the (immutable) value 4. Now the container is gone. The sigil now lies.

@ sigil - Listy container

my @a; creates a symbol bound to (auto initialized to) an array container. my @a is Array; is equivalent. @a = 4; puts a value in the array (probably by calling some method on the array). @a := (0, 1, 2); binds the symbol to the immutable list (0, 1, 2). my @l is List; creates a symbol bound to (auto initialized to) a List (instead of Array).

So in general = calls a method on the container and := changes the value of the symbol. Both respect the type constraint which the symbol might have attached.

(I don't fully understand where type constraints are actually saved: my $a = 2; my Int $b := $a; $b = "x" succeeds, indicating the constraint is part of the container. my Int $a = 2; $a := 4; $a := "x" fails, indicating the constraint is part of the symbol.)

codesections commented 3 years ago

my Int \x = 2; creates a symbol containing a value. x := 4; binds the symbol to the value 4. (The above somehow fails without the type constraint. Unsure why.)

I'm also confused by that, though in somewhat the opposite direction – my understanding is that sigilless symbols are never supposed to be re-bindable. I get that from @jnthn's answer to the StackOverflow question Is there a purpose or benefit in prohibiting sigilless variables from rebinding?

That leaves the question of binding behavior. It was decided to make the sigilless symbol form a "static single assignment" syntax, as explained in one of the other answers. There were various reasons for this, including … enhancing program readability by having a form that lets the reader know that the symbol will never be rebound to a new value, so one can effectively consider it constant within the scope.

In fact, I wonder whether the fact that x := 4 works in the snippet above represents a bug? If so, it probably shouldn't be part of how we explain these issues in the docs :grin:

treyharris commented 3 years ago

In fact, I wonder whether the fact that x := 4 works in the snippet above represents a bug? If so, it probably shouldn't be part of how we explain these issues in the docs 😁

As @patrickbkr mentioned (perhaps a bit obliquely), the error only shows up depending on whether you use the type declaration Int:

my Int \x = 2;
x := 4; # This works

say x*x;

# truncate file here and the program “works”,
# printing “16” but the following causes compile-time error:
my \y = 3;
y := 5; # This fails
say y*y;

This program fails to compile, so I suppose the first one doesn’t “work” so much as it “does not provoke a fatal compiler error”.

treyharris commented 3 years ago

Oops, my keyboard went wonky and a paragraph break turned into a “submit comment”…

I was just going to add, perhaps it’s an autoboxing issue of some sort? I note you can’t compile

my int8 \x = 4;

at all.

codesections commented 3 years ago

As @patrickbkr mentioned (perhaps a bit obliquely), the error only shows up depending on whether you use the type declaration Int

Yeah, I understood that. But my point is that

my Int \x = 2;
x := 4; # This works

say x*x;

does compile (and prints 16). That isn't consistent with the idea that "the sigilless symbol [is a] form a 'static single assignment' … that lets the reader know that the symbol will never be rebound to a new value". And I'm wondering if that inconsistency is a bug, in which case we shouldn't use it in the docs.

JJ commented 3 years ago

Possibly any (numeric) type declaration gives it a container:

my Rat \zipi = ⅓; zipi := ⅔

Might be related to autoboxing, as @treyharris says. It does not work with Str, for instance.

codesections commented 3 years ago

Might be related to autoboxing, as @treyharris says.

@jj, just to clarify: are you (and/or @treyharris) saying that being able to re-bind my Int \x = 2 seems like a bug, and that the bug is related to autoboxing?

Or are you saying that this behavior does not seem like a bug, and that it is instead correctly caused by something related to autoboxing?

treyharris commented 3 years ago

Yeah, I understood that. But my point is that

# code…

does compile (and prints 16). That isn't consistent with the idea that "the sigilless symbol [is a] form a 'static single assignment' … that lets the reader know that the symbol will never be rebound to a new value". And I'm wondering if that inconsistency is a bug, in which case we shouldn't use it in the #docs.

Sorry for piling ambiguity on ambiguity… I understood what you were saying. I wasn’t putting my oar in on whether rebinding should be allowed for any sigilless variables or not.

@JJ, just to clarify: are you (and/or @treyharris) saying that being able to re-bind my Int \x = 2 seems like a bug, and that the bug is related to autoboxing?

Or are you saying that this behavior does not seem like a bug, and that it is instead correctly caused by something related to autoboxing?

I don’t know about JJ, but I’m just saying the behavior difference may be related to this.

I just found you can’t use any native types in declarations to sigilless variables:

my int8 \b = 16; # Doesn’t compile: Type check failed
                 # in binding; expected int8 but got Int (16)
my int8 $sigilled = 16; # Compiles
my int8 \no-sigil = $sigilled; # same failure as above

This makes sense when you see that you can’t bind to sigilled variables of native types, either:

my int8 $sigilled := 16; # Fails with Cannot bind
                         # to natively typed variable
                         # '$sigilled'; use assignment instead

Since sigilled “assignment” is binding, this restriction makes sense.

This seems to explain the Int behavior, and indeed seems to indict autoboxing:

my Int \b = 8;
say "{b*b}"; # prints 64

b := 4;
say "{b*b}"; # prints 16

as compared with:

my \b = 8;
say "{b*b}";
b := 4;
say "{b*b}";

which fails compliing with

Cannot use bind operator with this left-hand side
at test:3
------> := 4;

I can see where this behavior makes some sense, from a (im)mutable-values perspective, but is it correct? I don’t think so…

treyharris commented 3 years ago

Since sigilled “assignment” is binding, this restriction makes sense.

Or, at least, as described in the above-mentioned Stack Overflow answer, Single Static Assignment, which seems to be like binding—as far as error messages are concerned, anyway…

JJ commented 3 years ago

Might be related to autoboxing, as @treyharris says.

@JJ, just to clarify: are you (and/or @treyharris) saying that being able to re-bind my Int \x = 2 seems like a bug, and that the bug is related to autoboxing?

No, I'm saying the behavior is unexpected. It might be a bug, but my money is on allowing (some) type constraints in sigilless variable declarations. Since, as @jnthn says, it's simply slapping a name to a literal, any type information should be whatever is obvious from the literal. It does not make a lot of sense to allow Int and Rat type constraints, and not allow Num or Str or anything else.

raiph commented 3 years ago

@treyharris

I’m happy to collaborate on this if you want to bounce some ideas back and forth.

Sure. Though once I start bouncing ideas, I get serious. If the following is more than you bargained for, well, I hope you're happy about that. And if not, well, too late...

  1. Each of the types of disambiguation strategies you isolated found in the core library to show how they work with various combinations (permutations, in some cases—don’t forget multis!) of sigils,

Agreed, something like that.

My preference as I write this would be that we start with someone else (you?) attempting to interpret what I wrote as categories, finding what seemed to them like plausible examples or counter-examples for each category, and listing them in a fresh comment they write here. Then I'll respond to that comment with one of my own.

(Presuming we @ mention each other in our comment, will GH inform us whenever we edit such a comment? If so, I'd prefer we just keep editing our original comments, adding, removing or altering examples or individual discussion of them, as our collective discussion unfolds, rather than each of us writing a series of new comments.)

  1. How one would go about doing these for one’s own subs and methods,
  2. How Signatures and Captures work to shape the prior item

I'd like to be the one who focuses on these in the first instance. That is, I'm hoping that you and/or others will enjoy and focus on doing 1. perhaps generally ignoring 2. and 3., at least early on, and let me enjoy and focus on 2. and 3., without too much concern about 1, at least early on.

  1. Recommendations for how to choose a sigil — your “five items…” section above gives some ideas, but they’re more like how the language sees them; we need to turn the perspective around to that of an interface designer to consider when to use which.

Makes sense.

There may be some somewhat related ideas in SO Q+As as found via searches like "[raku] what sigil to use when"](https://stackoverflow.com/search?q=%5Braku%5D+what+sigil+to+use+when). For example, an answer I wrote.

Right now it feels like a first stab at this would best be left until after good progress has been made on 1./2./3.

I see frequent confusion [about the foregoing related to] iterators and set operations (particularly when member-checking or smart matching), where context can get subtle and weird indeed if you start off on the wrong sigil footing, but can be as intuitive as a paintbrush if you start off right.

Examples of confusion / mistakes could be assembled as a separate task, and done from the get go, like the examples of clarity / best practice of task 1. (Perhaps at the same time, and perhaps included in the same comment.)

And I'd again prefer that someone else (you?) started the ball rolling on that in the first instance, and then I'd like to react, with whoever has written the examples of confusion / mistakes updating their comment(s) to slot things into categories we agree on, removing weak examples, adding strong ones, and so on.


I'd like this process to be unhurried. If it takes 2 years (I doubt it, but unhurried is unhurried), then so be it. If it takes a day (I doubt it, but inspired and having fun can be productive), then great.

I've been evolving my understanding of Raku over many years, and while it feels like the right time to see if we can nail things down about sigils in a manner that impacts the doc, I'd still want to feel free to reverse myself, mess with categories, refine or drastically alter metaphors and mental models, and so on, without that annoying whoever else is working on this.

Similarly, I'd want others to be equally bold and free in making radical switches in how they think about it all. I can guarantee that I will not be annoyed.

After a while we'll naturally find that we're failing to come up with new examples of relevant clarity or of relevant confusion, and that we have consensus about what we all feel are great responses, at least in outline, for each of 2., 3.., 4.,, (and any 5., 6. or more that we decide really matter for even a Minimum Viable Product).

At that stage, and not before, we can start thinking about what that would best mean for the doc, what needs to be written, who will do it, and so forth.

raiph commented 3 years ago

@patrickbkr

I will evade the word "variable" as I feel it confuses things for people having a preconception about what a variable is.

I hear you on folk having preconceptions, and that leading to confusion if they're the wrong preconceptions. And avoiding it for your story about what you think is going on helpful. That said, I consider this issue of wrong preconceptions, while potent, may yet be best resolved by tackling that head on in the actual doc about this stuff, rather than avoiding the word "variable".

No sigil - no container

I'm a bit confused by what you mean by "No sigil -- no container".

You say you don't see a way to bind to a new container -- but note one way which you say "doesn't count". I presume by "don't see a way" you've noticed that you can't just write Scalar.new. What about just $? Also, what do you mean by "Assigning another variable doesn't count"? That is to say, what do you mean by "Assigning", and what do you mean by "doesn't count"?

Consider::

my $container = 42;
my \container = $container;
container = 99;
say container;  # 99
say $container; # 99
my \another-container = $;
another-container = 100;
say another-container; # 100

Moving on:

my Int \x = 2; # creates a symbol containing a value.

Yes to creating a symbol.

(BTW, just as many devs have preconceptions about "variable" that don't fit Raku's notion of them, many devs have the preconception that symbols are complicated things. I get that you're using the term to avoid "variable" but again note that while that's helpful for following what you're saying, it may not be helpful in describing in the doc this stuff about differing sigils.)

x := 4; # binds the symbol to the value 4.

I currently think I'd like to see any code of the form x := ... made a compile-time error.

$ sigil - Scalar container

my $s; # creates a symbol bound to (auto initialized to) a scalar container with nothing in it.

I know what you mean, but no.

A symbol is always initialized to a value. There are no exceptions.

The value may be one that represents a definition of "value" that doesn't match up with a given dev's preconception about what a "value" is. In other words, we have the same problem that you rightly noted about "variable".

One way of defining "value" would allow that a value may be a container. This can be an extremely valuable approach that can help Raku make much more sense to those who don't yet fully clearly understand Raku's variable/symbol/value/container constructs. Using this definition, my $s; creates a symbol that's initialized to be bound to a new value that's a Scalar container.

Or one can adopt a more limited definition of "value" in which a container isn't a value but it always contains one. This can also be an extremely valuable approach that can again help Raku make much more sense to those who don't yet fully clearly understand Raku's variable/symbol/value/container constructs. Using this definition, my $s; creates a symbol that's initialized to be bound to a new Scalar container that "contains" an Any type object as its value.

$s = 6; # puts a value in the container (probably by calling some method on the container), the symbol itself is untouched.

Correct.

$s := 4; # binds the symbol to the (immutable) value 4. Now the container is gone. The sigil now lies.

Yes, except for the bit about the sigil lying. The sigil is telling the truth. The $ sigil does not mean "container".

@ sigil - Listy container

my @a; # creates a symbol bound to (auto initialized to) an array container.
my @a is Array; # is equivalent.

Correct.

@a = 4; # puts a value in the array (probably by calling some method on the array).
@a := (0, 1, 2); binds the symbol to the immutable list (0, 1, 2).

Yes, and the original Array is gone, just as the original Scalar was gone after $s := 4;.

my @l is List; # creates a symbol bound to (auto initialized to) a List (instead of Array).

Yes.

So in general = calls a method on the container and := changes the value of the symbol. Both respect the type constraint which the symbol might have attached.

Yes and no.

Like "variable" and "value", devs can have preconceptions about what "container" means and those preconceptions can be wrong.

The word "container" is sometimes/often used in Raku to specifically mean a Scalar, not an Array or Hash etc. Then again, some folk consider the latter to also be "containers".

(I don't fully understand where type constraints are actually saved:

my $a = 2; my Int $b := $a; $b = "x"; # succeeds, indicating the constraint is part of the container.
my Int $a = 2; $a := 4; $a := "x"; # fails, indicating the constraint is part of the symbol.)

I'm pretty sure both symbols and Scalar containers carry constraints. I currently think the my Int $b := $a; ought by rights fail. I'm pretty sure jnthn has recently mentioned that constraints aren't properly resolved between symbols and Scalars they're bound to and that this will be fixed when RakuAST lands. (Though maybe I'm imagining that.)

JJ commented 3 years ago

$a is a List in Scalar context. It's probably a trap, but not really specific for List operators, it's rather related to the use of sigils.

raiph commented 3 years ago

@JJ

$a is a List in Scalar context.

Yes. Which is an extremely basic and perfectly valid scenario.

How many things is a list? It's one thing. That's the very definition of scalar.

It's probably a trap

If so, then the notion of "one thing" is probably a trap. I have one head, and can see how it can be a problem, but is it probably a trap?

2colours commented 1 year ago

I think this is really an infinite dungeon of a topic. I can't stop anybody from thinking about ways to explain it better - nor does there seem to be a need for me to stop anybody, as there is no visible progress - but at the end of the day, I can't help but think this is ultimately a design issue. There are things that one is ashamed to write down because one can immediately feel that it's unconvincing and uncomfortable to read for anyone. That's exactly how I feel about this topic.

The problem is really multi-dimensional:

I don't want to talk too much about the first point but busting the pink clouds still seems necessary:

I suspect most of these points stand for %vars as well, and &vars are really just Scalars with nice sugar for invocation.

Now, we have collected more than enough reasons to want to stay away from @vars and %vars so what is keeping us? Well, mostly the point of this issue.

One might have perfectly legitimate reasons to have a singular mutable interface for a "plural datatype"; just check the issues with @-sigilled containers I listed, those are all things you actually do need that mutable interface for. However, one doesn't have equally legitimate and common reasons to retrieve a "plural datatype" as a single entity. If you wanted your iterable object to be treated as a single value, you probably wouldn't try to use a loop or a method that is tied to iterability. Since list-alikes don't auto-flatten at least since the Great List Refactor, prior to the first stable release, there is no reason to anticipate that something will get iterated that you meant to only cover as an item of a sequence.

Let's take a simple example. Creating a nested Array is so simple, one might say it's simpler than not creating it (since my @var will create an Array without any special considerations). Now, if you call flat on it, nothing will happen, and you need to jump hoops to get the only reasonable behavior one could have ever had in mind. You also need to be a bit precautious if you want to for-loop over one of the element - however, you don't need to do that if you call map.
And this is where it all starts to fall apart: as of Rakudo 2023.04 (I have no idea what is specced behavior and what isn't, at this point), [X] @nested-array will ignore containers but @nested-array[0] X @nested-array[1] X ... @nested-array[*-1] will not, and cross(@nested-array) also will not. ([X] $@nested-array will definitely be an identity, if you ever find yourself wanting this outcome, lol)

To summarize it again quickly: