lloyAlloy on GitHub

← Every RFC

Parameter destructuring

SyntaxImplementedThe file on GitHub →

Summary

A parameter may be a table pattern instead of a name: function draw({ x, y }: Point) binds x and y from the argument. A field may carry its own type, function foo({ bar: string, baz: number }), and the pattern then states the parameter's type. ...rest takes the fields the pattern does not name.

Motivation

A function that takes an options table reads its fields on the first line of every body:

local function spawn(options: SpawnOptions)
    local model, at, owner = options.model, options.at, options.owner
    ...
end

The names the body uses are three lines from the signature. A reader of the call site sees spawn({ model = m, at = p, owner = plr }) and a reader of the signature sees one word, options.

Alloy already destructures a binding and a loop:

local { name, hp } = player
for _, { x, y } in points do

The parameter list is the position where it is missing.

Design

The form

A parameter is a name, or a table pattern:

local function draw({ x, y }: Point)
    print(x, y)
end

draw(origin)

The pattern takes the same entries a local pattern takes:

and one entry a local pattern has no use for:

The type of the parameter

Two spellings, and the parameter's type comes out the same:

-- The type is named, and the pattern only picks names out of it.
local function draw({ x, y }: Point) end

-- No name exists for the shape, so the pattern states it.
local function foo({ bar: string, baz: number }) end

The second is sugar for (arg: { bar: string, baz: number }). A pattern that carries no field type and no annotation is an error: the compiler will not guess the shape.

error(3.x): TypeError: `{ bar, baz }` has no type; annotate the
parameter, `{ bar, baz }: Options`, or type each field

A pattern may not mix a field type and an annotation. One of them states the shape:

error(3.x): TypeError: `{ bar: string }: Options` states the shape
twice; drop the field types or drop the annotation

...rest

...rest binds the fields the pattern does not name, as a table:

local function tag({ id, ...rest }: Attributes)
    -- id is a string; rest holds every other field of Attributes.
end

Its type is the annotation's index type when the annotation has one ({ [string]: T } gives rest: { [string]: T }). A pattern that states its own field types gives rest: { [string]: unknown }, and the author narrows it where it is read. ...rest comes last; a name after it is an error.

This ... is not Luau's varargs. function f(...: number) still means the argument list, and a pattern's ...rest means the rest of one table. The two may stand in one signature:

local function log({ level }: Options, ...: string) end

Emit

Luau has no parameter destructuring, so the emit takes a parameter and opens it on the same line. The emit stays on the source's own lines:

local function draw({ x, y }: Point)
    print(x, y)
end
local function draw(_p1: Point) local x, y = _p1.x, _p1.y
    print(x, y)
end

...rest emits the table-copy the array form already emits for its tail, minus the named keys.

A struct argument keeps its metatable: the emit reads fields off the value and never rebuilds it.

Where it applies

Every parameter list: a local function, a function expression, a method in an impl, a method signature and a default body in a trait, a macro, and a declare block's signature. A remote declaration takes no pattern, because the wire layout reads the parameter names; the error says so.

self is never a pattern.

Errors

The language server

Drawbacks

Alternatives

Prior Art

Unresolved questions