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
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
db3e7bf
Initial work
dphblox Jan 25, 2025
c0cdcaf
Let's explore a different direction
dphblox Jan 25, 2025
01a3a92
Completed
dphblox Jan 26, 2025
d78a141
Last minute polish
dphblox Jan 26, 2025
4dccd2c
Rename files
dphblox Jan 26, 2025
e473d5d
Update syntax-multiple-index.md
dphblox Jan 26, 2025
f5ab7a8
Update outdated code sample
dphblox Jan 26, 2025
2817cd6
Fix ambiguity
dphblox Jan 26, 2025
f505820
Update syntax-multiple-index.md
dphblox Jan 26, 2025
8c69445
world's first Luau proposal that reuses the `until` keyword
dphblox Jan 26, 2025
7867133
`until` as operator
dphblox Jan 26, 2025
b1e00d1
For loop example
dphblox Jan 26, 2025
d49d29f
Nope that's ambiguous
dphblox Jan 26, 2025
004b916
Add local...in syntax
dphblox Jan 26, 2025
9354bbd
Add comment on braces prefix
dphblox Jan 26, 2025
b4a786d
Reworking
dphblox Jan 28, 2025
198cf7a
This seems OK
dphblox Jan 29, 2025
fcc3e32
get renamed, nerd
dphblox Jan 29, 2025
3bb50be
typo
dphblox Jan 29, 2025
10498ab
Tuple-like tables
dphblox Jan 29, 2025
da49909
Delimiters for bindings
dphblox Jan 29, 2025
c2fadf8
Update note around nested structure =
dphblox Jan 29, 2025
bf1a719
Simplify nested structure example
dphblox Jan 29, 2025
2cc95ab
Simplify nested structure wording
dphblox Jan 29, 2025
f6c1457
Show desugaring for nested structure
dphblox Jan 29, 2025
3ef7e09
unpack syntax
dphblox Jan 29, 2025
8931e7a
Remove consecutive key downside
dphblox Jan 29, 2025
f068d83
Update nested structure example
dphblox Jan 29, 2025
4efa1ee
Update nested structure example
dphblox Jan 29, 2025
0dccd1b
Whoops typos
dphblox Jan 29, 2025
43fc1a2
Simplify nested structure example
dphblox Jan 29, 2025
4d665f3
Fix redundant assignment
dphblox Jan 29, 2025
0d205d4
Specify type error behaviour
dphblox Jan 29, 2025
ad1b7af
Punt on type declarations
dphblox Jan 29, 2025
eaf45cc
Discard unpack syntax
dphblox Jan 29, 2025
7eed972
Add table.unpack comment
dphblox Jan 29, 2025
5df5e03
Right hand side identifiers
dphblox Feb 7, 2025
ee3c976
Fix syntax highlighting
dphblox Feb 7, 2025
a85808c
Whooops
dphblox Feb 7, 2025
edc415d
Fully qualified paths
dphblox Feb 7, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
334 changes: 334 additions & 0 deletions docs/syntax-structure-matching.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,334 @@
# Structure matching

## Summary

Agree on the generic syntax for *structure matching* - a prerequisite to implementing destructuring in any part of Luau.

This is intended as a spiritual successor to the older ["Key destructuring" RFC by Kampfkarren](https://github.com/luau-lang/rfcs/pull/24), which was very popular but requires more rigour and wider consensus to have confidence implementing the feature.

**This is not an implementation RFC.**

## Motivation

Simple indexes on tables are very common both in and outside of Luau. A common use case is large libraries. It is common in the web world to see something like:

```js
const { useState, useEffect } = require("react");
```

...which allows you to quickly use `useState` and `useEffect` without fully qualifying it in the form of `React.useState` and `React.useEffect`. In Luau, if you do not want to fully qualify common React functions, the top of your file will often look like:

```lua
local useEffect = React.useEffect
local useMemo = React.useMemo
local useState = React.useState
-- etc
```

...which creates a lot of redundant cruft.

It is also common to want to have short identifiers to React properties, which basically always map onto a variable of the same name. As an anecdote, a regex search of `^\s+local (\w+) = \w+\.\1$` comes up 103 times in the My Movie codebase, many in the form of indexing React properties:

```lua
local position = props.position
local style = props.style
-- etc...
```

...whereas in JavaScript this would look like:
```js
const { position, style } = props

// Supported in JavaScript, but not this proposal
function MyComponent({
position,
style,
})
```

React properties are themselves an example of a common idiom of passing around large tables as function arguments, such as with HTTP requests:

```js
// JavaScript
get("/users", ({
users,
nextPageCursor,
}) => { /* code */ })
```

## Design

This proposal does not specify any specific locations where this syntax should appear. Instead, the aim is to get consensus on the syntax we would be most comfortable with for all instances of destructuring we may choose to implement at a later date.

In particular, this proposal punts on implementation at sites of usage:

- Destructuring re-assignment (as opposed to destructuring `local` declarations)
- Defaults for destructured fields (unclear how this interacts with function default arguments)
- Unnamed function parameters (destructuring a parameter doesn't name the parameter)
- Type declarations on keys (for providing types when destructuring a function argument)

The purpose of this proposal is to instead find consensus on specific syntax for the matching itself.

This proposal puts forward a superset of syntax, able to match any table shape, with logical and simple desugaring, giving a rigorous foundation to the previously agreed-upon concise syntaxes.

### Structure matching

This proposal will use the term *structure matcher* to refer to syntax for retrieving values from table structures.

The most basic structure matcher is a set of empty braces. All matching syntax occurs between these braces.

```Lua
{ }
```

Empty structure matchers like these are not invalid (they still fit the pattern), but aren't very useful - linting for these makes sense.

#### Basic matching

This is the most verbose, but compatible way of matching values.

Keys are specified in square brackets, and are allowed to evaluate to any currently valid key (i.e. not `nil`, plus any other constraints in the current context).

To save the value at that key, an identifier is specified to the left of the key. An `=` is used to indicate assignment.

```Lua
{ foo = [1], bar = [#data] }
```

This desugars to:

```Lua
foo, bar = data[1], data[#data]
```

#### Dot keys with names

Keys that are valid Luau identifiers can be expressed as `.key` instead of `["key"]`.

```Lua
{ myFoo = .foo, myBar = .bar }
```

This desugars once to:

```Lua
{ myFoo = ["foo"], myBar = ["bar"] }
```

Then desugars again to:

```Lua
myFoo, myBar = data["foo"], data["bar"]
```

#### Dot keys without names

When using dot keys, the second identifier can be skipped if the destination uses the same identifier as the key.

```Lua
{ .foo, .bar }
```

This desugars once to:

```Lua
{ foo = .foo, bar = .bar }
```

Then desugars twice to:

```Lua
{ foo = ["foo"], bar = ["bar"] }
```

Then desugars again to:

```Lua
foo, bar = data["foo"], data["bar"]
```

#### Nested structure

Keys can be chained together to match values in nested tables.

```Lua
{ .foo.bar }
```

This desugars once to:

```Lua
{ bar = .foo.bar }
```

Then desugars twice to:

```Lua
{ bar = ["foo"]["bar"] }
```

Then desugars again to:

```Lua
bar = data["foo"]["bar"]
```

To avoid fully qualifying multiple paths, parentheses can be used to share a common prefix:

```Lua
{ .foo(.bar, myBaz = ["baz"]) }
```

This desugars once to:

```Lua
{ .foo.bar, myBaz = .foo["baz"] }
```

Then desugars twice to:

```Lua
{ foo = ["foo"]["bar"], myBaz = ["foo"]["baz"] }
```

Then desugars again to:

```Lua
local bar, myBaz = data["foo"]["bar"], data["foo"]["baz"]
```

## Alternatives

### Unpack syntax

Dedicated array/tuple unpacking syntax was considered, but rejected in favour of basic syntax.

For unpacking arrays, this proposal suggests:

```Lua
{ foo = [1], bar = [2] }
```

Or alternatively, using `table.unpack`:

```Lua
foo, bar = table.unpack(data)
```

For disambiguity with other languages, we would still not allow:

```Lua
{ foo, bar }
```

The original `unpack` syntax is listed below.

Instead of listing out consecutive numeric keys, `unpack` would be used at the start of a matcher to implicitly key all subsequent items.

```Lua
{ unpack foo, bar }
```

This would desugar once to:

```Lua
{ foo = [1], bar = [2] }
```

Then desugars again to:

```Lua
foo, bar = data[1], data[2]
```

`unpack` would have skipped dot keys and explicitly written keys. If an explicit key collided with an implicit key, this would be a type error.

```Lua
{ unpack foo, bar = [true], baz, .garb }
```

This would desugar once to:

```Lua
{ foo = [1], bar = [true], baz = [2], garb = ["garb"] }
```

Then desugars again to:

```Lua
foo, bar, baz, garb = data[1], data[true], data[2], data["garb"]
```

### Indexing assignment

A large amount of effort was poured into finding a way of moving the destructuring syntax into the middle of the assignment.

A `.=` and/or `[]=` assignment was considered for this, for maps and arrays respectively:

```Lua
local amelia, bethany, caroline .= nicknames
local three, five, eleven []= numbers
```

However, this was discarded as it does not align with the design of other compound assignment operations, which mutate the left-hand-side and take the right-hand-side of the assignment as the right-hand-side of the operation itself.

```Lua
local foo = {bar = "baz"}
foo .= "bar"
print(foo) --> baz
```

Many alternate syntaxes were considered, but discarded because it was unclear how to introduce a dinstinction between maps and arrays. They also didn't feel like they conformed to the "shape of Luau".

```Lua
local amelia, bethany, caroline [=] nicknames
local amelia, bethany, caroline ...= nicknames
local ...amelia, bethany, caroline = nicknames
```

### Type-aware destructuring

Another exploration revolved around deciding between array/map destructuring based on the type inferred for the right-hand-side.

However, this was discarded because it made the behaviour of the assignment behave on non-local information, and was not clearly telegraphed by the syntax. It would also not work without a properly inferred type, making it unusable in the absence of type checking.

### Multiple indexing

A syntax for indexing multiple locations in a table was considered, but rejected by the Luau team over concerns it could be confused for multi-dimensional array syntax.

```Lua
local numbers = {3, 5, 11}

local three, five, eleven = numbers[1, 2, 3]
```

### Don't do anything

This is always an option, given how much faff there has been trying to get a feature like this into Luau!

However, it's clear there is widespread and loud demand for something like this, given the response to the previous RFC, and the disappointment after it was discarded at the last minute over design concerns.

This proposal aims to tackle such design concerns in stages, agreeing on each step with open communication and space for appraising details.

## Drawbacks

### Structure matchers at line starts

This design precludes the use of a structure matcher at the start of a new line, among other places, because of ambiguity with function call syntax:

```Lua
local foo = bar

{ } -- bar { }?
```

Such call sites will need a starting token (perhaps a reserved or contextual keyword) to dispel the ambiguity.

We could mandate a reserved or contextual keyword before all structure matchers:

```Lua
match { myFoo = .foo }
in { myFoo = .foo }
```

But this proposal punts on the issue, as this is most relevant for only certain implementations of matching, and so is considered external to the main syntax. We are free to decide on this later, once we know what the syntax looks like inside of the braces, should we agree that braces are desirable in any case.