Some languages allow you to write something like
x, y, z = (1, 2, 3)
Which is equivalent to
x = 1
y = 2
z = 3
What are some of the syntax constructs that exist that perform variable destructuring/unpacking?
Some languages allow you to write something like
x, y, z = (1, 2, 3)
Which is equivalent to
x = 1
y = 2
z = 3
What are some of the syntax constructs that exist that perform variable destructuring/unpacking?
For JavaScript, there are two valid syntaxes, depending on what you are destructuring. In both cases they attempt to mirror the data structure they work on:
Uses [ ]
to enclose the variables that need to be unpacked and does so positionally - the first variable takes on the first value, the second variable takes on the second value, etc.:
const someArray = [1, 2, 3];
const [x, y, z] = someArray;
Positions can be skipped via elision - by not supplying a variable:
const someArray = [1, 2, 3];
const [x, , z] = someArray;
Which is essentially the same as:
const x = someArray[0];
const z = someArray[2];
...
The remainder of the unpacked values can be collected into a single array by using the syntax ...<variable name>
, for example
const someArray = [1, 2, 3];
const [x, ...others] = someArray;
will have others = [2, 3]
The common name is "array destructuring" but it does not only work on arrays but any iterable. For example sets or generators:
const someSet = new Set().add(1).add(2).add(3);
const [x, y, z] = someSet;
const someGenerator = function* () {
yield 1;
yield 2;
yield 3;
}
const [x, y, z] = someGenerator();
In a lot of common cases it is more or less the same whether you use it on arrays or not. However, that is not necessarily correct for all iterables - if consuming an iterable keeps producing values, the array destructuring will keep drawing, rather than only get the same position:
const someArray = [1, 2, 3];
const [x] = someArray;
const [y] = someArray;
const [z] = someArray;
Will produce x = 1
, y = 1
, and z = 1
because each of them is assigned the first position of the array. Destructuring the array produces a new iterator each time that starts from the start.
While
const someGenerator = function* () {
yield 1;
yield 2;
yield 3;
}
const [x] = someGenerator;
const [y] = someGenerator;
const [z] = someGenerator;
Will produce x = 1
, y = 2
, and z = 3
because there is one iterator in total that keeps producing values.
Uses { }
to enclose the variables that need to be unpacked and does so by name:
const someObject = {x: 1, y: 2, z: 3 };
const { x, y, z } = someObject;
Essentially the same as:
const someObject = {x: 1, y: 2, z: 3 };
const x = someObject.x;
const y = someObject.y;
const z = someObject.z;
The longer form also allows for re-mapping the properties to differently named variables:
const someObject = {x: 1, y: 2, z: 3 };
const { a: x, b: y, c: z } = someObject;
Essentially the same as:
const someObject = {x: 1, y: 2, z: 3 };
const a = someObject.x;
const b = someObject.y;
const c = someObject.z;
...
Very similar to how the array destructuring syntax can use ...<variable name>
to collect the rest of the unpacked values, the same can be done with object destructuring to collect all key/values that have not been extracted otherwise:
const someObject = {x: 1, y: 2, z: 3 };
const { x, ...others } = someObject;
will have others = { y: 2, z: 3 }
These can be arbitrarily nested and combined to access any property and value:
const users = [
{
name: "Fred",
userInfo: { likes: "bowling" },
addresses: ["Town of Bedrock", "some previous address"]
}
];
const [
{
name,
userInfo: { likes: hobby },
addresses: [lastAddress]
}
] = user;
Will extract: name = "Fred"
, hobby = "bowling"
, and lastAddress = "Town of Bedrock"
.
({ name }) => name
, or use iterable destructuring with strings.
$\endgroup$
An approach I'd like to see would be a construct that combines new automatic-scope object declarations and initializer-like constructs with a compound statement. Perhaps something like:
union ( int x=foo->bar : 4,8, y = baz->bongo : 3,9;)
{
...
}
The syntax would be identifiable by the opening parenthesis following the union
keyword--something that would otherwise be impossible.
The "initializers" would need to be lvalues, optionally followed by a bit width and offset. Such a syntax would invite a compiler to behave as though the declared identifiers were objects of the indicated type which were loaded any time before their value was used, and written back any time after their value was modified, in any manner consistent with normal "caching" semantics (if a compiler modifies the temporary object, future reads would need to come from that object rather than the original until the change was written back; if the compiler reads and caches the original object, but then modifies the orginal object, it would need to ensure that the cached copy was updated or reloaded before it was used againm etc.).
If two or more initializers are bound to the same side-effect-free lvalue expression, a compiler would have to allow for the possibility that changes to one might affect the other, but such allowance would not be required if lvalue expressions have side effects or don't match each other.
Note that while this construct would be semantically most useful when handlings things like bit fields, it could also allow compilers the freedom to keep an object that would be updated multiple times in a register, and consolidate multiple writes to an object, when doing so would be useful, but allow a compiler to flush such cached objectss in cases where the alternative would be an extra register spill (thus allowing what would be the sequence:
load x into r0
... do some stuff with r0
push r0
... do some stuff that needs to use r0 for some other purpose
... pop r0
store r0 into x
to be improved by storing r0 into x before the stuff that would use r0 for some other purpose, and avoiding the need to pop it and then store it.