luau-lang / luau

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

Implement table destructuring #617

Closed crywink closed 4 months ago

crywink commented 2 years ago

Description

A feature present and highly-used in JavaScript, table destructuring lets the developer assign table values to variables natively. I'd like to suggest this same (or functionally similar) functionality be implemented in Luau. Additionally, this feature can be used with special types. (examples below)

Usage

Variable Assignments for Arrays

With the ability to use unpack to destructure arrays, the need for this feature can seem negligible. Nevertheless, unpack cannot be used on dictionaries.

-- Current
local foo = {1, 2, 3}

local one, two, three = unpack(foo) -- 1, 2, 3
-- New
local foo = {1, 2, 3}

local {one, two, three} = foo -- 1, 2, 3

Variable Assignments for Dictionaries

-- Current
local foo = {
    red = "good";
    green = "better";
    blue = "best";
}

local red, green, blue = foo.red, foo.green, foo.blue
print(red) -- good
print(green) -- better
print(blue) -- best
-- New
local foo = {
    red = "good";
    green = "better";
    blue = "best";
}

local {red, green, blue} = foo
print(red) -- good
print(green) -- better
print(blue) -- best

Variable Assignments for Function Returns

-- Current
local function f()
    return {
        name = "John";
        age = 25;
        gender = "male";
    }
end

local data = f()
local name, age, gender = data.name, data.age, data.gender

print(name) -- John
print(age) -- 25
print(gender) -- male
-- New
local function f()
    return {
        name = "John";
        age = 25;
        gender = "male";
    }
end

local {name, age, gender} = f()
print(name) -- John
print(age) -- 25
print(gender) -- male

Variable Assignments for UserData & Vectors

-- Current
local Part = workspace.Part
local Position = Part.CFrame.Position
local X, Y, Z = Position.X, Position.Y, Position.Z

print(X, Y, Z) -- 0, 0, 0
-- New
local Part = workspace.Part
local {Position} = Part.CFrame
local {X, Y, Z} = Position

print(X, Y, Z) -- 0, 0, 0

Variable Assignments for Indexing Children (???)

-- Current
local Baseplate = workspace.Baseplate
print(Baseplate) -- Baseplate
-- New
local {Baseplate} = workspace
print(Baseplate) -- Baseplate

Destructuring Function Parameters

-- Before
local function f(data)
    local name = data.name
    local age = data.age
    local gender = data.gender

    print(name, age, gender) -- John, 25, male
end

f({
    name = "John";
    age = 25;
    gender = "male";
})
-- New
local function f({ name, age, gender })
    print(name, age, gender) -- John, 25, male
end

f({
    name = "John";
    age = 25;
    gender = "male";
})

Defining Alternative Identifiers (Questionable?)

In this example, fizz-buzz is not a valid identifier. You should be able to instead define an alternative identifier, like so.

local foo = {
    ["fizz-buzz"] = true;
}

local { fizzbuzz = "fizz-buzz" } = foo
print(fizzbuzz) -- fizz-buzz

Conclusion

There are more features that could be added once destructuring is implemented, like defining default function parameters. I overall believe that this, if implemented correctly, would be an extremely helpful QOL feature.

Dionysusnu commented 2 years ago
local foo = {
    1,
    2,
    3,
    one = "one",
    two = "two",
    three = "three",
}

local {one, two, three} = foo

Would this be 1, 2, 3 or "one", "two", "three"? The way it's proposed has ambiguity.

I think this would also have to go through the RFC process, which is more detailed for when the language receives significant changes.

crywink commented 2 years ago
local foo = {
    1,
    2,
    3,
    one = "one",
    two = "two",
    three = "three",
}

local {one, two, three} = foo

Would this be 1, 2, 3 or "one", "two", "three"? The way it's proposed has ambiguity.

I think this would also have to go through the RFC process, which is more detailed for when the language receives significant changes.

This would be "one", "two", "three" -- you're indexing one, two, and three.

Dionysusnu commented 2 years ago
local foo = {1, 2, 3}

local {one, two, three} = foo

So then, this is nil, nil, nil? That's not what the comment in your first example says. Or does the string key take priority, and is the array part of the table a fallback? That's also very confusing.

crywink commented 2 years ago
local foo = {1, 2, 3}

local {one, two, three} = foo

So then, this is nil, nil, nil? That's not what the comment in your first example says. Or does the string key take priority, and is the array part of the table a fallback? That's also very confusing.

When the table being destructured is an array, the assignment acts the same as unpack would. I agree it's somewhat confusing, but I think it makes the most sense given the fact that arrays and dictionaries are both encapsulated in curly brackets.

In JavaScript, arrays are wrapped in brackets, while objects (dictionaries) are wrapped in curly brackets. Hopefully this example I wrote gives you a good idea as to how array vs dictionary destructuring works.

const obj = {
    name: "John",
    records: [
        {
            date: "7/29/2022",
            text: "Hello World!"
        }
    ]
}

let {
    name,
    records: [
        { text }
    ]
} = obj;

console.log(name + " says " + text) // John says Hello World!

Since I'm bringing up the JavaScript examples, I'd also like to shine light on the fact that you could also assign new variable names to destructured values. Here's a good example of that using the same object from the previous example:

const obj = {
    name: "John",
    records: [
        {
            date: "7/29/2022",
            text: "Hello World!"
        }
    ]
}

let {
    name: objectName,
    records: [
        { text: firstRecordText }
    ]
} = obj;

console.log(objectName + " says " + firstRecordText) // John says Hello World!

Destructuring can be super powerful when you understand how it works and how it can be used. Let the examples above act as proof to that.

JohnnyMorganz commented 2 years ago

Just an FYI that an issue has already been made for this a while ago, but was closed as it should've been an RFC: https://github.com/Roblox/luau/issues/126#issuecomment-1005007173

If you want this to go any further than the previous one, I would recommend starting an RFC for it instead

JDaance commented 2 years ago

I'll just throw a lazy vote of "no", I love lua for being small and I like being able to keep the lua syntax in my head ❤️, and would like luau to stay as close to lua as possible. I am not a maintainer just a happy user

ghost commented 2 years ago

If an RFC were written about this feature, I think it'd be important to explain behavior with mixed tables, and behavior with __index.

local value = {secret = "key", 1, 2}
local {secret} = value -- Is `secret` `key` or `1`?
m-doescode commented 1 year ago

If an RFC were written about this feature, I think it'd be important to explain behavior with mixed tables, and behavior with __index.

local value = {secret = "key", 1, 2}
local {secret} = value -- Is `secret` `key` or `1`?

Perhaps for mixed tables, numeric indices are treated as regular keys, so in order to get 1 or 2, you would need to use numbers like this:

local value = {secret = "key", 1, 2}
local {secret} = value -- secret is secret
local {first = 1}

This wouldn't work with the current proposal, but maybe with what crywink suggested here:

Since I'm bringing up the JavaScript examples, I'd also like to shine light on the fact that you could also assign new variable names to destructured values. Here's a good example of that using the same object from the previous example:

It would be cool to have a syntax similar to as follows:

local {folderName = Name, Locked} = folder
-- Locked has no equals sign, so it is taken as Locked = Locked. folderName does, so it takes it from whatever the value is.
-- The value can't be anything other than an identifier or a number representing a numeric index
local {a = 2, b = 1} = {10,12}
print(a,b) -- 12,10

Although, this does bring up the problem with non-string and non-numeric indices and __index. How would we allow, for example, function indices? Or coroutines, or other tables, or userdata, etc...

Another thing is, for functions, how would typed arguments work?

-- New
local function f({ name, age, gender })
    print(name, age, gender) -- John, 25, male
end

f({
    name = "John";
    age = 25;
    gender = "male";
})

If I wanted to explicitly set "name" as string, how would I do it? Maybe like this?

local function f({ name: string, age: number, gender: string })
    print(name, age, gender) -- John, 25, male
end

Finally, for unwrapping variables, should we allow this behavior to work with assigning existing variables, or only creating new ones? If so, should we allow creation of global variables (defining non-local variables for the first time)? In both cases, this would require the syntax to work without the {} which may not be possible due to this example of ambiguity:

local a,b
f()
{a,b} = {1,2}
-- Can be confused as:
local a,b
f()({a,b}) = {1,2}

Of course, we can tell that this is a destructuring statement because of the equals sign, but this would require looking ahead which can add complexity to the parser.

Kampfkarren commented 1 year ago

We talked about this before in OSS Discord and to make sure that it's preserved. From what I recall, here was the outcome: