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

Implement table destructuring #617

Open
crywink opened this issue Jul 29, 2022 · 9 comments
Open

Implement table destructuring #617

crywink opened this issue Jul 29, 2022 · 9 comments
Labels
enhancement New feature or request

Comments

@crywink
Copy link

crywink commented Jul 29, 2022

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.

@crywink crywink added the enhancement New feature or request label Jul 29, 2022
@Dionysusnu
Copy link

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
Copy link
Author

crywink commented Jul 29, 2022

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
Copy link

Dionysusnu commented Jul 29, 2022

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
Copy link
Author

crywink commented Jul 30, 2022

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
Copy link
Contributor

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:
#126 (comment)

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

@JDaance
Copy link

JDaance commented Jul 30, 2022

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
Copy link

ghost commented Jul 31, 2022

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
Copy link

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
Copy link
Contributor

Kampfkarren commented Mar 30, 2023

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

  • There's probably no way the syntax local { a, b } = x will be accepted. It is not clear if this is array-unpacking (a = x[1], b = x[2]) or dictionary-unpacking (a = x.a, b = x.b). Misunderstanding this will cause very subtle logic errors that might not be caught by non-strict type checking.
  • local [a, b] = x also would not work, because [] isn't used for arrays in Luau anyway, but also it causes issues with nesting where local [[a, b], c] is parsed as a multi-lined string from [[.
  • local { .a, .b } = x might work for dictionary-unpacking, since it's more clear what exactly it's doing. It also fits defaults potentially with something like local { .a = 1 } = x.
  • Array-unpacking maybe doesn't get in immediately. I think we had a solution for this but I don't remember what it was, or if it was readable

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Development

No branches or pull requests

6 participants