-
Notifications
You must be signed in to change notification settings - Fork 105
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
Normative: Add initializer callback for side effects #165
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If key is null and any placement is provided, that looks to me like programmer error. Could we instead throw when a placement is provided unnecessarily, just like we throw in #30?
How does providing a placement for “no property” make sense? |
@ljharb It affects when it runs and what the receiver is. The typical usage will be to pass |
Isn't this trying entirely too hard to avoid the constructor? This is very quickly approaching the "magic" annotations of Java. Adding repeatable nonsense to the list of properties just to trigger side effects can:
|
@littledan gotcha, that makes sense then - thanks. |
|
I like the idea, but it seems a bit strange to have a field name provided to the decorator for it to just throw it away. How hard would it be to have a decorator without a name, just a init? class Ex {
@dec x = 1
// no init
@sideEffect ;
// or some init
@sideEffect this.x = 2; // Option A
@sideEffect { this.x = 2 }; // Option B
} |
@jridgewell That makes more sense visually, but what would the decorator receive? Is the side effect independent of the field it's supposed to act against? |
Better question, if the decorator is just supposed to trigger some side-effect without regard to any particular field, then why isn't it a class-level decorator? |
@jridgewell I am not sure if we need that syntax extension. The hope here is that these side effects will be used by semantically field-like declarations, or added by a class decorator. Do you have a use case in mind for those other forms? |
I guess not, it was just my first reaction. |
I probably didn't explain the use cases very clearly in the above commit message. As an example, you could make a decorator to give a field declaration class MyComponent extends HTMLElement {
@set onclick = (event) => { /* ... */ }
} As the following: function set(desc) {
assert(desc.placement === "own");
assert(desc.kind === "field");
assert((typeof desc.key)[0] === "s"); // symbol or string
assert(desc.descriptor.configurable);
assert(desc.descriptor.writable);
assert(desc.descriptor.enumerable);
return {key: null, placement: "own", descriptor: { },
initializer() { this[desc.key] = desc.initializer.call(this) } };
} |
Instead of |
@nicolo-ribaudo That suggestion sounds totally reasonable to me. Want to write it up in a replacement PR? |
I can't probably do it this week since I don't have my PC, but if no one does it first I hope I will be able to do it next week. |
Many decorators will want to perform a side effect when instantiating a class, for example: - To select [[Set]] rather than [[DefineOwnProperty]] semantics for defining a field - To store the field in an entirely different place, neither an ordinary property or private field (e.g., MobX stores some fields in a Map) - To register the instance under construction in some way (e.g., from a class decorator). "Instance finishers" have been proposed for this purpose, but it's not clear when to run such a finisher. For many use cases (e.g., interacting with the DOM), an instance "starter" is just as good, due to run-to-completion semantics. This patch permits decorators to create elements of the form { kind: "initializer", placement: "own", initializer: fn } to be used purely for their side effect, with evaluation semantics and ordering identical to fields, and interspersed with them in evaluation order.
71725e4
to
9f6d669
Compare
Updated to include |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- I don't remember its name right now, but there is a function which checks that there aren't duplicated keys which probably should be updated to handle ElementDescriptors without [[Key]].
- Maybe we should throw if the
initializer
property of a{ kind: "initializer" }
element isundefined
, since it would be almost certainly a programming error?
spec.html
Outdated
@@ -689,7 +720,7 @@ <h1>DecorateConstructor ( _elements_, _decorators_ )</h1> | |||
1. Append _elementsAndFinisher_.[[Finisher]] to _finishers_. | |||
1. If _elementsAndFinisher_.[[Elements]] is not *undefined*, | |||
1. Set _elements_ to the concatenation of _elementsAndFinisher_.[[Elements]] and _privateElements_. | |||
1. If there are two class elements _a_ and _b_ in _elements_ such that _a_.[[Key]] is _b_.[[Key]] and _a_.[[Placement]] is _b_.[[Placement]], throw a *TypeError* exception. | |||
1. If there are two class elements _a_ and _b_ in _elements_ such that _a_.[[Kind]] is not `"initializer"` and _b_.[[Kind]] is not `"initializer"`, _a_.[[Key]] is _b_.[[Key]], and _a_.[[Placement]] is _b_.[[Placement]], throw a *TypeError* exception. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can this be made more readable? (I don't know if it valid from a spec-grammar point of view)
1. If there are two class elements _a_ and _b_ in _elements_ such that all the following conditions are true:
1. _a_.[[Kind]] is not `"initializer"`
1. _b_.[[Kind]] is not `"initializer"`
1. _a_.[[Key]] is _b_.[[Key]]
1. _a_.[[Placement]] is _b_.[[Placement]]
1. Then throw a *TypeError* exception.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good idea, done
spec.html
Outdated
1. Let _initializer_ be ? Get(_elementObject_, `"initializer"`). | ||
1. Let _elements_ be ? Get(_elementObject_, `"elements"`). | ||
1. If _elements_ is not *undefined*, throw a *TypeError* exception. | ||
1. If _kind_ not `"field"`, | ||
1. If _kind_ not `"field"` or `"initializer"`, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Typo: "If kind is not"
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
spec.html
Outdated
<ul> | ||
<li>_element_.[[Key]] absent.</li> | ||
<li>_element_.[[Descriptor]] is absent.</li> | ||
<li>_element_.[[Placement]] is `"own"`.</li> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We might consider allowing "static" as the decorator way of doing https://github.com/tc39/proposal-class-static-block
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree; static should be allowed here since they also can have initializers
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't see this as a way towards a static block. This is just a way that a decorator can add extra code to run. But decorators are already able to run code at the end of when a class is executing, in the finisher callback. We could allow this out of consistency (it would have slightly different timing from finishers), but I don't see a use case.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A use case is the same as for "own" - being able to invoke a setter on a superclass (in this case, on a superclass's constructor that had, say, static set foo(v) {}
)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@ljharb What would be the problem with using a finisher for that?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Certainly you could use a finisher for that - but then you'd be forced to use a class-level decorator to change the semantics of a specific field, so instead of:
class C {
@useSet static a = x;
static b = y;
@useSet static c = z;
}
you'd need to have:
@useSet('a')
@useSet('c')
class C {
static a = x;
static b = y;
static c = z;
}
or:
@useSet('a', 'c')
class C {
static a = x;
static b = y;
static c = z;
}
both of which are decidedly less ergonomic and elegant.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't see why that would be the case. You can use a finisher on a static field declaration too.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@ljharb, wait, but you can use finisher
with a field or a method decorator. It is not required to use class-level decorators to activate finishers.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ahh, that wasn't clear to me. In that case - instead of this "initializer" approach, is there a reason not to allow finishers on "own" fields, to provide an "instance finisher"?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@littledan I have a concern about Then I came to a thought: why not to do the |
spec.html
Outdated
1. Let _elements_ be ? Get(_elementObject_, `"elements"`). | ||
1. If _kind_ is `"initializer"`, | ||
1. If _key_ is not *undefined*, throw a *TypeError* exception. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
shouldn't this check happen right after Let _key_ be ? Get(_elementObject_,
"key")
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I previously tried to sort the validations towards the end, but I can do them mixed inline if you prefer.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I feel like in general we strive to have validations occur as soon as possible, to avoid unnecessary observable behavior.
@Lodin I agree that there's overlap between initializers and fields; that's why I started off this PR by saying it was a field with a null key. I also agree that static initializers are not extremely interesting; that's why this patch omits them. A |
@littledan Oh I see. My bad, started to read, caught the idea and immediately wrote it. Thanks for explanation. Well, then, maybe decorators don't need |
Finishers are something different, for when the class itself is done being created. They are useful for instance elements as well, e.g., to attach runtime metadata. |
To @ljharb 's point on the overlap between finishers and initializers: This patch ignores the return value of initializers, but a finisher which returns non-undefined can replace the constructor. The other issue is a subtle one about ordering: All the finishers run after all the field declarations. If we were to try to have initializers subsume finishers, as @ljharb seems to be suggesting above, we'd make the following changes:
(Should we rename initializer to finisher? It's a bit confusing to juggle the term initializer for what this PR introduces and initializer for what a field has.) |
I still argue that a field should have [[Set]] semantics by default. That said, do we really need this if you can do constructor replacement in a finisher? |
Regardless of whether Set or Define was the default, I think you'd want to be able to use a decorator on a single field to have the other semantic, without having to replace the entire constructor and deal with the |
Let's keep the Set vs Define discussion in the class fields repo. @rbuckton I don't think you would even need constructor replacement for it, just the ability to perform a side-effecting operation (which you would get either from finishers or static initializers). The above post is about, if we want to try to replace finishers with initializers (as @ljharb suggested above), then how would we do it. Do you see any part of the above plan that wouldn't provide a replacement for finalizers while enabling the kind of decorator described in the commit message? Would it be too complicated, or unergonomic? |
I am very concerned this will further muddy the waters for the decorators proposal. I think we would be better served to consider this as a follow-on proposal rather than adopt it as part of the current proposal. In addition I feel it hijacks the semantics of If we were to add this, I would be more willing to accept something like |
@rbuckton Are you talking about this PR or the plan in #165 (comment) ? The later comment would be like your last paragraph, except the "instance finisher" would run just after all the field initializers and before the constructor body. |
How could an instance finisher work in a nice way? I'm trying to understand it using static fields/class finishers, but I'm struggling to implement even simple thigns (replace [[Define]] with [[Set]]): how can I replace a field with a finisher? This works, but it puts the decorator and the field in different places: @useSetOn("foo")
class A extends Base {
static foo = 2;
}
function useSetOn(name) { return _useSetOn.bind(null, name); }
function _useSetOn(name, descriptor) {
let index = descriptor.elements.findIndex(el => el.key === name);
let { initializer } = descriptor.elements[index];
descriptor.elements.splice(index, 1);
descriptor.finisher = Class => {
Class.foo = initializer.call(Class);
};
} This doesn't work (it throws): class A extends Base {
@useSet static foo = 2;
}
function useSet(descriptor) {
let { initializer, key } = descriptor;
return {
finisher(Class) {
Class[key] = initializer.call(Class);
}
};
} Btw, with finishers instead of initializers this would never work: class A {
@useSet foo = 2;
bar = this.foo;
} |
I was envisioning a field decorator able to run an instance finisher as well, to avoid that very problem. |
I used class finishers with static fields, but if we had instance finishers I could ask the same question for instance field. |
@nicolo-ribaudo This is a good point. It seems like I was being too optimistic about one thing subsuming the other. Maybe we should just stick with the |
If finishers aren’t a solution, then I’m still not sure why this PR wouldn’t cover both own and static. |
A possible fix to my point would be to make finishers be declared like normal element descriptors, similarly to the function useSet(descriptor) {
let { initializer, key } = descriptor;
return {
kind: "finisher",
placement: "static",
finisher(Class) {
Class[key] = initializer.call(Class);
}
};
} However, if I had to choose I'd still prefer class A {
@useSet foo = 2;
bar = this.foo;
} |
If class A {
@dec foo = 2;
}
function dec(desc) {
return {
...desc,
key: null
}
}
(new A)[null]; // 2 |
Also move error checks in ToElementDescriptor to happen ASAP
@ljharb @nicolo-ribaudo Oh, right. Thanks for bearing with me. The most recent commit permits initializers to be static (or prototype, since why not), and doesn't make any of the other changes mentioned, since we've deduced upthread that they don't make sense. Thoughts? |
One question remains: Should we look at the return value of the initializer callback, and throw an exception if it's not undefined? This could help avoid programmer errors of the kind @rbuckton may be thinking of, and it could make the non-undefined return value a possible future extension point. |
👍 Decorators, even being as powerful as possible, should be as strict as possible to avoid as many potential errors as possible. |
spec.html
Outdated
@@ -563,7 +617,7 @@ <h1>Element Descriptors</h1> | |||
<p>An <dfn>element descriptor</dfn> describes an element of a class or object literal and has the following shape:</p> | |||
<pre><code class=typescript> | |||
interface ElementDescriptor { | |||
kind: "method" or "field" | |||
kind: "method", "initializer" or "field" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
kind: "method", "initializer" or "field" | |
kind: "method", "initializer", or "field" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
@@ -689,7 +743,12 @@ <h1>DecorateConstructor ( _elements_, _decorators_ )</h1> | |||
1. Append _elementsAndFinisher_.[[Finisher]] to _finishers_. | |||
1. If _elementsAndFinisher_.[[Elements]] is not *undefined*, | |||
1. Set _elements_ to the concatenation of _elementsAndFinisher_.[[Elements]] and _privateElements_. | |||
1. If there are two class elements _a_ and _b_ in _elements_ such that _a_.[[Key]] is _b_.[[Key]] and _a_.[[Placement]] is _b_.[[Placement]], throw a *TypeError* exception. | |||
1. If there are two class elements _a_ and _b_ in _elements_ such that all of the following are true: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This reads a bit awkwardly; maybe it should be in an abstract operation?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is following @nicolo-ribaudo 's suggestion above. I think the factoring might be awkward too; let's work out these editorial issues during Stage 3.
spec.html
Outdated
@@ -781,26 +842,38 @@ <h1>ToElementDescriptor ( _elementObject_ )</h1> | |||
<emu-alg> | |||
1. Assert: _elementObject_ is an ECMAScript language value. | |||
1. Let _kind_ be ? ToString(? Get(_elementObject_, `"kind"`)). | |||
1. If _kind_ is not one of `"method"` or `"field"`, throw a *TypeError* exception. | |||
1. If _kind_ is not one of `"initializer"`, `"method"` or `"field"`, throw a *TypeError* exception. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
1. If _kind_ is not one of `"initializer"`, `"method"` or `"field"`, throw a *TypeError* exception. | |
1. If _kind_ is not one of `"initializer"`, `"method"`, or `"field"`, throw a *TypeError* exception. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
1. If _kind_ is `"method"`, | ||
1. If _initializer_ is not *undefined*, throw a *TypeError* exception. | ||
1. Let _elements_ be ? Get(_elementObject_, `"elements"`). | ||
1. If _elements_ is not *undefined*, throw a *TypeError* exception. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this use a HasProperty, instead of a Get, since it’s just checking existence? Or is the intention to allow an explicit undefined property - and if so, that seems like it’d be an error, so why allow it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The Get matches the use of Get when actually using the elements in a class decorator, per POLS.
Any further concerns with this patch? @rbuckton It's unclear to me whether we've addressed your concerns. In my opinion, it's important to get this change in Decorators v1, because I don't want an idiom of "throwaway fields" (used for their side effect) to emerge. I believe it would be very difficult for implementations to optimize away the storage space used in throwaway fields, so I don't want to encourage it in the ecosystem. These have been recommended in various issue threads, and the need for a side effect in this path has been known for more than a year. |
OK, given the all-around positive reviews, merging this PR. We'll review it in the November 2018 TC39 meeting, and revert or revisit if necessary. Thanks for the help, everyone! |
Many decorators will want to perform a side effect when instantiating
a class, for example:
defining a field
property or private field (e.g., MobX stores some fields in a Map)
a class decorator). "Instance finishers" have been proposed for this
purpose, but it's not clear when to run such a finisher. For many use
cases (e.g., interacting with the DOM), an instance "starter" is just
as good, due to run-to-completion semantics.
This patch adds a third decorator descriptor kind of "initializer" to register
an initializer to run, interspersed with field initializers.
This PR depends on #163 and #164
Fixes #44