luau-lang / luau

A fast, small, safe, gradually typed embeddable scripting language derived from Lua
https://luau.org
MIT License
4.05k stars 382 forks source link

Support table keys destructuring and spread #126

Closed rimuy closed 2 years ago

rimuy commented 3 years ago

Concept

Recently the language has gained support to if-then-else expressions, which shortened the way we deal with conditional values, rather than using if statements. On that note, I'd like to suggest adding support to tables destructuring and spread the same way typescript does with objects.

That way it would not only bring a key into the scope without needing to create a variable for the table and for every key we want to use, but also make our code a lot smaller. Of course, in Luau's case, that would only apply to string indexes, since we can make a tuple via table.unpack and reference the returned values likewise.

Examples

Merging dictionaries

Currently there is not way to destructure string indexed values in a table, and we cannot call unpack to do so, since that's used for number indexed values to return tuples, like said before. If we wanted to do so, we would need to create an utility function for that case. (e.g. merge, assign)

To resolve that problem, we could use ... as the spread operator, just like we do when handling varargs, but instead placing it before a table reference, inside a table.

For example, here we want to create a new table that contains all the keys from t1 and t2:

Current

local t1 = { a = 5 }
local t2 = { b = 10, c = 20 }

local newTbl = {}

local function assign(t)
    for k, v in pairs(t) do
        newTbl[k] = v
    end
end

assign(t1)
assign(t2)

print(newTbl) -- { ["a"] = 5, ["b"] = 10, ["c"] = 20 }

With spread

local t1 = { a = 5 }
local t2 = { b = 10, c = 20 }

print({ ...t1, ...t2 }) -- { ["a"] = 5, ["b"] = 10, ["c"] = 20 }

For the record, we could also do the same above without variables:

{ ...{ a = 5 }, ...{ b = 10, c = 20 } }

Referencing Keys

Let's say we have the functions foo and bar. Both takes as a parameter a destructured table.

type Point = { x: number, y: number }
type ExtendedPoint = { x: number, y: number, z: number }

Current

local function foo(point: Point)
    print(point.x + point.y)
end

local function bar(point: ExtendedPoint)
    print(point.x * point.y * point.z)
end

foo({ x = 1, y = 2 })
bar({ x = 10, y = 20, z = 30 })

With destructuring

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

local function bar({ x, ...rest }: ExtendedPoint)
    print(x * rest.y * rest.z)
end

foo({ x = 1, y = 2 })
bar({ x = 10, y = 20, z = 30 })

Using spread inside a table that is already being destructured, creates a new table that contains all the keys that were not referenced like x.

Nested Tables

local props = {
    a = { name = "fizz" },
    b = { name = "buzz" },
    c = { name = "fizzbuzz" }
}

local {
    a = { name = fizzVar },
    b = { name = buzzVar },
    c = { name = fizzbuzzVar }
} = props

print(fizzVar, buzzVar, fizzbuzzVar) -- fizz buzz fizzbuzz

Key Assignment

In addition, it would also be nice to assign the reference of a key to a new one:

local { foo = a, bar = b, baz = c } = { foo = "luau", bar = 3, baz = true }

assert(a == "luau")
assert(bar == 3) -- Should error, because the key reference was assigned to "b"

This would be a great alternative to creating variables for aliases when working with libraries that returns a dictionary (e.g. roact):

local { Component, createElement = e } = require(Packages.Roact)

local Button = Component:extend("Button")

function Button:render()
    return e("TextButton", ...)
end
Halalaluyafail3 commented 3 years ago

The proposed spread syntax is ambiguous: {...-t1,...-t2} could be interpreted as merging the results of -t1 and -t2, or it could be interpreted as doing subtraction with the first variable argument and t1 and t2. Currently it is interpreted as doing subtraction.

Would {...{1},...{2}} be {2}, {1}, or {1,2}? {2} and {1} makes sense if it is just mapping keys into the table (the order of assignments is undefined). {1,2} makes sense if it doing array concatenation.

I believe that having functions do these tasks is better than having a built in syntax. You mentioned unpack, but that isn't a good function to use for copying arrays. {table.unpack(t)} will work fine if t is small, but if it is too large then it'll error.

You said that want support destructuring the same way Typescript does, but in the examples you use = for property renaming while Typescript uses = for default value.

local k = 1
local {x=k} = {} -- declare x as 1 or declare another k with nil?
local c = 2
local {x=v=c} = {} -- declare v with 2, confusing
zeux commented 2 years ago

This issue by itself isn't actionable so I'm going to close this.

Both features would require an RFC to be considered. OTOH spread is unlikely to be accepted at this point in time because it's difficult to remove implicit spread in tail position so explicit spread makes the spread story "fragmented", and there's some implementation challenges - an ideal proposal for spread would come up with some idea of how to move away from implicit spread... unsure how.

Destructuring is much more straightforward (don't treat this as a promise that we'll get that feature...). I think Quenty had plans to submit an RFC for that.