Destructuring by &reference

# Andrea Giammarchi (7 months ago)

Another one (cit. DJ Khaled)

has "by reference" ever been considered as "yet another JS syntax" ???

let object = {value: 1}; let {&value} = object; value = 2;

object.value; // 2

allowed everywhere destructuring is possible, throwing for frozen properties ... is this just meh? .. .'cause I think, specially for function signatures declaration, it might make a difference, when mutability matters.

Cheers 👋

P.S. for transpilers this would simply carry, repeatedly, the initial reference.prop around, instead of using just prop.

# Claudia Meadows (7 months ago)

I would like to see this happen, though I'd like to see it integrated with reified refs. (I know there's a proposal laying around somewhere drafted up by a current or former TC39 person, but I can't find it.)


Claudia Meadows contact at isiahmeadows.com

# Augusto Moura (7 months ago)

I don't know, the by reference destructuring seems a bit too much magical, a scoped variable that sets to a property to a object just sounds like the old with statement but reduced and more explicit, given is just a sugar and currently the problem is not a great of a pain point I would be very picky a proposal like that, any prior art in other languages?

Also, from a grammar/vendor implementation standpoint I would imagine the definition of a by reference variable bind would be kinda fishy, I might be wrong though

Em ter., 2 de mar. de 2021 às 21:03, Claudia Meadows < contact at isiahmeadows.com> escreveu:

# Andrea Giammarchi (7 months ago)

The way I see it: it's just a convention/shortcut to have the destructured object around.

function augment({&value}, testCase) {
  if (/thing/.test(testCase))
    value += testCase;
}

let any = {value: ''};
augment(any, 'thing');

The magical behavior goal is to define side effects when overwriting properties ... for instance:

  • a destructured property, as getter, is just a value
  • every destructured property can be overwritten ... as these are just references

Now here some wonder:

let any = {
  get value() { return 'no setter'; },
  method() {
    console.log(this.value);
  }
};

function stuff({&value, &method}, extra) {
  method(); // "no setter"

  // throws an error
  value = extra;
}

Basically, at the engine level, is just a shortcut for having the original destructure object around, so that both &value and &method would behave exactly like object.value = ... or object.method();

I personally don't know how many times I wanted this simplicity, as opposite of going through this pattern:

function stuff(source, extra) {
  const {value, method} = source;

  if (condition)
    method.call(source);

  if (value === extra)
    source.value = 'no more';
}

The constant change between having the value once, then eventually needing to remember the source is missing, so the function needs refactoring, or it needs binding, or it needs ... you name it, I think referencing the source object somehow, would allow a lot of refactoring, smaller code, and easier to reason about, in IDEs (special highlights for referenced variables), in developers intent (no need to grab the source repeatedly when updates are needed), and in clarity (having an explicit & in destructuring describes the intent of wanting to deal with that value as reference).

My only concern here would be: what if we pass that value to another signature? Well, as long as we don't also have a way to pass such value as reference, in example, "restructuring", as in callback({&value, other}) instead of just callback({value, other}), this should be a no-brainer, but maybe the two ideas should be spec'd together, and also ship together, so that you can propagate a single "reaction point" down the lane, without leaking the whole object across the whole lane.

I hope this makes sense, but that's the idea behind. The fact C can point at pointers and force-update this but JS needs whole objects propagations everywhere, in a highly reactive situation, like the one we have these days, bugs me way too much.

👋

# Augusto Moura (7 months ago)
function stuff(source, extra) {
  const {value, method} = source;

  if (condition)
    method.call(source);

  if (value === extra)
    source.value = 'no more';
}

I mean, in this case you can skip destructuring altogether, having a one way and way only of value indirection is a Pretty Good Thing™ (even though we already have proxys, globalThis and other indirection shenanigans), I never felt annoyed of just using source.value or source.method() instead of value and method(), again the proposal is just a debatable syntax sugar for something we already can do. I wonder if we could ever do the reference thingy in user-land with variable level decorators, if it ever gets discussed again in the meetings. Would be still kinda fishy to propose and implement

# Andrea Giammarchi (7 months ago)

the proposal is just a debatable syntax sugar for something we already

can do

that's basically the entirety of the syntax sugar proposals since ES2015, right? also proxy and globalThis are really unrelated to this, imho, while leaking objects all over down the pipe is my major concern, something this proposal avoids, as no code will have a reference to the entirety of the source object, they'll deal with a known property name passed by reference, incapable of changing anything else in the source object ... so it's rather a signal, than a convention.

this means composability, different classes/prototypes passed as arguments, and the certainty nothing can inspect, change, or deal, with the source, which means increased security, as opposite to pass instances and whole objects references around.

You have a reference to a property, you don't see its source.

# Andrea Giammarchi (7 months ago)

to expand further: you could access such reference via arguments[0], but that won't be possible with arrow functions, as example, so that ({&reactive}, maybe) => { if (maybe) reactive = true; } is another

pattern that doesn't need the whole object around, or to be propagated, when you can pass it via {&reactive} to anything else, covering then the arguments point too.

# Augusto Moura (7 months ago)

that's basically the entirety of the syntax sugar proposals since ES2015,

right?

Definitely no, but talking about the syntax additions since ES2015, they are in one or more of the categories below:

  • avoid known footguns in the language (arrow functions and lexical this, classes and prototype, let/const and block scoping, nullish coalescing operator, etc.)
  • syntax sugars with strong community feedback AND battle proven prior art (classes, destructuring, string templates, rest, spread and default values, async/await, etc.)
  • introducing or specifying new mechanisms that didn't exist before in ecma (modules, classes, varargs, etc.)

also proxy and globalThis are really unrelated to this

Proxy and globalThis (and the with statement for that matter), are mechanisms of value indirection aside from the "classic" instance properties

while leaking objects all over down the pipe is my major concern,

something this proposal avoids, as no code will have a reference to the entirety of the source object, they'll deal with a known property name passed by reference, incapable of changing anything else in the source object ... so it's rather a signal, than a convention.

How will you prevent the passing of the object down the pipe? You mean the reference variable being passed to another function and setting the prop into the source object?

function foo(source) {
  let { &value } = source;
  value = 'foo';
}

function second(source) {
  // You still need to pass the object forward right?
  foo(source)

  // Or the proposal is something like this
  let { &value } = source;
  foo(value);
  // and then if foo sets the value argument it should reflect in source
}

Also the usual way of preventing the "passing the full object down" problem is restricting the contract with other functions using a wrapper/proxy, a well defined more specific interface or in the readonly case just omitting the other properties

// Wrapper way
class Nameable {
  constructor(instance) { this.#obj = instance }
  get name() { return this.#obj.name }
  set name(newName) { this.#obj.name = newName }
}

function printName(nameable) {
  console.log(nameable.name)
  nameable.name += ' [printed]'
}
function foo(source) {
  printName(new Nameable(source))
}
foo({ name: 'foo', type: 'pojo' })

// Well defined contract way (using Typescript, but you could rely on duck
typing if you trust the good manners of the developers)
interface Nameable {
  name: string;
}
interface Pojo extends Nameable {
  type: string;
}

function printName(nameable: Nameable) {
  console.log(nameable.name)
  nameable.name += ' [printed]'
  // the function still can access the type field by ignoring the typing,
but at this point this is the least scary thing a developer in a app
}
function foo(source: Pojo) {
  printName(source)
}

// Omit and readonly way
function printName(nameable) { /* ... */ }
function foo(source) {
  printName(pick(source, ['name']))
}
# #!/JoePea (7 months ago)

I think it is a good idea, but maybe we need to think about what the final syntax would be. About segregation, if the library changes the signature, then they'll have the whole source object, right? Maybe for the mentioned security the user would need to opt in:

theFunction(&obj.foo, &obj.bar);

would allow the function to destructure, and only destructure, by reference of those given properties, for example. Then the contract can not silently change from secure to insecure.

#!/JoePea

# #!/JoePea (7 months ago)

Or maybe it would be

theFunction(obj.&foo, obj.&bar);

to explicitly pass the particular properties, and would account for nested properties too:

theFunction(obj.foo.&bar, obj.lorem.&ipsum);

#!/JoePea

# Andrea Giammarchi (7 months ago)

How will you prevent the passing of the object down the pipe?

const downThePipe = ({&source}) => {
  // you can read source
  source;
  // you can set source
  source = 'blah';
  // you can't know where source comes from
  // but you could propagate that reference further
  evenFurtherDown({&source, any: 'value'}, Math.random());
};

downThePipe({
  secret: 'nothing out there can reach me',
  get source() { 'this object'; },
  set source(value) {
    console.log('hello', value, this.secret);
  }
});

You can pass objects already in JS so this changes nothing in terms of logic, except the callback has a way to signal reactive properties or retrieved methods.

Any boilerplate with Proxies would be slower and more convoluted, so this syntax simplieis all the code you wrote via an explicit intent: the callback would like to invoke, or update an accessor, of the given object, without holding, or having, the whole object in its scope.

I hope this explains a bit better why I think this feature would be extremely cool. Polyfills won't need to do much, code remains short and clean, accessors/reactive properties becomes instantly clear (they say accessors are a footgun, here we're flagging these for better awareness) and methods can be invoked with the right context too, without needing whole objects references around.