1
0
Fork 0
mirror of https://github.com/luau-lang/rfcs.git synced 2025-04-06 19:40:56 +01:00
rfcs/docs/syntax-structure-matching.md
Daniel P H Fox (Roblox) 86e71dec4f
Simplify
2025-03-03 10:58:47 -08:00

7.6 KiB

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, 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:

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:

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:

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

...whereas in JavaScript this would look like:

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:

// 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.

{ }

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.

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

This desugars to:

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

Dot keys with names

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

{ myFoo = .foo, myBar = .bar }

This desugars to:

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

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.

{ .foo, .bar }

This desugars to:

{ foo = .foo, bar = .bar }

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

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

Nested structure

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

{ .foo.bar }

This desugars to:

{ bar = .foo.bar }

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

bar = data["foo"]["bar"]

Alternatives

Unpack syntax

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

{ unpack foo, bar }

Instead, for unpacking arrays, this proposal suggests:

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

Or alternatively, using table.unpack:

foo, bar = table.unpack(data)

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

{ foo, bar }

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:

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.

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".

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.

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:

local foo = bar

{ } -- bar { }?

Such call sites will need a reserved keyword prior to the matcher to dispel the ambiguity.

in { myFoo = .foo }

That said, this would only manifest if attempting to add destructuring to re-assignment operations, which isn't done in other languages and has limited utility (only really being useful for late initialisation of variables).