Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Structure matching syntax #95

Open
wants to merge 40 commits into
base: master
Choose a base branch
from
Open

Conversation

dphblox
Copy link

@dphblox dphblox commented Jan 29, 2025

Gathering consensus on the destructuring syntax inside of the braces (i.e. punting on other issues explicitly), so we can align on this with the community.

Rendered.

docs/syntax-structure-matching.md Outdated Show resolved Hide resolved
docs/syntax-structure-matching.md Outdated Show resolved Hide resolved
docs/syntax-structure-matching.md Outdated Show resolved Hide resolved
docs/syntax-structure-matching.md Outdated Show resolved Hide resolved
@jackdotink
Copy link
Contributor

I think that we should leave the proposed unpack syntax and other similar syntaxes out of this RFC. At the moment, they would not serve much use, and unpack already exists as a global function for that use. I think it will streamline the process of getting this RFC done and accepted or rejected if that is left for a later time.

@dphblox
Copy link
Author

dphblox commented Jan 29, 2025

From my POV, the thing that sunk the previous RFCs were exactly the questions that the unpack syntax and similar features answer. From Andy's prior comments, it's clear that the Luau team need to feel confident that destructuring can actually serve as a strong, intuitive, forwards-compatible language feature before its accepted, so I disagree that less detail is needed here. In fact, we should be proving to satisfaction that we can avoid all of the pitfalls beyond a "just about sufficient" spec.

That's the reason this is not an implementation RFC, by the way. The intention is that we align on overall direction first, and then implement in parts, as needed.

To quote Andy from the previous thread:

One of the difficult things about syntax extensions to programming languages is that reversing course after the fact is essentially impossible. This RFC does an admirable job of navigating constraints passed down to us from Lua and presenting a least-worst solution to this specific problem, but it doesn't handle assignments or array destructuring and there doesn't seem to be any path at all to get there.

We need the bar to be higher than least-worst if we're going to successfully maintain Luau's identity as a small, predictable language.

It's worth noting that this RFC already punts on many of the integration problems we found, such as how destructuring assignments should work (or if they should work, given myself and Hunter don't know of any languages that support this). If we punt on substantially more than we do already, we'll just be back to the old RFC, at which point I don't know what different result we expect to find.

So we need to decide on what our approaches will be for those facets of the feature, hence all of the extra syntax above what we already agreed upon. Whether we decide to support each feature or drop them from the spec, the decision must be documented, not deferred.

@dphblox
Copy link
Author

dphblox commented Jan 29, 2025

Dropping the unpack syntax from the spec in favour of basic indexing.

@rihok
Copy link

rihok commented Jan 30, 2025

Coming into this a bit late I guess, but wanted to just ask some questions. Is there some issue with copying the JavaScript syntax wholesale? It's quite well thought out and tried and tested. For example one question I have is, why is it necessary to have a dot prefix for the destructured keys instead of:

-- keyed table desctructuring
local { foo, bar } = t

-- indexed table destructuring
local a, b, ...rest = t
-- or
local [a, b, ...rest] = t

@dphblox
Copy link
Author

dphblox commented Jan 30, 2025

I don't entirely disagree with that idea personally, but IIRC others shot this down already.

The main trouble comes from the fact that array literals would look the same as keyed table destructuring:

-- this would not work as expected, despite looking visually similar
local { foo, bar } = { "foo", "bar" }

This was considered a showstopper for that syntax in particular.

Beyond that, the square brackets idea has been floated a few times, but each time there's generally some disapproval since the only other time Luau uses square brackets is for indexing; we dont use [] to declare arrays like JS does.

I think JS has a well designed syntax for JS, but in the context of Luau there's multiple arguments against some of its choices.

As for:

local foo, bar, ...rest = t

This would be backwards incompatible, as Luau supports multiple returns. Today, this runs:

local foo, bar, baz = { "hello" }

print(foo, bar, baz) --> { [1] = "hello" } nil nil

@surfbryce
Copy link

surfbryce commented Jan 31, 2025

I very much prefer something along the lines of what @rihok suggested at the very bottom of the code sample, possibly something that outlines similarly to:

local KeyedTable = {
    Hello = true;
    [5] = false;
    [Instance] = 5;
}
local PackedTable = table.pack(1, 2)
local NestedTable = {
    Nested = {
        World = true;
    };
}

local [ ["Hello"] = A, [5] = B, [Instance] = C ] = KeyedTable
local [ [1] = D, [2] = E, ["n"] = F ] = PackedTable
local [ ["Nested"] = [ ["World"] = G ] ] = NestedTable

I assume square brackets here would be an understandable extension of the Luau language given that square brackets are only used for indexing (a point illustrated by @dphblox) and table destructuring is simply an abstraction of that.

A few things to touch on surrounding this:

  1. Most of the time destructuring will be done on string literal indices, so potentially string literals can be written without the index syntax to appear as:
local [ Hello = A, [5] = B, [Instance] = C ] = KeyedTable
  1. There is concern about whether or not the equals sign could indicate assignment. Another alternative is to use colons instead, this would align with the ideology that table types use where we understand it as mapping a property to something else (in the case of table types, another type): so - in this case - we would be mapping a property to a variable name.
local [ Nested: [ World: G ] ] = NestedTable
  1. When used for global variable instantiation we run into the issue where it can be confused for actual indexing by the parser:
local something = someTable
[ SomeIndex: GlobalVariable ] = someTable

This has the same behavior as parentheses do in the language and results in roughly the same error contextually. This shouldn't be seen as a drawback and instead a reinforcement of the languages already well established behavior.

I don't know what the implications are on parser complexity in regards to this or whether or not this fits within the purview/scope of this RFC. But I believe it's a fun and reasonable suggestion purely in terms of syntax.

@dphblox
Copy link
Author

dphblox commented Feb 7, 2025

I assume square brackets here would be an understandable extension of the Luau language given that square brackets are only used for indexing (a point illustrated by @dphblox) and table destructuring is simply an abstraction of that.

I would be somewhat OK with this, but it's a little weird that we would use square brackets both to surround the keys and to surround the whole matcher.

Perhaps it would help with the "array problem":

local [ these, are, keys ] = data
local { these, are, consecutive, values } = data

But this would still be inverse of JS, which could be a stumbling block. IIRC, this is part of what caused the original RFC to fail, hence the dot prefixes. The older RFC has more details on that whole thing.

There is concern about whether or not the equals sign could indicate assignment. Another alternative is to use colons instead

Colons are used for type annotations, so it would be almost certainly inappropriate to use them here.

@dphblox
Copy link
Author

dphblox commented Feb 7, 2025

Here, let me throw out a few crazy syntax ideas, specifically trying to figure out how best to make array/dict difference obvious:

local {in these, are, keys} = data
local {...these, are, consecutive, values} = data
local in {these, are, keys} = data
local ...{these, are, consecutive, values} = data

@dphblox
Copy link
Author

dphblox commented Feb 7, 2025

I think I'll try and write down my full chain of thoughts at the moment around the array and reassignment comments from the old RFC, as well as some syntax comments more generally. Take all this syntax with a pinch of salt - it's just to illustrate.

Unlike JS, Luau allows you to shuttle around multiple values at once, making unpacking the natural paradigm for array destructuring:

local these, are, consecutive, values = table.unpack(data, 1, data.n)

So if we were to suggest any syntax there, it really feels like it ought to be expression-side, not assignment-side:

local these, are, consecutive, values = ...data

Naturally I would be inclined to extend that to keys (thus making this RFC redundant), but that introduces the problem of having to specify the keys twice in almost all situations:

local these, are, keys = ...data["these", "are", "keys"]

That's the grounds upon which I think the original destructuring RFC was made, and probably what we should focus on solving. That also aligns with the discussions we've had above. Trying to make this work for arrays seems misguided and I don't reckon it's even relevant.

Now onto syntax. One of my earlier explorations was having a "destructuring assignment", though I was concerned about it looking too much like a compound assignment, or like it's unpacking values positionally:

local these, are, keys ...= data

In my eyes, the problem there is that throughout the rest of Luau, the names of those identifiers are not significant - only their positions are. Therefore, if the names are going to be significant, it's worthwhile to alter how those names are presented to make that clear. That's what the original RFC did.

I'd say dot prefixes seem like a sensible and low-syntax way to indicate those names are keys.

local .foo, .bar, .baz = data

Of course, without the local indicating this is a new variable declaration, this would be ambiguous syntax. I personally only believe there is one valid use case for that, though, and that would be forward declaration of uninitialised variables:

local thing

local function usesThing()
    task.delay(1, function()
        print(thing)
    end)
end

local otherThings, foo, bar
.thing, .otherThings, .foo, .bar = getThings()

Forward declaration is a little bit of a code smell IMO (though not always!), it feels unintuitive for people to destructure in this position, and JS doesn't support destructuring when assigning - only when declaring - so I would be personally OK dropping this requirement, though I'm not the person who brought it up in the first place.

So all of that is my bias coming into this. I'm not saying we should do any of the above, but that's the general direction I lean in philosophically.

My aim is to try and enmesh that with the previous RFC and the syntax everyone seemed to be OK with at the time.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.

6 participants