[Proposal] New syntax for lazy getters
How would this work when the getter lives on a prototype? (like most
builtin getters) Would it install the memoized value on this
, or on the
object that contains the getter? Would it use [[Set]] or [[Define]]?
My 2 cents, I use lazy getters since about ever and I'd love to have such syntax in place but I think there is room for some improvement / simplification in terms of syntax.
## Keep it getish
From parsing perspective, introducing
lazy tail()
seems way simpler than
introducing lazy tail:
for the simple reason that everything that can
parse get tail()
and set tail()
is in place already in every engine. I
don't write them but I'm sure having an extra keyboard to catch shouldn't
be crazy complicated.
## class compatible
because you used delete this.tail
and mentioned functional programming,
I'd like to underline ES doesn't force anyone to one programming style or
another. That means new syntax should play nicely with classes too, and in
this case the proposal doesn't seem to address that because of the direct
value mutation, as generic property, and the removal of that property from
the object, something not needed if inherited.
My variant would do the same, except it would keep the value an accessor:
const take = (n, xs) => n === 0 ? null : xs && {
head: xs.head,
lazy tail() {
return Object.defineProperty(this, 'tail', {
configurable: false,
get: (value =>
// still a getter
() => value
)(
// executed once
take(n - 1, xs.tail)
)
}).tail;
}
};
This would keep initial accessor configuration, in terms of enumerability, but it will freeze its value forever and, on top of that, this will play already well with current valid ES2015 classes syntax.
I also believe myself proposed something similar a while ago (or somebody else and I agreed with that proposal) but for some reason it never landed.
Hopefully this time the outcome would be different.
Best
- having an extra keyword ...
in case the counter proposal is not clear enough (apologies for that), this should read simpler than that
const take = (n, xs) => n === 0 ? null : xs && {
head: xs.head,
lazy tail() {
const value = take(n - 1, xs.tail);
Object.defineProperty(this, 'tail', {
configurable: false,
get: () => value
});
return value;
}
};
Hope it's clear what I'd change.
Best
When the getter lives on a prototype it would install the memoized value on this using [[Define]]. This is the same way it would work in regular lazy getters: const defclass = prototype => { const constructor = prototype.constructor; constructor.prototype = prototype; return constructor; };
const Person = defclass({ constructor: function Person(firstname, lastname) { this.firstname = firstname; this.lastname = lastname; }, lazy fullname: this.firstname + " " + this.lastname });
const john = new Person("John", "Doe"); const jane = new Person("Jane", "Doe");
console.log(john.fullname); // John Doe console.log(jane.fullname); // Jane Doe
Note that the this within the lazy property value expression (i.e. within the expression this.firstname + " " + this.lastname) refers to the instance (i.e. either john or jane). This can be confusing because without the lazy keyword, this would refer to the lexical context. Although I'm comfortable with it yet I'm open to other suggestions.
On Tue, Jun 12, 2018, at 3:31 AM, Jordan Harband wrote:
How would this work when the getter lives on a prototype? (like most builtin getters) Would it install the memoized value on
this
, or on the object that contains the getter? Would it use [[Set]] or [[Define]]?> On Tue, Jun 12, 2018 at 12:13 AM, Aadit M Shah <aaditmshah at fastmail.fm> wrote:>> __Hello TC39,
I recently opened an issue[1] in the tc39/ecma262[2] repository, proposing a new syntax for lazy getters, and I was directed to the CONTRIBUTING[3] page which stated that I should start a conversation on this mailing list.>> So, my feature proposal is to have syntactic sugar for creating lazy getters[4]. To summarize my original proposal (which you can read by following the very first link above), I find that creating lazy getters is very verbose. For example, consider:>> const take = (n, xs) => n === ? null : xs && { head: xs.head, get tail() { delete this.tail; return this.tail = take(n - 1, xs.tail); } };
My proposed solution is to add a new keyword lazy to the language. This keyword can only be used as a prefix to longhand property names in object initializers, and it defers the execution of the value expression until the property is accessed. In short, it's just syntactic sugar for lazy getters:>> const take = (n, xs) => n === ? null : xs && { head: xs.head, lazy tail: take(n - 1, xs.tail) };
This is purely syntactic sugar. The semantics of this new syntax would remain the same as that of the desugared syntax. In particular, calling Object.getOwnPropertyDescriptor(list, "tail") would return an accessor descriptor before accessing list.tail and a data descriptor afterwards.>> Furthermore, there are other advantages of having this syntactic sugar. For example, creating cyclic data structures becomes much easier. Examples are provided in my original proposal which is linked above. Hope to hear your thoughts on this.>> , Aadit M Shah
es-discuss mailing list es-discuss at mozilla.org, mail.mozilla.org/listinfo/es-discuss
Links:
That is why I'm proposing to use a getter, you don't have to reason about
the context in lazy fullname() { return this.firstName + ' ' + this.lastName; }
and it still works with de-contextualized invokes such
lazy random: () => Math.random()
as long as you transpile that as getter
calling the right context (when/if needed).
Both cases can de-sugar to same redefinition of a getter.
Actually, from a parsing perspective I believe it shouldn't be too
difficult to implement the lazy name: expression
syntax. In
addition, I'm not too keen on your lazy name() { return expression; }
syntax because:
- It's more verbose.
- It seems to me that it's no different than creating a regular getter:
const take = (n, xs) => n === ? null : xs && { head: xs.head, get
tail() { const value = take(n - 1, xs.tail); Object.defineProperty(this, "tail", { configurable: false, get: () => value }); return value; } };
Regarding the second bullet point, I've probably misunderstood what you were trying to convey. Perhaps you could elucidate. Anyway, making the property non-configurable after accessing it seems like a reasonable thing to do.
On Tue, Jun 12, 2018, at 3:44 AM, Andrea Giammarchi wrote:
My 2 cents, I use lazy getters since about ever and I'd love to have such syntax in place but I think there is room for some improvement / simplification in terms of syntax.> ## Keep it getish**
From parsing perspective, introducing
lazy tail()
seems way simpler than introducinglazy tail:
for the simple reason that everything that can parseget tail()
andset tail()
is in place already in every engine. I don't write them but I'm sure having an extra keyboard to catch shouldn't be crazy complicated.> ## class compatiblebecause you used
delete this.tail
and mentioned functional programming, I'd like to underline ES doesn't force anyone to one programming style or another. That means new syntax should play nicely with classes too, and in this case the proposal doesn't seem to address that because of the direct value mutation, as generic property, and the removal of that property from the object, something not needed if inherited.> My variant would do the same, except it would keep the value an accessor:>
const take = (n, xs) => n === 0 ? null : xs && { head: xs.head, lazy tail() { return Object.defineProperty(this, 'tail', { configurable: false, get: (value => // still a getter () => value )( // executed once take(n - 1, xs.tail) ) }).tail; } };
This would keep initial accessor configuration, in terms of enumerability, but it will freeze its value forever and, on top of that, this will play already well with current valid ES2015 classes syntax.> I also believe myself proposed something similar a while ago (or somebody else and I agreed with that proposal) but for some reason it never landed.> Hopefully this time the outcome would be different.
Best
On Tue, Jun 12, 2018 at 9:13 AM, Aadit M Shah <aaditmshah at fastmail.fm> wrote:>> __
Hello TC39,
I recently opened an issue[1] in the tc39/ecma262[2] repository, proposing a new syntax for lazy getters, and I was directed to the CONTRIBUTING[3] page which stated that I should start a conversation on this mailing list.>> So, my feature proposal is to have syntactic sugar for creating lazy getters[4]. To summarize my original proposal (which you can read by following the very first link above), I find that creating lazy getters is very verbose. For example, consider:>> const take = (n, xs) => n === ? null : xs && { head: xs.head, get tail() { delete this.tail; return this.tail = take(n - 1, xs.tail); } };
My proposed solution is to add a new keyword lazy to the language. This keyword can only be used as a prefix to longhand property names in object initializers, and it defers the execution of the value expression until the property is accessed. In short, it's just syntactic sugar for lazy getters:>> const take = (n, xs) => n === ? null : xs && { head: xs.head, lazy tail: take(n - 1, xs.tail) };
This is purely syntactic sugar. The semantics of this new syntax would remain the same as that of the desugared syntax. In particular, calling Object.getOwnPropertyDescriptor(list, "tail") would return an accessor descriptor before accessing list.tail and a data descriptor afterwards.>> Furthermore, there are other advantages of having this syntactic sugar. For example, creating cyclic data structures becomes much easier. Examples are provided in my original proposal which is linked above. Hope to hear your thoughts on this.>> , Aadit M Shah
es-discuss mailing list es-discuss at mozilla.org, mail.mozilla.org/listinfo/es-discuss
Links:
it's not about being difficult, it's about compatibility with classes, where getters are OK since ever. However, since classes fields are in Stage 3 [1] maybe it's OK to have:
class Test {
x = 123;
lazy random = () => this.x + Math.random();
}
However, like you've noticed, while it's easy to reason about the context within a class declaration, it's easy to create a footgun outside that.
const test = {
x: 123,
lazy random: () => this.x + Math.random()
};
That is a whole new meaning of arrow function I'd rather like not to ever encounter.
So what about extra new syntax?
class Test {
x = 123;
lazy random() => this.x + Math.random();
}
const test = {
x: 123,
lazy random() => this.x + Math.random()
};
But I'm pretty sure at methods shorthand discussions that came up and for some reason didn't work.
Actually, never mind. I've just realized this would work as well, and it's clean enough.
class Test {
x = 123;
lazy random = this.x + Math.random();
}
const test = {
x: 123,
lazy random: this.x + Math.random()
};
My only concern is if that class method is created N times per each instance (one of the reasons I use lazy getters in classes prototypes instead of per object) as it is AFAIK for current class fields if you attach an arrow function, instead of once per class, which seems to be preferable in a class context.
Otherwise, I'd be happy to see that happen.
Best
On June 12, 2018 9:44:31 AM GMT+02:00, Andrea Giammarchi <andrea.giammarchi at gmail.com> wrote:
My 2 cents, I use lazy getters since about ever and I'd love to have such syntax in place but I think there is room for some improvement / simplification in terms of syntax.
Yeah I find this better.
Also fixes the this questions by unambiguously setting it to the instance.
## Keep it getish
From parsing perspective, introducing
lazy tail()
seems way simpler than introducinglazy tail:
for the simple reason that everything that can parseget tail()
andset tail()
is in place already in every engine. I don't write them but I'm sure having an extra keyboard to catch shouldn't be crazy complicated.## class compatible
because you used
delete this.tail
and mentioned functional programming, I'd like to underline ES doesn't force anyone to one programming style or another. That means new syntax should play nicely with classes too, and in this case the proposal doesn't seem to address that because of the direct value mutation, as generic property, and the removal of that property from the object, something not needed if inherited.My variant would do the same, except it would keep the value an accessor:
const take = (n, xs) => n === 0 ? null : xs && { head: xs.head, lazy tail() { return Object.defineProperty(this, 'tail', { configurable: false,
Why enforcing configurable false? When you use get x() / set x() syntax, it leaves the thing configurable true. I feel it is more consistent to have the replaced getter copy configurable from existing status quo, that is, what is actual configurable of this.tail.
And of course, Object.defineProperty family needs a way to create this, I think {get: getterFn, lazy: true, …} could work.
Classes in JavaScript don't allow assignments within the class body. Hence, the following code is invalid: class Test { x = 123; // This throws an error. lazy random = this.x + Math.random(); // Similarly, this should be invalid.}
Hence, the more I think about it the more it makes sense to use the following syntax instead: const zeros = { head: , lazy tail() { return this; } };
class Random { lazy random() { return Math.random(); } }
const x = new Random; const y = new Random;
console.log(x.value === x.value); // true console.log(x.value === y.value); // false (in all probability)
It's more verbose than my initial proposal. However, as Andrea mentioned
it's consistent with the class method syntax and the this keyword can be
used within the lazy getter without causing confusion.
Finally, I think that lazy <name>() { <body> }
should be syntactic
sugar for: get name() { return Object.defineProperty(this, "<name>", { value: (function () { <body> }()), configurable: false // Making it non-configurable is debatable. }).<name>; }
Using Object.defineProperty instead of simple assignment will ensure that it works in strict mode. In addition, it'll make the property non- writable which is the behavior we want because it's non-writable before memoization too, since it doesn't have a setter.
On Tue, Jun 12, 2018, at 5:52 AM, Andrea Giammarchi wrote:
Actually, never mind. I've just realized this would work as well, and it's clean enough.>
class Test { x = 123; lazy random = this.x + Math.random(); } const test = { x: 123, lazy random: this.x + Math.random() };
My only concern is if that class method is created N times per each instance (one of the reasons I use lazy getters in classes prototypes instead of per object) as it is AFAIK for current class fields if you attach an arrow function, instead of once per class, which seems to be preferable in a class context.> Otherwise, I'd be happy to see that happen.
Best
On Tue, Jun 12, 2018 at 11:44 AM, Andrea Giammarchi <andrea.giammarchi at gmail.com> wrote:>> it's not about being difficult, it's about compatibility with
classes, where getters are OK since ever. However, since classes fields are in Stage 3 [1] maybe it's OK to have:>>
class Test { x = 123; lazy random = () => this.x + Math.random(); }
However, like you've noticed, while it's easy to reason about the context within a class declaration, it's easy to create a footgun outside that.>>
const test = { x: 123, lazy random: () => this.x + Math.random() };
That is a whole new meaning of arrow function I'd rather like not to ever encounter.>> So what about extra new syntax?
class Test { x = 123; lazy random() => this.x + Math.random(); } const test = { x: 123, lazy random() => this.x + Math.random() };
But I'm pretty sure at methods shorthand discussions that came up and for some reason didn't work.>>
[1] tc39/proposal-class-fields#field-declarations>> On Tue, Jun 12, 2018 at 11:32 AM, Aadit M Shah <aaditmshah at fastmail.fm> wrote:>>> __
Actually, from a parsing perspective I believe it shouldn't be too difficult to implement the
lazy name: expression
syntax. In addition, I'm not too keen on yourlazy name() { return expression; }
syntax because:>>>
- It's more verbose.
- It seems to me that it's no different than creating a regular getter: const take = (n, xs) => n === ? null : xs && { head: xs.head, get tail() { const value = take(n - 1, xs.tail); Object.defineProperty(this, "tail", { configurable: false, get: () => value }); return value; } };>>> Regarding the second bullet point, I've probably misunderstood what you were trying to convey. Perhaps you could elucidate.>>> Anyway, making the property non-configurable after accessing it seems like a reasonable thing to do.>>>
On Tue, Jun 12, 2018, at 3:44 AM, Andrea Giammarchi wrote:
My 2 cents, I use lazy getters since about ever and I'd love to have such syntax in place but I think there is room for some improvement / simplification in terms of syntax.>>>> ## Keep it getish**
From parsing perspective, introducing
lazy tail()
seems way simpler than introducinglazy tail:
for the simple reason that everything that can parseget tail()
andset tail()
is in place already in every engine. I don't write them but I'm sure having an extra keyboard to catch shouldn't be crazy complicated.>>>> ## class compatiblebecause you used
delete this.tail
and mentioned functional programming, I'd like to underline ES doesn't force anyone to one programming style or another. That means new syntax should play nicely with classes too, and in this case the proposal doesn't seem to address that because of the direct value mutation, as generic property, and the removal of that property from the object, something not needed if inherited.>>>> My variant would do the same, except it would keep the value an accessor:>>>>
const take = (n, xs) => n === 0 ? null : xs && { head: xs.head, lazy tail() { return Object.defineProperty(this, 'tail', { configurable: false, get: (value => // still a getter () => value )( // executed once take(n - 1, xs.tail) ) }).tail; } };
This would keep initial accessor configuration, in terms of enumerability, but it will freeze its value forever and, on top of that, this will play already well with current valid ES2015 classes syntax.>>>> I also believe myself proposed something similar a while ago (or somebody else and I agreed with that proposal) but for some reason it never landed.>>>> Hopefully this time the outcome would be different.
Best
On Tue, Jun 12, 2018 at 9:13 AM, Aadit M Shah <aaditmshah at fastmail.fm> wrote:>>>>> __
Hello TC39,
I recently opened an issue[1] in the tc39/ecma262[2] repository, proposing a new syntax for lazy getters, and I was directed to the CONTRIBUTING[3] page which stated that I should start a conversation on this mailing list.>>>>> So, my feature proposal is to have syntactic sugar for creating lazy getters[4]. To summarize my original proposal (which you can read by following the very first link above), I find that creating lazy getters is very verbose. For example, consider:>>>>> const take = (n, xs) => n === ? null : xs && { head: xs.head, get tail() { delete this.tail; return this.tail = take(n - 1, xs.tail); } };
My proposed solution is to add a new keyword lazy to the language. This keyword can only be used as a prefix to longhand property names in object initializers, and it defers the execution of the value expression until the property is accessed. In short, it's just syntactic sugar for lazy getters:>>>>> const take = (n, xs) => n === ? null : xs && { head: xs.head, lazy tail: take(n - 1, xs.tail) };
This is purely syntactic sugar. The semantics of this new syntax would remain the same as that of the desugared syntax. In particular, calling Object.getOwnPropertyDescriptor(list, "tail") would return an accessor descriptor before accessing list.tail and a data descriptor afterwards.>>>>> Furthermore, there are other advantages of having this syntactic sugar. For example, creating cyclic data structures becomes much easier. Examples are provided in my original proposal which is linked above. Hope to hear your thoughts on this.>>>>> , Aadit M Shah
es-discuss mailing list es-discuss at mozilla.org, mail.mozilla.org/listinfo/es-discuss
Links:
On June 12, 2018 11:32:22 AM GMT+02:00, Aadit M Shah <aaditmshah at fastmail.fm> wrote:
Actually, from a parsing perspective I believe it shouldn't be too difficult to implement the
lazy name: expression
syntax. In addition, I'm not too keen on yourlazy name() { return expression; }
syntax because:
- It's more verbose.
- It seems to me that it's no different than creating a regular getter:
const take = (n, xs) => n === ? null : xs && { head: xs.head,
get tail() { const value = take(n - 1, xs.tail); Object.defineProperty(this, "tail", { configurable: false, get: () => value }); return value; } };
I am pretty sure Andrea mixed syntax of lazy getter with its implementation for brevity, and the actual lazy getter would look like:
lazy tail() { return take(n - 1, xs.tail); }
Regarding the second bullet point, I've probably misunderstood what you were trying to convey. Perhaps you could elucidate. Anyway, making the property non-configurable after accessing it seems like a reasonable thing to do.
Here I disagree. No syntax construct so far forces immutability. The get x() / set x() ones are configurable. If you defined lazy getter so far by get(), you could have changed it using Object.defineProperties if there was some strange need for it. You had to explicitly freeze etc. or defineProperty with configurable false if you wanted to make it so.
This autofreezing if the value sticks out out this philosophy of " malleable unless specified otherwise".
Why enforcing configurable false?
The fact the lazy getter is called getter, and the fact that a getter cannot be assigned or the engine throws errors, as opposite of a non configurable property that will silently ignore the assignment, makes me think substituting a lazy getter with a fixed getter is the way to.
But I also think being lazy, usually meaning with expensive computations needed once only, it's a nice to have, and clean guard, to fix that getter forever.
However, that creates observability of the getter, through its own property descriptor, but also, as a getter, it might signal side effects, while a fixed property on the object set as value would indicate there are, probably, no side effects in accessing it.
TL;DR I'm not fully sure about the getter should be defined, if through its inherited definition but with a different get callback that returns the computed value, and only when it comes to classes, or through its own property accessor, when it comes to objects.
Maybe this is the best way to go though, so that the following code:
const test = {
x: 123,
lazy random() {
return this.x + Math.random();
}
};
class Test {
x = 123;
lazy random() {
return this.x + Math.random();
}
}
would de-sugar in something like this:
const getDescriptor = (o, k) => (
o ? (
Object.getOwnPropertyDescriptor(o, k) ||
getDescriptor(Object.getPrototypeOf(o), k)
) : o
);
const test = {
x: 123,
get random() {
const desc = getDescriptor(this, 'random');
const value = (function () {
return this.x + Math.random();
}).call(this);
desc.get = () => value;
Object.defineProperty(this, 'random', desc);
return value;
}
};
class Test {
x = 123;
get random() {
const desc = getDescriptor(this, 'random');
const value = (function () {
return this.x + Math.random();
}).call(this);
desc.get = () => value;
Object.defineProperty(this, 'random', desc);
return value;
}
}
preserving whatever behavior previously defined.
On June 12, 2018 1:32:04 PM GMT+02:00, herby at mailbox.sk wrote:
This autofreezing if the value sticks out out this philosophy of "
of the value
Okay, so my previous statement about field declarations in classes being invalid was incorrect. I didn't see Andrea's link to the class field declarations proposal[1]. Hence, from what I understand we're considering the following syntax: const zeros = { head: , lazy tail: this };
class Random { lazy value = Math.random(); }
As for semantics, Herby's philosophy of "malleable unless specified otherwise" makes sense. Hence, the above code would be transpiled to: const zeros = { head: , get tail() { return Object.defineProperty(this, "tail", { value: this }).tail; } };
class Random { get value() { return Object.defineProperty(this, "value", { value: Math.random() }).value; } }
I guess we'd also be adopting the syntax for private fields and static fields? For example, lazy #value and lazy static #value?
On Tue, Jun 12, 2018, at 7:32 AM, herby at mailbox.sk wrote:
On June 12, 2018 11:32:22 AM GMT+02:00, Aadit M Shah <aaditmshah at fastmail.fm> wrote:>> Actually, from a parsing perspective I believe it shouldn't be too
difficult to implement the
lazy name: expression
syntax. In addition, I'm not too keen on yourlazy name() { return expression;>> }
syntax because:
- It's more verbose.
- It seems to me that it's no different than creating a regular getter:
const take = (n, xs) => n === ? null : xs && { head: xs.head,
get tail() { const value = take(n - 1, xs.tail); Object.defineProperty(this, "tail", { configurable: false,>> get: () => value }); return value; } };I am pretty sure Andrea mixed syntax of lazy getter with its implementation for brevity, and the actual lazy getter would look like:> lazy tail() { return take(n - 1, xs.tail); }
Regarding the second bullet point, I've probably misunderstood what you>> were trying to convey. Perhaps you could elucidate. Anyway, making the property non-configurable after accessing it seems>> like a reasonable thing to do.
Here I disagree. No syntax construct so far forces immutability. The get x() / set x() ones are configurable. If you defined lazy getter so far by get(), you could have changed it using Object.defineProperties if there was some strange need for it. You had to explicitly freeze etc. or defineProperty with configurable false if you wanted to make it so.> This autofreezing if the value sticks out out this philosophy of " malleable unless specified otherwise".>
On Tue, Jun 12, 2018, at 3:44 AM, Andrea Giammarchi wrote:
My 2 cents, I use lazy getters since about ever and I'd love to have such syntax in place but I think there is room for some improvement / simplification in terms of syntax.> ## Keep it getish**
From parsing perspective, introducing
lazy tail()
seems way simpler>>> than introducinglazy tail:
for the simple reason that everything>>> that can parseget tail()
andset tail()
is in place already in>>> every engine. I don't write them but I'm sure having an extra keyboard to catch shouldn't be crazy complicated.> ## class compatiblebecause you used
delete this.tail
and mentioned functional programming, I'd like to underline ES doesn't force anyone to one programming style or another. That means new syntax should play nicely with classes too, and in this case the proposal doesn't seem to address that because of the direct value mutation, as generic property, and the removal of that property from the object, something>>> not needed if inherited.> My variant would do the same, except it would keep the value an accessor:>
const take = (n, xs) => n === 0 ? null : xs && { head: xs.head, lazy tail() { return Object.defineProperty(this, 'tail', { configurable: false, get: (value => // still a getter () => value )( // executed once take(n - 1, xs.tail) ) }).tail; } };
This would keep initial accessor configuration, in terms of enumerability, but it will freeze its value forever and, on top of that, this will play already well with current valid ES2015 classes syntax.> I also believe myself proposed something similar a while ago (or somebody else and I agreed with that proposal) but for some reason it>>> never landed.> Hopefully this time the outcome would be different.
Best
On Tue, Jun 12, 2018 at 9:13 AM, Aadit M Shah <aaditmshah at fastmail.fm> wrote:>> __
Hello TC39,
I recently opened an issue[1] in the tc39/ecma262[2] repository, proposing a new syntax for lazy getters, and I was directed to the>>>> CONTRIBUTING[3] page which stated that I should start a conversation>>>> on this mailing list.>> So, my feature proposal is to have syntactic sugar for creating lazy>>>> getters[4]. To summarize my original proposal (which you can read by>>>> following the very first link above), I find that creating lazy getters is very verbose. For example, consider:>> const take = (n, xs) => n === ? null : xs && { head: xs.head, get tail() { delete this.tail; return this.tail = take(n - 1, xs.tail); } };
My proposed solution is to add a new keyword lazy to the language.>>>> This keyword can only be used as a prefix to longhand property names>>>> in object initializers, and it defers the execution of the value expression until the property is accessed. In short, it's just syntactic sugar for lazy getters:>> const take = (n, xs) => n === ? null : xs && { head: xs.head, lazy tail: take(n - 1, xs.tail) };
This is purely syntactic sugar. The semantics of this new syntax would remain the same as that of the desugared syntax. In particular,
calling Object.getOwnPropertyDescriptor(list, "tail") would return>> an accessor descriptor before accessing list.tail and a data descriptor>>>> afterwards.>> Furthermore, there are other advantages of having this syntactic sugar. For example, creating cyclic data structures becomes much easier. Examples are provided in my original proposal which is linked
above. Hope to hear your thoughts on this.>> , Aadit M Shah
es-discuss mailing list es-discuss at mozilla.org, mail.mozilla.org/listinfo/es-discuss
Links:
Links:
On Tue, Jun 12, 2018 at 12:31 PM, Aadit M Shah <aaditmshah at fastmail.fm>
wrote:
Classes in JavaScript don't allow assignments within the class body.
Hence,
the following code is invalid:
class Test { x = 123; // This throws an error. lazy random = this.x + Math.random(); // Similarly, this should be invalid. }
Andrea was using syntax from the class fields proposal, currently at Stage 3 and fairly commonly used via transpilation (he said "class fields" later in the post, but didn't emphasize it).
-- T.J. Crowder
forgot about this:
I think {get: getterFn, lazy: true, …} could work.
sort of, because descriptors can be only for properties or accessors, but lazy + set, as accessor might look weird.
However, if I can dare bringing in another use case I have daily in my code, I have a case for having a setter that defines the getter if explicit, so that by default I have a lazy getter, but if before it gets used somebody sets the property, it overrides the lazy getter.
This might happen, as example, during constructor time, as opposite of after initialization.
Example:
class Weirdo {
constructor(theThing = null) {
if (theThing != null) {
this.theThing = theThing;
}
}
get theThing() {
const value = doVeryExpensiveThing();
Object.defineProperty(this, 'theThing', {
get: () => value
});
return value;
}
set theThing(value) {
Object.defineProperty(this, 'theThing', {
get: () => value
});
}
}
Honestly, I don't see why this would create any issue with the accessor
descriptor, so that I can lazy theThing() { return doVeryExpensiveThing(); }
and eventually provide a set theThing() {}
to
do whatever I want to do when, and if, needed.
So yes, if lazy: true
plays well with accessors, and it's to define the
special get
behavior only, then it's a valid welcome, and nice to have
addition.
Object.defineProperty(
test,
'random',
{
configurable: false, // by default
enumerable: false, // by default
lazy: false, // by default
get() { return this.x + Math.random(); },
set() { /* optional, do whatever */ }
}
);
My last question / concern at this point, in case descriptors should be part of this proposal, is how to retrieve the initial lazy descriptor from an object which lazy getter overwrote such value, but I also think this should not be a real-world concern or, eventually, a must know caveat for functional programmers that play procedurally with accessors.
I think I've exposed all my thoughts on this issue, I'll hope for some extra outcome.
Best
Actually, by malleable I meant only configurable:true, so one can change it via Object.defineProp… api, I did not mean necessarily to define it as value.
I have no strong opinion on what should be there after the first access, but it boils down on how will it be exposed via Object.defineProperty, really, because as little as possible should be changed, IOW as much as possible retained.
So on case things are defined as (only pondering the property descriptor here, the call is obvious):
{ lazy: true, get: () => Math.random() } … [1]
or, bigger example:
{ lazy: { configurable: false }, enumerable: false, get: () => foos.length, set: x => console.log(set ${x}
) } … [2]
Then what should be generated is indeed a getter so that setter may be retained as well in [2].
If the definition is:
{ lazy: { configurable: false, writable: false, enumerable: true, compute: () => Math.random() }, enumerable: false } … [3]
then it defines a value (which is not enumerable until first accessed thus created; contrived example, I know).
This post also shows a proposal how to, in future proof way, define what attributes will the replaced getter/value have: put it In lazy field of prop descriptor, lazy: true means shortcut for “the default way / Inherit from what is there now”.
On June 12, 2018 2:37:05 PM GMT+02:00, herby at mailbox.sk wrote:
Actually, by malleable I meant only configurable:true, so one can change it via Object.defineProp… api, I did not mean necessarily to define it as value.
I have no strong opinion on what should be there after the first access, but it boils down on how will it be exposed via Object.defineProperty, really, because as little as possible should be changed, IOW as much as possible retained.
Not to create a confusion, I do_not propose that both ways be working, what I wanted to say is that only o e should be chosen, and that will select what will be there as a replacement.
Though, now that I think about it,
lazy field = expr;
and
lazy getter() { return expr; }
are different beasts.
Anyway, as Andrea said, enough said for the moment.
FWIW, I think to keep it simple lazy: true
would be enough for the time
being.
Having the new descriptor inside the descriptor seems a bit over
engineering at this point, imo, but having a field already won't exclude,
in the feature, the ability to improve that field (similar way
addEventListener on DOM got promoted from (type, handler, boolean)
signature to (type, handler, boolean || options)
)
I also agree that lazy field = expr
is a different beast, and it won't
play well with descriptors as we know, but it might allow overwrites if
accessed directly.
I wouldn't mind that as long as it plays well with objects and classes, and as long as there is an official way to lazy define properties, and if it could be so lazy that if redefined disappears, in that direct assignment form, it would be a solution to all my cases.
The problem here is that if lazy field and lazy getter are indeed different beast, one should be able to discriminate the two inside the property descriptor.
So maybe
// replaces with getter on first access {lazy: producerFn, get: null, ...}
// replaces with value on first access {lazy: producerFn, value: null, ...}
and in case both get and value are present, just be consistent with what is the behaviour now (early error or preferences of one over the other).
I don't think a new keyword is the right path here. For classes lazy fields
can easily be implemented via decorators in the last proposal1 (also,
Groovy has previous work implementing a @Lazy
annotation2 with AST
transformations for the same use case). In code something like:
const lazy = (fieldMetadata) => {
// change the field value in descriptor to a lazy factory
};
class Foo {
@lazy bar = slowCalculation();
@lazy statements = (() => {
if (this.bar > 0) {
return fooify();
} else {
return barify();
}
}());
}
What it's doesn't cover (and in my opinion should be the focus of a new proposal) is Decorators for literal objects. Something like the code below is yet not proposed:
const foo = {
@lazy bar: 3,
};
I didn't made a deep search in ESDiscuss history but I remember this feature being mentioned sometime ago.
To me decorators are the answer to pretty much everything but for the last year I've seen zero progress on what seems to be a forever Stage 2 proposal: tc39/proposals#stage-2
Is there anything anyone can do to speed up adoption/implementation of that proposal?
Yes, something is getting done about decorators. tc39/agendas#398
BTW, I proposed similar (lazy values) 9 months ago 1, and it's been on the list plenty of times 2. I'd personally love to see it happen, but I find it not very likely it'd make it, especially as a property (since decorators can rectify that).
Isiah Meadows me at isiahmeadows.com, www.isiahmeadows.com
What it's doesn't cover (and in my opinion should be the focus of a new proposal) is Decorators for literal objects. Something like the code below is yet not proposed:
const foo = { @lazy bar: 3, };
what would happen if you tried to JSON.stringify foo? a core-value of javascript to industry is as an idiot-proof/least-surprise language for serializing json-data across browser <-> server. junior-programmers who naively employ hard-to-serialize things like custom-getters in their low-level code, mostly end up annoying senior-programmers when they have to debug high-level integration-code that have problems baton-passing those states around.
kai zhu kaizhu256 at gmail.com
If you're wanting to propose new places for decorators, you'd have much better luck here: tc39/proposal-decorators
I do agree this is an obvious place for a decorator, and it is probably the ideal way of specifying it for object properties, but your larger proposal of adding decorators for objects would have better luck there.
Isiah Meadows me at isiahmeadows.com, www.isiahmeadows.com
On June 27, 2018 09:23, kai zhu <kaizhu256 at gmail.com> wrote:
what would happen if you tried to JSON.stringify foo? a core-value of javascript to industry is as an idiot-proof/least-surprise language for serializing json-data across browser <-> server. junior-programmers who naively employ hard-to-serialize things like custom-getters in their low-level code, mostly end up annoying senior-programmers when they have to debug high-level integration-code that have problems baton-passing those states around.
Depends on the chosen implementation (which will be open for discussion for
sure). The Groovy AST transformation annotation @Lazy
for example
translates the field into a getter (without a setter, making it virtually a
readonly field) which initializes the field once at the first invocation
(there is a example of the generated code at Groovy docs[1]). If we follow
this path we can easily implement it in Javascript as a enumerable get
property descriptor. About JSON.stringify
, it renders all enumerable
properties of a object, including getters, so it will initialize the field
and represent it as any other property. My previous decorator example could
be loosely interpreted as:
const foo = {
get bar() {
if (!this.hasOwnProperty('_bar')) {
this._bar = 3;
}
return this._bar;
},
};
// JSON.stringify would write the correct bar value, initalizing it if necessary
JSON.stringify(foo) // "{"bar":3}"
We could make a setter too, but the logic to implement it it's a lot more complex and subject to confusion
On June 27, 2018 10:11, Isiah Meadows <isiahmeadows at gmail.com> wrote:
If you're wanting to propose new places for decorators, you'd have much better luck here: tc39/proposal-decorators
I think that the main proposal is already long and complex, maybe it's a better idea to finalize it first and then start a new proposal about this others decorators places (as with function expression decorators and function parameters decoratos).
I agree the main proposal is long and complex, but this kind of addition could be designed with little effort to "fall out of the grid", since it has so much in common with classes already (properties, getters/setters, methods). The main question is with properties, but those are easy to just leave out, since function calls do just as well.
Function declaration/expression/parameter decorators are completely different beasts with little in common, so I feel it's a bad comparison.
An errata in my code The getter is mutating the object with a enumerable property, so consecutives invocations of JSON.stringify will result different from the first call (if the property is yet not initialized). The current problem is:
JSON.stringify(foo) // Returns "{"bar":3}"
// After the first bar "get" the object has now another property "_bar"
JSON.stringify(foo) // Returns "{"bar":3,"_bar":3}"
I did it as a quick loose scratch and didn't test it. A more robust implementation (with a helper function) probably would use closures to maintain the factory state. As exemplified:
const defineLazyProp = (obj, propName, valueFactory) => {
let selfDestroyingFactory = () => {
const value = valueFactory();
selfDestroyingFactory = () => value;
return value;
};
return Object.defineProperty(obj, propName, {
enumerable: true,
configurable: false, // It might be discussed if the lazy prop should be configurable or not
get() {
return selfDestroyingFactory();
},
});
};
const foo = {};
defineLazyProp(foo, 'bar', () => 3);
// This should work correctly now
console.log(JSON.stringify(foo));
console.log(JSON.stringify(foo));
It's a bit more verbose, but it's the best way I can think of "ponyfilling" it at the moment.
On June 28, 02:45, Isiah Meadows <isiahmeadows at gmail.com> wrote:
I agree the main proposal is long and complex, but this kind of addition could be designed with little effort to "fall out of the grid", since it has so much in common with classes already (properties, getters/setters, methods). The main question is with properties, but those are easy to just leave out, since function calls do just as well.
You are right, but yet I prefer to get the classes decorators advancing to a greater stage as soon as possible, a objet literal decorator would be a easy extension to the current proposal. By the way, I was comparing with the 2 others proposals by the fact that they are more like "extensions" to the main proposal, not by the complexity (as far as I know, mix the 3 decorators proposals in one would stall most of the work), but yeah they really are different beasts.
FWIW, individual parts of the "grid" don't all have to ship at the
same time. Private instance fields are farther along than private
static fields, for instance, and private methods are being considered
separately from private fields. Another example is with async
functions beating async generators/iterators and generator arrow
functions being stage 1 while everything else in the function "grid"
has a generator variant. And of course, the export foo from "..."
/export * as foo from "..."
proposals filling out that "grid".
And as for the decorators proposal itself, it's been sharply delayed from having its entire data model redone multiple times over the course of several months (according to their meeting notes). I don't think this requires nearly as much consideration as private fields, etc.
Isiah Meadows me at isiahmeadows.com, www.isiahmeadows.com
Hello TC39,
I recently opened an issue[1] in the tc39/ecma262[2] repository, proposing a new syntax for lazy getters, and I was directed to the CONTRIBUTING[3] page which stated that I should start a conversation on this mailing list. So, my feature proposal is to have syntactic sugar for creating lazy getters[4]. To summarize my original proposal (which you can read by following the very first link above), I find that creating lazy getters is very verbose. For example, consider: const take = (n, xs) => n === ? null : xs && { head: xs.head, get tail() { delete this.tail; return this.tail = take(n - 1, xs.tail); } };
My proposed solution is to add a new keyword lazy to the language. This keyword can only be used as a prefix to longhand property names in object initializers, and it defers the execution of the value expression until the property is accessed. In short, it's just syntactic sugar for lazy getters: const take = (n, xs) => n === ? null : xs && { head: xs.head, lazy tail: take(n - 1, xs.tail) };
This is purely syntactic sugar. The semantics of this new syntax would remain the same as that of the desugared syntax. In particular, calling Object.getOwnPropertyDescriptor(list, "tail") would return an accessor descriptor before accessing list.tail and a data descriptor afterwards. Furthermore, there are other advantages of having this syntactic sugar. For example, creating cyclic data structures becomes much easier. Examples are provided in my original proposal which is linked above. Hope to hear your thoughts on this. , Aadit M Shah
Links: