Luau: Table destructuring for variables and type definitions

Particularly when using modules, I often find myself wanting to access the contents of a table, where I don’t actually care about the table itself. For example:

local Fusion = require(ReplicatedStorage.Fusion)
local New = Fusion.New
local Children = Fusion.Children
local OnEvent = Fusion.OnEvent
local OnChange = Fusion.OnChange

There’s two problems with the above code;

  1. We’ve polluted the namespace with Fusion - a table we only need because we’re requiring a module. We pull out all the members we care about and store them separately for conciseness.
  2. We’re taking up a lot of space with repetitive statements, that contain unnecessary duplication of the member names on each line.

This can also happen if you store your Luau types in a dedicated module - it gets especially awkward with generics, because they have to be re-specified:

local Types = require(Package.Types)
type StateOrValue<T> = Types.StateOrValue<T>
type Set<T> = Types.Set<T>
type Dependency<T> = Types.Dependency<T>

Therefore, I propose a general solution to this kind of problem - destructuring. This already exists in other languages like JavaScript; the idea is to allow multiple variables to be declared based on the contents of a table:

local tbl = {
    foo = 2,
    bar = true
}

local {foo, bar} = tbl

print(foo, bar) -- 2 true

This would simplify the above examples and eliminate a lot of duplication:

local {New, Children, OnEvent, OnChange} = require(ReplicatedStorage.Fusion)
type {StateOrValue, Set, Dependency} = require(Package.Types)

It remains to be seen whether some different syntax is preferable, or how well this deals with edge cases and backwards compatibility concerns.

59 Likes

I feel it’d be appropriate to suggest adding shorthand properties too?

Rather than doing the following:

local foo = 2
local bar = true

return {
    foo = foo,
    bar = bar,
}

We could use shorthand, like you’d expect to be able to use in ES6 objects:

local foo = 2
local bar = true

return { foo, bar }
3 Likes

That’s ambiguous - that code snippet initialises an array with two values taken from the variables foo and bar!

8 Likes

That’s true. I forgot that {} was for arrays too!

2 Likes

I support this as I consider it pretty damn useful for my exhaustive pattern matching package in Roblox-TS. Would probably consider it obsolete if destructuring structs wasn’t possible.

const isOdd = (x: number) => x % 2 === 1;

expect(
	match({ x: 2 })
		.with({ x: when(isOdd) }, ({ x }) => `${x} is odd`) // destructuring
		.with(__, ({ x }) => `${x} is even`)
		.exhaustive(),
	).to.equal("2 is even");

expect(
	match({ foo: { bar: 1 } })
		.with({ foo: { bar: _select() } }, (a) => a) // returns 1; destructuring foo; and selecting a deep value.
		.exhaustive(),
	).to.equal(1);

Another strong use-case OP mentioned is import capabilities which I will show in TypeScript and Rust which is very valuable when you only want specific nested objects of the namespace.

import { RunService, ReplicatedStorage, Workspace} from "@rbxts/services"

and in Rust:

use serenity::{
    client::{Context, EventHandler}
}

Additionally, destructuring structs is common syntax for many modern languages such as:

  • C++
  • C#
  • Erlang
  • Haskell
  • JavaScript/TypeScript
  • Rust
  • Swift
    … The list goes on!

A bad solution is taking use of janky syntax:

function destruct(obj: {})
   local t = {}
   return function(args) do 
      for i, v in ipairs(args) do 
          table.insert(t, i, v)
      end
      return unpack(t)
   end
end

local foo, bar, baz = destruct Table { "foo", "bar", "baz" } -- Which is very verboose!
5 Likes
local New,Children,OnEvent,OnChange do
	local Fusion = require(ReplicatedStorage.Fusion)
	New = Fusion.New
	Children = Fusion.Children
	OnEvent = Fusion.OnEvent
	OnChange = Fusion.OnChange
end

You can put the Fusion variable in a scope which stops it from leaking out, if that is a major issue.

Since you mentioned Javascript destructuring, does this proposal also include array destructuring and other features of Javascript destructing? The examples provided don’t describe that well what the syntax and semantics are beyond the most basic cases.

local [x,y] = {1,2} -- x=1,y=2
local [ [a,b]] = {{1,2}} -- works while nested
--     ^ this space is required
local [] = {} -- empty assignment
-- is any value allowed for this since it does nothing?
local [,,w,,] = {1,2,3,4,5} -- commas to skip elements, w=3
-- ellipsis syntax
local [...c] = {1,2,3} -- check for the first nil or use the length operator?
local [...[f,g,h]] = {1,2,3}
local [...{u}] = {}
local {...U} = {a=1,b=2}
-- defaults and nesting inside of an index
-- for defaults, what would be the order of evaluation?
-- should they be evaluated even if not needed?
local {q=1} = {} -- x=1
local {x:n} = {x=1} -- n=1
local {x:m=2} = {} -- m=2
local [k=1] = {} -- k=1
local {x:[i,o]} = {x={1,2}} -- i=1,o=2
local {x:[I,O=2]={1}} = {} -- I=1,O=2
local loop = {}
loop.x = loop
local {x:{x:{x:{x:{x:p}}}}} = loop -- p=loop
local aloop = {}
aloop[1] = aloop
local [ [ [ [ [ [ [ [j]]]]]]]] = aloop -- j=aloop

Will destructuring support metamethods? I presume that people may use destructuring on game if this is allowed:

local {Workspace,Players,ReplicatedStorage} = game
-- uses __index, not GetService
-- so it has the same problems as doing game.ServiceName

There could be a new metamethod added for indexing via destructuring (to allow only destructuring properties), but this would be confusing.

If metamethods are supported, is the order of indexing operations guaranteed?

local {a,b} = foo() -- is it guaranteed that .a happens before .b, or vice versa?

Currently, multiple assignments and table constructors do not guarantee the order of assignment, maybe something similar should be done for this too.

Not supporting metamethods isn’t a great alternative, things like proxy tables would benefit from supporting metamethods.

With type destructuring, does it evaluate the expression or only use the expression for the purposes of types? What are the limitations on what values can provided?

local m = require(module)
type {a,b} = m -- is this ok?
3 Likes

I deal with lots of object-oriented programming in my projects, I know for a fact this would help define sub/super classes a lot more effectively.

This is the perfect opportunity to improve LuaU; readability improves, writing efficiency improves.

4 Likes

Bump. This would increase the readability of Luau by a lot.

I know this should be brought to those who made LuaU, but you could see the same concept in Rust programming language.
I would really love this concept of table destructuring!

1 Like

This extension was proposed here Create table-destructuring.md by crywink · Pull Request #629 · Roblox/luau · GitHub
But it didn’t get to a conclusion that everyone is happy with.

1 Like