-
Notifications
You must be signed in to change notification settings - Fork 106
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
Homonyms and confused names into descriptors #173
Comments
In the spec, a property is either a data property or an accessor property - so i think that the term “property” is too generic to cover just getters/setters. “Descriptor” is the proper generic term; whether it’s a property descriptor or a decorator descriptor seems like it should be clear from the context? |
The spec also considers accessors to be methods, as they are part of MethodDefinition (https://tc39.github.io/ecma262/#sec-method-definitions).
From: Jordan Harband <notifications@github.com>
Sent: Saturday, November 10, 2018 11:16 AM
To: tc39/proposal-decorators <proposal-decorators@noreply.github.com>
Cc: Subscribed <subscribed@noreply.github.com>
Subject: Re: [tc39/proposal-decorators] Homonyms and confused names into descriptors (#173)
In the spec, a property is either a data property or an accessor property - so i think that the term “property” is too generic to cover getters/setters.
“Descriptor” is the proper generic term; whether it’s a property descriptor or a decorator descriptor seems like it should be clear from the context?
—
You are receiving this because you are subscribed to this thread.
Reply to this email directly, view it on GitHub<https://na01.safelinks.protection.outlook.com/?url=https%3A%2F%2Fgit.luolix.top%2Ftc39%2Fproposal-decorators%2Fissues%2F173%23issuecomment-437612654&data=02%7C01%7Cron.buckton%40microsoft.com%7C22e99111795547ea05ae08d64740e216%7C72f988bf86f141af91ab2d7cd011db47%7C1%7C0%7C636774741333160859&sdata=6r3r4aNGkdw4uCuRiToJreXUA1BZl5LwUa%2F01VpNam8%3D&reserved=0>, or mute the thread<https://na01.safelinks.protection.outlook.com/?url=https%3A%2F%2Fgit.luolix.top%2Fnotifications%2Funsubscribe-auth%2FADuNrECoTA3x-YwSQWXJGv3xLwGJ5PxAks5utyXTgaJpZM4YYHz_&data=02%7C01%7Cron.buckton%40microsoft.com%7C22e99111795547ea05ae08d64740e216%7C72f988bf86f141af91ab2d7cd011db47%7C1%7C0%7C636774741333170863&sdata=dI%2F9ldVQl21RarVfjc0qxd5E1eVz5XUkKPynfPoRFys%3D&reserved=0>.
|
That’s true, altho I’d argue that “method” for accessors was a confusing choice in the first place :-) |
I would have to disagree. Accessors have argument lists, method bodies, method scoping and evaluation rules, and are represented as functions in a property descriptor. That said, I generally classify class members as either a “method” (named member with “call” semantics), a “property” (named member with read/write semantics that can have side effects on access), or a “field” (named member with read/write semantics with no side effects on access). I’ve always felt that “Property Descriptors” in ECMAScript muddy the water a bit, terminology wise, but that’s my OOP/C# experience showing.
For ECMAScript I generally use the terms:
* Property Descriptor – The meta-information object that describes how a value or method is installed on an object.
* Member Descriptor – The meta-information object that describes a member of a class (be it a method, field, or accessor), including onto _which_ object representation of a class its associated Property Descriptor is to be installed (i.e. the prototype, the instance, or the constructor).
* Class Descriptor – The meta-information object that describes the shape of a class.
There _is_ overlap between member descriptors and property descriptors, but the proposal attempts to divide the responsibilities between the two so as reuse existing developer knowledge about how Property Descriptors work as well as to reuse existing spec language for defining properties (i.e. using CreateDataPropertyOrThrow, etc.). We could have just avoided using Property Descriptor as part of a Member Descriptor and merged the two for decorators, though I don’t know if it would have been worth it to sacrifice that reuse.
|
@pabloalmunia Thanks for your detailed writeup and thoughts here. I appreciate how you're providing guidance on ergonomics. It sounds like the basic model of decorators worked for you, and there are just these tweaks that could make it easier to implement your own decorators; is that right?
I'm a little skeptical of using How about
I'm pretty convinced by your argument here. Let's unbox the property descriptor into the surrounding decorator descriptor. I found the two-level structure annoying when writing up the examples for this repository, as well.
I'm not sure about this one. Fields and methods are pretty fundamentally different here. I don't want to add extra dynamic-ness and slow-ness to methods just to make things look the same (when syntax always produces a value that's always the same, and never has a side effect when evaluated), or extra complexity to fields to give them a two-version thing (when syntax always produces an initializer, which may have a side effect, not a fixed value). |
I like the idea of flattening the |
OK, does someone want to write a PR for this flattening? |
Could we simplify this further and use |
Initializers are optional for fields, but I imagine we could use In that case, we could just omit the kind then, and use other uniquely named properties for finishers and initializers. |
if (desc.kind === "field") {
// a field
} else if (desc.kind === "method") {
// a method
} else if (desc.kind === "accessor") {
// an accessor
} if far better than if (desc.initializer === null || desc.initializer) {
// a field
} else if (desc.value) {
// a method
} else if (desc.get || desc.set) {
// an accessors
} |
Yes, |
FWIW, big +1 for
@littledan - I can do a PR for this, though not within the next week -- probably the week after, if that would be useful. |
Thanks for laying the changes out so clearly. Two separate PRs for each of those would be great. If you're busy next week, let's leave this task open for others to pick up (it might be me), and you can do it if you come back and it's still not done yet. |
Flattening the element descriptor, the Adding in #182's changes, that would give us:
¹ (for There's also likely to be #183's |
Hmm, I would be fine with replacing value with method if others want to. |
It should receive the replacing element as a parameter to use it's parts though. I see it in following way: {
key: 'connectedCallback',
kind: 'finish',
replace({descriptor: {value, ...descriptor}, ...propertyDescriptor}) {
return {
descriptor: {
...descriptor,
value() {
value.call(this);
// do some rendering stuff
}
},
...propertyDescriptor,
};
}
} Hmm, ability to change key in |
@tjcrowder, BTW, your proposal looks really great. It addresses a lot of pain points I've got after working with stage 2 decorators. Flattening the decorator is nice catch as well as My 5 cents: I believe that It could look following with typescript interface: interface ReplaceDescriptor {
key: PropertyKey | PrivateName;
kind: "start" | "finish";
replace(descriptor?: Descriptor): Descriptor | null;
} I still don't know if it would be better to allow changing key in |
The idea of the replace callback is that it would be called with the constructor, not the descriptor. For your use case, would it work to use a method decorator on connectedCallback to annotate it when it's in the class, and a class decorator to create it when it's not provided? Would this be too ugly from an ergonomics perspective? |
@Lodin - Thanks! But just FWIW, this is mostly not my work, just me gathering together the work of others (along with some minor points of my own). The question of a |
@littledan, let me describe the project I'm working on. I'm working on the Web Component framework based completely on the decorators. It's called Corpuscule, still in WIP, but I'm going to finish it soon. Among other packages there is @element('my-element')
class MyElement extends HTMLElement {
@attribute('foo', Boolean)
foo;
@property(v => typeof v === 'string')
bar = 'test';
connectedCallback() {
if (this.foo === undefined) {
this.foo = 'Default value';
}
}
[render]() {
return html`
<div>
${this.foo}
<slot></slot>
${this.bar}
</div>
`;
}
} As you can see, there is no basic class, no If we work with the basic class, as we do in the
However, I chose the third path. We have decorators, don't we? And in the current state decorators allows to re-declare method already declared in the class. So all we need is just to save the user's method, re-declare There even is a way to extend Custom Element marked with However, all of these approaches require access to Well, I would agree that
Yes, this idea may work, but it is quite noisy for user as well. It has almost no difference with calling |
@Lodin - I'm probably missing a subtlety here. :-) You still have access to For instance, this works (it's obviously just a quick-and-dirty) with the current @babel/plugin-proposal-decorators, and while some details may change thanks to this issue and #182, none of the functionality goes away: function assert(condition, message = "assertion failed") {
if (!condition) {
throw new Error(message);
}
}
function element(name) {
return function elementDecorator(desc) {
let {kind, elements} = desc;
assert(kind === "class");
let connectedCallbackElement = elements.find(e => e.kind === "method" && e.key === "connectedCallback");
let superConnectedCallback;
if (connectedCallbackElement) {
const original = connectedCallbackElement.descriptor.value;
connectedCallbackElement.descriptor.value = async function(...args) {
await Promise.resolve(); // stand-in for "invalidate"
superConnectedCallback.call(this);
return Reflect.apply(original, this, args);
};
} else {
elements.push({
key: "connectedCallback",
kind: "method",
placement: "prototype",
descriptor: {
async value() {
await Promise.resolve(); // stand-in for "invalidate"
return superConnectedCallback.call(this);
}
}
});
}
desc.finisher = function(cls) {
superConnectedCallback = Object.getPrototypeOf(cls.prototype).connectedCallback;
};
desc = elements = connectedCallbackElement = undefined;
};
}
class FakeHTMLElement {
constructor(x) {
this.x = x;
}
connectedCallback() {
console.log(`FakeHTMLElement's connectedCallback called, x = ${this.x}`);
}
}
@element("my-element1")
class Example1 extends FakeHTMLElement {
}
new Example1("one").connectedCallback();
// Expect:
// "FakeHTMLElement's connectedCallback called"
@element("my-element2")
class Example2 extends FakeHTMLElement {
connectedCallback() {
console.log(`Example2's connectedCallback called, x = ${this.x}`);
}
}
new Example2("two").connectedCallback();
// Expect:
// "FakeHTMLElement's connectedCallback called"
// "Example2's connectedCallback called" |
@tjcrowder hmm... I feel little confused. @littledan said that P.S. Thanks a lot for your idea of getting supers! That may be way better solution than mine current! |
@Lodin - I see what you mean. So this whole discussion has nothing to do with this issue (#173), nor with #182 which is sort of mentioned in passing and which I thought you were commenting on. I have to admit I found it quite confusing, I was looking for context in the issue you posted the comment on. :-) Perhaps you could raise a new issue, if removing |
@tjcrowder, yep, you're right, I feel kinda lost in all discussions here :) I'm going to create another issue. |
This is an ergonomics improvement, as discussed in #173
OK, I'm starting to write up the spec text for this change, and a few things occur to me:
I'd love to hear your thoughts on these potential changes; I'm working on writing them up, together with the above discussed changes, as specification text. |
This is an ergonomics improvement, as discussed in #173
I'm a bit confused - anything with |
Well, if you look at the way finishers are used in example code, then forcing them to be separated out into separate extras would just be a bunch of extra boilerplate object literals. That's all. It seems just as unambiguous to permit this, Could you elaborate on your concerns about maintenance? |
To clarify on this point, right now you can have many finishers registered, and each finisher can either return undefined or a new class. They run in an order based on who asked for them, and they will run based on the class returned from the previous one, or the one before that if the previous one returned undefined. I'm proposing that we keep doing that. |
ah - i was under the impression that it wouldn't make sense for a decorator to run on "not the item it was typed next to", but i guess it also makes sense that if i typed two decorators, and the first replaced the item, that it'd be reasonable to assume that i (the class author) intended that behavior. It might help if you could show me (or link to) pairs of examples, where one side has them separate and another condensed. |
I am not suggesting anything that is really different from how finishers work in the current spec, where the finisher is then not passed to the next decorator. The only difference is how it shows up in the elements array. |
Oh, I realized a big flaw with this idea to allow start/register/replace callbacks together with the element: Frequently, you'll want a static register callback on top of an own/prototype class element. This will still have to be an extra. So if the ergonomics improvement is gone in the most common case, probably best to not include this complexity. |
I thought we'd decided to change
That makes sense. (And goes well with the name
If the programmer includes both
I think you've already decided not to do this for other reasons, but in case you're still thinking about it: While I like avoiding having to add elements, I would worry a bit about the semantics. Adding a |
I think it’s very helpful to make one word require a replacement be provided, and a different word disallow one. |
@ljharb - Agreed. The suggestion in #182 (comment) was that |
Do you think a hook with the same placement and multiple callbacks is something that will come up very often? I wonder if we should restrict it to one, just so we don't have to specify the relative order of replace/finish |
I'm afraid I don't have a feel for that. But if I needed to specify more than one, I know it would irk me slightly if I couldn't. My gut is that specifying But for me, allowing all three and specifying the order ( |
I think it's not necessary to allow finish and replaced together, as replace subsumes finish--you can just put all your finish logic in replace. We don't have to decide on any nontriviak relative ordering logic for start vs replace, so it seems fine to allow that combination. |
* Normative: Rename kind: "initializer" to kind: "hook" * Normative: Rename initializer For hooks, rename to start For fields, rename to initialize Keep the internal slot [[Initializer]], as class fields uses that name * Normative: Replace finisher with replace, finish hooks As discussed in #173
Homonyms and confused names into descriptors
We have found a group of homonyms (same word with different meanings) and other confused names into descriptor objects.
I have collected some opinions of my team and I include them here with humility. We are not specialists in the definition of programming language standards, we are just a team of enthusiastic programmers of this proposal.
"method"
Cases:
kind: "method"
with a method included intovalue
.kind: "method"
with a getter / setter included intodescriptor.get
anddescriptor.set
.#162
Alternatives:
kind: "property"
for getter / setter.kind: "accessor"
for getter / setter.We vote for
"property"
and obtain this values forkind
:"class"
,"method"
,"field"
and"property"
."descriptor"
Cases:
descriptor or decotator descriptor is the generic name used for parameters and returns into decorator functions.
descriptor
is a field of decorator descriptor with anObject.defineProperty()
descriptor.#162 (comment)
Alternatives:
descriptor
field and include its member into the first level of decorator descriptor.propertyDescriptor
.We vote for remove
descriptor
and change this:into this:
"initializer"
vs"descriptor.value"
Cases:
kind: "initializer"
is used for stand-alone initializer."initializer"
is the field used whenkind: "initializer"
and include a function than is callback for side effect."initializer"
is a field used whenkind: "field"
and include a function than return the initial value for this field.The field
"descriptor.value"
is used whenkind: "method"
and include the function assigned as value.When a programmer define a field with a method as value:
the value is included into
initializer
:Alternatives:
initializer
with a funtion than return the initial value (primitive, function, object)descriptor.value
(orvalue
) for initial values andinitializer
only for side effect.We are no sure. We intuit that
initializer
function is important in cases where initial values can not be safely included indescriptor.value
. Perhaps the solution is use alwayinitializer
function and return a function, included whenkind: "method"
.The text was updated successfully, but these errors were encountered: