problem-solving
problem-solving copied to clipboard
Destructuring assignment lhs has different semantics from the corresponding literal
Consider the following scenario.
my @a = <1 2>;
my $b = 3;
dd (@a, $b);
this yields ([1, 2], 3)
so the array of @a is embedded into the list.
However:
my (@a, $b) = ([1, 2], 3);
dd @a, $b
# Array @a = [[1, 2], 3]
# Any $b = Any
# Nil
The destructuring flattened and @a ate all the right handside as one array. Nothing is left for $b. The same would happen if we had @b so (@a, @b) = (@b, @a) itself could led to a crazy result - @a containing everything and @b nothing, even though the right handside was still a list of arrays.
I don't know if there is a clear rationale behind this behavior but it's WTF-worthy in any programming language presentation. Other than being ugly and useless in this particular scenario, it can cause serious headache to the developer. I couldn't find anything about it and after all, I'm nothing but a newbie to Raku - first off, it would be good to know about this behavior. However, I have to say I can't imagine any acceptable reason for this behavior to stay because it's inconsistent. If it flattens on the left side, it should on the right side as well or vice versa. I suppose both can be reasoned but the inconsistency cannot.
That's actually working as intended (though it's not currently documented very well; see Raku/doc#3962 for WIP efforts to improve the related docs).
This is a kind of tricky corner of Raku and took me a while to get my head around (see Rakudo/rakudo#4536 and the links in it for all the gory details). But here's the short version:
my (@a, $b) = ([1, 2], 3);
is not "destructuring assignment" – it's list assignment.
my (@a, $b) := ([1, 2], 3);
is closer to "destructuring assignment" (though I believe the plan is for the docs to call it just "destructuring", since it really operates by binding to a signature and then destructuring that signature). Whatever we call it, though, it gets the results that it sounds like you were expecting: @a
is [1, 2]
and $b
is 3
.
I agree that this behavior can lead to a WAT at first and 100% needs to be more clearly explained in the docs.
Oh, and I also initially found the way list assignment flattens about as surprising as you do, which lead me to pose the question why does list assignment flatten its left hand side? on StackOverflow. jnthn gave a helpful answer, though I'm still not entirely sure if I would have supported the flattening semantics for list assignment if I'd been involved in that decision. (Though I might; as jnthn points out, since we can destructure with :=
, there's something to be said for giving =
different semantics if people find those useful).
No matter what we call it, actually I don't think people would find this semantics useful - and if there is one argument for it, it's simply to stay, again, consistent.
Actually, even that list assignment says:
The last two examples above are simple destructuring assignments that select the first item of the right-hand side list. See for a more elaborate discussion of destructuring assignments in the context of variable declarations the section on declaring a list of variables with lexical or package scope.
Even though it doesn't mention @-sigilled variables explicitly, this is more than confusing because this still hints that there should be no fundamental difference. (If you ask me, having separate list assignment and item assignment with different precedence is just confusing enough, no need to add to that...)
Whichever way is chosen for my
it would be confusing to somebody. The current approach is the least confusing path when it comes to array assignment. Just try this:
my @a;
@a = [1, 2], 3;
@a = ([1, 2], 3);
Both cases give you the same result. So, one would expect my @a = [1, 2], 3
to do the same as the first assignment above. Why having it like my (@a, $b)
must change the way the assignment happens drastically? As long as we draw no difference between [1,2],3
and ([1,2],3)
in most other situations, what'd be the reason for the latter one to be an exception? Once again, as @codesections noticed, we talk about assignment here. In either case, the assignment receives a list.
Apparently, use of a scalar in place of @a
puts things into a different perspective. It enforces "singular" context, let's name it so. So, $a = [1,2],3
results in a warning about use of 3 in sink context as only [1,2]
array goes into $a
. Yet, use of braces enforces the content between them to be treated as a singular entity. Thus, $a = ([1,2],3)
will entirely become the content of $a
.
With this respect the fact that ($a, $b) = ([1, 2], 3)
gets kind of destructured might seem confusing until we step back a little and see the braces on the LHS. They give us a list of scalars, and thus we get a case of list assignment again.
Then again, the documentation still have a lot of interesting details.
I don't think the parallel you drew is valid. my @a = [1, 2], 3
doesn't flatten - it contains two elements, one of which is an array. However, my (@a, $b)
does flatten and this is exactly the inconsistency I'm talking about. Your parallel would be okay if we talked about writing my @a, $b
for my (@a, $b)
but that's not the thing.
Also, correct me if I'm wrong but I also noticed that the "quasi-destructuring-assignment" @codesections showed won't work for swapping variables in place because you cannot reassign references.
This means: ($a, $b) = ($b, $a)
will work for swapping variables but (@a, @b) = (@b, @a)
won't and there isn't even an easy way to get around this.
This is not the first problem I've encountered with using the @ sigil (anything but $ or \, to be frank).
So far, the only argument I see is from @jnthn under that SO question of @codesections and I have to say it's anything but convincing:
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.
Even Jonathan proposes a perfectly reasonable workaround for my ($first, @rest) = @all
(more reasonable than this syntax in the first place) - what do we have for swapping @-sigilled variables?
what do we have for swapping @-sigilled variables?
The idiom I've seen for that is (@a, @b) Z= (@b, @a)
(or sometimes with «=»
instead of Z=
).
That's not to say that I entirely disagree with your point of view, just that we can handle that use case - and all it takes is one more character.
(I tentatively think that, if I'd been around when this decision was made, I would have argued for my ($first, @rest) = @all
to be non-flattening and to have handled flattening with a slurpy: my ($first, *@rest) = @all
. Wild speculation: I wonder if the thinking at the time was that we'd have more Perl programmers around and that syntax might look confusingly like perl's typeglobs?)
I don't think the parallel you drew is valid.
my @a = [1, 2], 3
doesn't flatten - it contains two elements, one of which is an array. However,my (@a, $b)
does flatten and this is exactly the inconsistency I'm talking about.
Yes, but assigning into an array and list assignment are two very distinct things (as in, these are two different types, each with their own STORE
implementation). An Array
creates new Scalar
containers for each array element. A List
is in itself immutable, so the only way it can have assignment semantics is when the List
was created with assignable things.
my ($first, *@rest) = @all
When we have an assignment, the LHS is just a List
, and so there is no way to convey the *
.
In general, assignment is a relatively late-bound thing. Any aggregate type can implement how it wants to react to an assignment by implementing the STORE
method. So ($first, @rest) = @all
compiles into ($first, @rest).STORE(@all)
. So what's really being argued over here is the semantics of List.STORE
.
By contrast, binding is a compiler special form. Types don't get to control how binding to them works. my ($first, *@rest) = @all
works because what's on the LHS is a Signature
and thus it compiles into a signature bind (with @all
first coerced into a Capture
). In Raku, destructuring and signature binding are the same thing, meaning that the entire power of the signature language is available.
I suspect the docs probably do mention "destructuring assignment" somewhere, but I think destructuring should only ever really be used to talk about the binding form.
Yes, but assigning into an array and list assignment are two very distinct things
Please keep in mind that you brought up "assigning into an array", I was trying to talk about lists (or something that appears to be lists) on the two sides of assignment.
What you just mentioned with STORE makes stuff even more interesting - if ($first, @rest) = @all
indeed boils down to ($first, @rest).STORE(@all)
, does that mean that a non-flattening list implements STORE with flattening semantics?
@2colours You probably mean something different, not flattening. Just consider:
sub foo(*@a) { dd @a }; foo [1,2],3;
my @a is List = [1,2],3; dd @a; dd @a.flat;
However,
my (@a, $b)
does flatten and this is exactly the inconsistency I'm talking about.
It doesn't because RHS ends up in @a
exactly as it is.
Please keep in mind that you brought up "assigning into an array"
No, I just used the standard Raku terminology. You wrote my @a = [1, 2], 3
. @a
is an Array
. It is being assigned into. Desugared, it's approximately my @a := Array.new; @a.STORE(([1, 2], 3));
.
I was trying to talk about lists
But "lists" is just some hand-wavy term; if we want to discuss language semantics, precision would be helpful. You wrote one example where the target of the assignment operation is an Array
(my @a = ...
) and another that alluded to the target of the assignment operation being a List
(my (@a, $b) = ...
).
does that mean that a non-flattening list implements STORE with flattening semantics?
There's no such concept as a non-flattening List
(nor a flattening List
); flattening or not is a property of an operation. There's just the semantics of List.STORE
, which are:
- Obtain an iterator over the data source, and then...
- If the target is a
Scalar
container, pull one value from the iterator and assign it - Otherwise, pass what remains to the
STORE
of the target
- If the target is a
And the semantics of Array.STORE
, which are:
- Discard the current content of the
Array
, if any - Obtain an iterator over the data source, and then...
- Pull a value from the iterator, assign it into a
Scalar
container, and bind that into the next array element
- Pull a value from the iterator, assign it into a
No, I just used the standard Raku terminology
At this point, I'm not even sure what you are talking about. Please let's just agree about the fact that @a = [1, 2], 3;
and Ë™@a = ([1, 2], 3);do the same thing has nothing to do with the semantics of
(@a, $b)or
(@a, @b)` either on the left handside or the right handside of an assignment. The rest is besides the point.
You wrote one example where the target of the assignment operation is an Array (my @a = ...) and another that alluded to the target of the assignment operation being a List (my (@a, $b) = ...).
Again, I have a hard time believing that you really don't know what I mean. Sorry that I haven't spent years of designing and implementing language backends and Raku in particular and in before, I'm not an English native, either. However, it seems to me that @codesections managed to understand my point just fine after having wondered about the same thing earlier.
In the context of dd (@a, $b)
, (@a, $b)
means a nested list and in the context of (@a, $b) = =([1, 2], 3)
, the same (@a, $b)
means a flat list. Am I being clear enough?
There's no such concept as a non-flattening List (nor a flattening List); flattening or not is a property of an operation
Again, I feel this might be a bit unnecessary. (@a, $b)
doesn't flatten when accessed as data. This means that the list itself has the data with the structure preserved.
The latter is the important point. From what I can tell, at the point of calling STORE, the structure is still preserved - what I take from this with a bit of a stretch is: the flattening behavior happens at a relatively high level and hence it's mostly a design decision, not a technical constraint. Blantly put: it works like this not because it has to but because this option won.
Anyways, I don't want to appear thankless - in fact I appreciate your pointers about the implementation of list assignment and the STORE method. This can help me understand the language better either way.
It doesn't because RHS ends up in @a exactly as it is.
Actually this is just a different matter of phrasing: you phrased it as someone who knows the implementation that Jonathan explained while I phrased it like codesections in the Stackoverflow question - as someone who tries to describe the experienced semantics. I hope this clears things up.
This is a bit like talking about "sunrise". From what we know scientifically. The Sun doesn't "rise" even according to the obsolete terms of mechanics but the experience is the same as if it rose.
I never regret knowing more about how these assignments actually work - it also makes me think that there aren't serious technical constraints to consider when choosing the semantics of these assignments. Therefore I would stick to the original question: what other arguments lead to accepting this behavior as intended?
@codesections I tried Z= and «=», they don't work:
my @a = 1,2;
my @b = 3,4;
(@a, @b) Z= (@b, @a);
dd @a, @b;
yields
Array @a = [3, 4]
Array @b = [3, 4]
I assume this is the same issue why you can't just write @a = @b; @b = @a;
Almost a year passed, and I'm thinking what to do with this issue.
Even though I still think it would be easier to live without any "list assignment" but I did see some useful properties of Array containers (like being able to present the same underlying containers in different ways while maintaining mutability of the very same data) so I'm rather thinking on the next step than "fixing" the current (@a, @b) = (@b, @a)
semantics.
I have the impression that the simplest and most consistent solution would be to support "itemized non-scalar" as a first class thing, different from a non-scalar container inside a scalar container. The name is not important - the important thing is that it STOREs into the underlying non-scalar on assignment, however, it exposes this operation as an "item/scalar assignment". Now, there could be a syntax (e.g a sigil) for wrapping a @variable
(or a %variable
) into this dummy so that something like (§@a, §@b) = (@b, @a)
could work exactly as I originally wished for (@a, @b) = (@b, @a)
.
To be honest, I'm not sure if the proposal is worth it but it would be nice to have. Maybe I could take on it if I learned something about the internals of Rakudo. I wonder if a demo implementation could be created using pure Raku and proxies.
Let me show you a simple way of locally "switch" your variables (using "binding" not "assignment"):
my @a = <a b c d>;
my @b = 1,2,3,4;
for @a, @b -> @b, @a {
say "a:", @a;
say "b:", @b;
}
# a:[1 2 3 4]
# b:[a b c d]
Binding does work indeed but it comes with some gotchas even in the plain (@a, @b) := (@b, @a)
form. The syntax looks nice but the semantics is too different from swapping the content of two arrays.
Long before (@a, @b) := (@b, @a)
can work, you'd need to be able to rebound an already bounded variable. But as soon as you declare a variable in any lexical scope it is bounded:
my @a;
@a := <a b c>; # Error at compile time: Cannot bind to '@a' because it was bound…
At raku´s high level, a "binding" is a one time operation (at lexical scope entering/cloning).
But nothing forbids you to implement, using raku's low level machinery, an slang where you can play dirty with the lexpad. :-)