-
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
Update README with more details #354
Conversation
README.md
Outdated
@logged | ||
m(arg) { | ||
this.#x = arg; | ||
- Class _properties_, defined by applying the `prop` keyword to a class field. These have a getter and setter, unlike fields, which default to getting and setting the value on a private storage slot (equivalent to a private class 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.
Slight ambiguity from the phrasing here: it currently reads as though fields default t o getting and setting on a private storage slot.
- Class _properties_, defined by applying the `prop` keyword to a class field. These have a getter and setter, unlike fields, which default to getting and setting the value on a private storage slot (equivalent to a private class field): | |
- Class _properties_, defined by applying the `prop` keyword to a class field. Unlike fields, these have a getter and setter, which default to getting and setting the value on a private storage slot (equivalent to a private class field): |
README.md
Outdated
- `"class"` | ||
- `"method"` | ||
- `"init-method"` | ||
- `"getter"` | ||
- `"setter"` | ||
- `"field"` | ||
- `"prop"` |
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.
Given the discussion below, I'm led to wonder whether there's value in distinguishing private and public here as well—given that there are values you can count on being present or absent for public or private fields or methods respectively. 🤔
I see that this is covered by the isPrivate
boolean value as well; just thinking about what the experience consuming it would be like. It may well be that narrowing down the thing being decorated by first checking kind
and then checking isPrivate
is totally reasonable.
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.
private is an orthogonal property that can apply to almost all of these. I think it'd be a bit awkward if you were forced to parse the string, if we had, e.g., "public-method"
vs "private-method"
, to figure out if it's a method at all.
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.
Yeah, that makes sense. 👍🏼
class C { | ||
#x = 1; | ||
@init: bound method() { return this.#x; } | ||
@validateString |
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.
@validateString | |
@validatePrivateString |
README.md
Outdated
@logged | ||
m(arg) { | ||
this.#x = arg; | ||
- Class _properties_, defined by applying the `prop` keyword to a class field. These have a getter and setter, unlike fields, which default to getting and setting the value on a private storage slot (equivalent to a private class 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.
“property” already has a meaning (a normal object property) - i think it’s important here to come up with a term that isn’t overloaded already. Class properties already exist: they’re static fields and methods.
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 with this point and considered it when writing this, it's something I want to bring up in the upcoming meetings. If we do stick with the prop
keyword, then this conflict may pop up in the future, especially in common usage among software devs. We should definitely consider alternatives, both for the name of the construct itself, and for the keyword, but there was broad consensus that the keyword was the best option suggested so far.
README.md
Outdated
```ts | ||
type ClassMethodDecorator = (value: Function, context: { | ||
kind: "method"; | ||
name?: string; |
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.
surely this could also be a Symbol?
name?: string; | |
name?: PropertyKey; |
README.md
Outdated
```ts | ||
type Decorator = (value: Input, context: { | ||
kind: string; | ||
name?: string; |
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.
name?: string; | |
name?: PropertyKey; |
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.
Agreed, or alternatively, string | symbol
to make it more readable for people who might not know the jargon.
README.md
Outdated
```ts | ||
type ClassGetterDecorator = (value: Function, context: { | ||
kind: "getter"; | ||
name?: string; |
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.
name?: string; | |
name?: PropertyKey; |
README.md
Outdated
```ts | ||
type ClassDecorator = (value: Function, context: { | ||
kind: "class"; | ||
name: string; |
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.
what gets passed here for an anonymous class, or one that gets an inferred name? I’d assume undefined
is appropriate for the former (the empty string probably wouldn’t be). I haven’t tested it, but i think the latter means the name could also be a Symbol
README.md
Outdated
``` | ||
|
||
### Limited access to private fields and methods | ||
Properties, unlike fields, define a getter and setter on the class prototype. This getter and setter default to getting and setting a value on a private slot. The above roughly desugars to: |
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.
what is the point of doing this? How is an undecorated prop
any different than a public field? (i suppose it does a brand check, but is there a use case 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.
The answer to your question about how an undecorated prop
differs from a public field in semantics is below in this document. As for use cases: I could imagine using prop
fields to faithfully emulate other cases in JS and the web platform where such patterns are used by built-in features. But really, what brings them over the bar for being "worth it" to add to the language is decorators, IMO.
I think @decorator prop field;
makes a lot more sense than @prop: decorator field;
since prop
changes the shape of the class in a way that's unrelated to the application of any one particular decorator. (I guess init
in this proposal has that same property, though.)
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.
Right, the thinking is that prop
fundamentally changes the semantics of a field, and then decorators layer naturally on top of the new semantics. While the semantics on their own have limited use cases, there are many use cases for decorators with these semantics. Given the other constraints around shape changes and static analyzability, this option keeps the application of decorators consistent while solving all of the use cases.
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.
Separate from the name (which is imo a nonstarter), I don't think we should be adding fundamental field semantics in the absence of decorators for imagined use cases.
I think @prop: decorator
and @init: decorator
are indeed the same in this sense, and i'd prefer to see both of them remain exclusively attached to decorator syntax.
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 difficulty comes with combining @prop: decorator
and @decorator
declarations then. Does applying one @prop: decorator
then turn the field into a prop-field, and all other decorators now need to handle that prop-field? Likewise with init-methods.
This to me signals that the @prop:
style modifier is fundamentally in the wrong location. It is not actually modifying the decorator, it is modifying the element itself, so it is correct to place the modifier on the element, regardless of whether it has utility on its own.
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.
Another thing to note is that the current location would compose nicely with the proposed Grouped Accessors and Auto-Accessors proposal. Like prop
, users would be able to modify a field declaration with { get; set; }
to create getters and setters on it. prop
would effectively become syntactic sugar for this style of declaration.
The reason we decided not to use the auto-accessor syntax was so that we aren't blocked on that proposal, since it is currently in stage 1, and because the syntactic sugar of the keyword was seen as a good thing among users who will have to use prop
to convert their existing decorators.
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.
But yes, if you compose decorators, and one of them adds prop, then the rest need to handle that, just like if a class decorator changes the .name or identity of a class, the rest of the decorators need to handle that.
So let's dig in a bit more there. If this is the case, then all three of these would be a valid way to get a prop, and effectively be equivalent, no?
@prop: a @b @c x = 123;
@a @prop: b @c x = 123;
@a @b @prop: c x = 123;
// Can also call it on all of them
@prop: a @prop: b @prop: c x = 123
It seems like this creates more confusion than it helps, again because the transform is not coming from the decorator itself, its coming from the base element being changed.
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.
It's coming from the way the decorator is called, essentially, yes - just like the this
value and the arguments of a function do.
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.
hmm, I think there may be a bit of a miscommunication. Are you suggesting that the value would only change after the @prop:
decorator? e.g.
@a // prop
@prop: b //prop
@c // field
x = 123;
That feels like it would make a bit more sense. I still think it would be a major refactoring hazard though
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.
Another possibility would be to allow prop
and field
decorators to interleave, and call field decorators as field decorators instead:
@a // kind: 'field'
@prop:b // kind: 'prop'
@c // kind: 'field'
x = 123;
I think this might work, since the field decorators would just be returning initializer functions, which could be tacked onto the private slot for the prop
README.md
Outdated
```ts | ||
type ClassInitMethodDecorator = (value: Function, context: { | ||
kind: "init-method"; | ||
name?: string; |
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.
name?: string; | |
name?: PropertyKey; |
|
||
#### Class Initialized Methods | ||
|
||
Class initialized methods are a new construct, defined by adding the `init` keyword in front of a class method: |
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.
What are the use cases for this in the absence of a decorator?
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 what I'm more suspicious of; I'd prefer we stick with @init:
.
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.
Similar to the prop
keyword, the main point is to change the semantics of methods so that decorators can layer naturally on top, replacing a init-method
with another init-method
. This is important for use cases such as @bound
, where some initialization logic needs to run with the method.
Doing it this way keeps it consistent with the strategy for prop
, so we don't have two different ways of introducing alternative semantics for decoratable values. @rbuckton also thought that this made more sense than @init:
, because it does change the semantics of the element.
I agree though that the potential use cases for init
on its own are even more limited than the use cases for prop
on its own.
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.
Changing the semantics in a decorator is fine tho, the issue was about not being able to statically know how the semantics were being changed.
README.md
Outdated
} | ||
|
||
C.prototoype[Symbol.metadata]["#x"]; |
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.
What happens if i also have @myMeta ['#x'] = whatever;
on the same class?
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.
Ah, this is a great point, I hadn't considered that. I think we'll need to change this somehow to account for that.
const ret = value.call(this, ...args); | ||
console.log(`ending ${name}`); | ||
return ret; | ||
}; |
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.
Caution with arrow function and this
.
function logged(value, { kind, name }) {
if (kind === "method") {
return function (...args) { // Must be a regular function
console.log(`starting ${name} with arguments ${args.join(", ")}`);
const ret = value.call(this, ...args); // this is used here
console.log(`ending ${name}`);
return ret;
};
}
}
// ending m | ||
```js | ||
class Example { | ||
@bound init myMethod() { |
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.
What was the motivation for using this syntax rather than @init: bound
? I got a lot of feedback in the direction of preferring the @init:
syntax.
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 main reason is for consistency with the prop
keyword and strategy for changing semantics of a decorator target. If we were to introduce @init:
syntax, it would mean adding two separate ways to do this, and the rules for applying @init:
seem less well defined.
One possible benefit of @init:
I suppose is that it could also apply to other decorators. For instance, class decorators could use it, which would solve some of the problems brought up in #329:
@init: defineElement('my-element')
class MyElement extends Element {}
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.
Some initial comments, but this PR is a big improvement--thanks for putting in this work.
|
||
In general, decorators receive two parameters: | ||
|
||
1. The value being decorated, or `undefined` in the case of class fields which are a special case (see below for details). |
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.
You could make this sound smoother by saying "undefined
in the case of class fields, as these don't have a corresponding shared value" (a similar amount of text but less scary-sounding)
README.md
Outdated
```ts | ||
type Decorator = (value: Input, context: { | ||
kind: string; | ||
name?: string; |
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.
Agreed, or alternatively, string | symbol
to make it more readable for people who might not know the jargon.
README.md
Outdated
- `"class"` | ||
- `"method"` | ||
- `"init-method"` | ||
- `"getter"` | ||
- `"setter"` | ||
- `"field"` | ||
- `"prop"` |
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.
private is an orthogonal property that can apply to almost all of these. I think it'd be a bit awkward if you were forced to parse the string, if we had, e.g., "public-method"
vs "private-method"
, to figure out if it's a method at all.
type ClassGetterDecorator = (value: Function, context: { | ||
kind: "getter"; | ||
name?: string; | ||
access?: { get?(): unknown }; |
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 basically to do the private brand check, right? It'd probably be good to clarify that somewhere.
- `access`: An object containing methods to access the value. This is only available for _private_ class elements, since public class elements can be accessed externally by knowing the name of the element. These methods also get the _final_ value of the private element on the instance, not the current value passed to the decorator. This is important for most use cases involving access, such as type validators or serializers. See the section on Access below for more details. | ||
- `isStatic`: Whether or not the value is a `static` class element. Only applies to class elements. | ||
- `isPrivate`: Whether or not the value is a private class element. Only applies to class elements. | ||
- `defineMetadata`: Allows the user to define some metadata to be associated with this property. This metadata can then be accessed on the class via `Symbol.metadata`. See the section on Metadata below for more details. |
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.
Is this a new closure each time, or does it read an internal slot of the context object? (Either answer seems OK to me, but we have to decide.)
README.md
Outdated
``` | ||
|
||
### Limited access to private fields and methods | ||
Properties, unlike fields, define a getter and setter on the class prototype. This getter and setter default to getting and setting a value on a private slot. The above roughly desugars to: |
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 answer to your question about how an undecorated prop
differs from a public field in semantics is below in this document. As for use cases: I could imagine using prop
fields to faithfully emulate other cases in JS and the web platform where such patterns are used by built-in features. But really, what brings them over the bar for being "worth it" to add to the language is decorators, IMO.
I think @decorator prop field;
makes a lot more sense than @prop: decorator field;
since prop
changes the shape of the class in a way that's unrelated to the application of any one particular decorator. (I guess init
in this proposal has that same property, though.)
|
||
#### Class Initialized Methods | ||
|
||
Class initialized methods are a new construct, defined by adding the `init` keyword in front of a class method: |
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 what I'm more suspicious of; I'd prefer we stick with @init:
.
} | ||
``` | ||
|
||
This example could be roughly desugared as follows: | ||
Initialized methods are methods that are defined on the prototype, but then set as an instance property. The above roughly desugars to: |
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.
Is this a commonly needed thing, or just solving for @bound
?
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.
There is also the event handler decorator which @justinfagnani has raised before, and @leobalter raised the @wire
decorator provided by Salesforce's component library (scroll down to "Decorate a Function with @wire").
qux: { "my-meta": true, [MY_META]: true }, | ||
}; | ||
|
||
C.prototype[Symbol.metadata] = { |
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.
How are fields and methods with the same name differentiated?
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.
My thinking was that they would not be, given this is not an extremely common pattern, and init-method
s make it even less necessary. Decorators on elements of the same name would be coalesced, and if your decorator needed to do something specific for the field or the method, it could include the value it decorated in the metadata itself (e.g. using kind
).
I think @ljharb's point about the collision between private fields and dynamic public fields is more of a problem though, since those are actually different names, one is just the "spelling" of the private field but isn't at all related to the public field. I think that means that we'll need to introduce namespacing for private values no matter what, so we could consider namespacing different types of elements as well. I was mainly worried about a combinatorial explosion there (private fields, public fields, private methods, public methods, etc...) but maybe that is not avoidable.
README.md
Outdated
|
||
If it's not acceptable to require a superclass/mixin for cases requiring initialization action, then the `@init:` decorator syntax in a method declaration allows decorators to add initialization actions, run when the constructor executes. | ||
#### Privatizing metadata |
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.
#### Privatizing metadata | |
#### Hiding metadata |
2276f88
to
fba22a0
Compare
Updates the README with a more detailed breakdown of the API, along with a number of tweaks to the APIs including: - Adds the `prop` keyword and class element type - Adds the `init` keyword and class element type - Updates field semantics to not transform to getter/setter pairs by default - Adds the `access` property to the context object for private elements - Refactors the metadata API to be more intuitive and well defined
fba22a0
to
6f4409d
Compare
Considering the last README is outdated and there are just so much we need to discuss for the current README, I'd suggest we just have this PR landed and break outstanding comments into separate issues. I've seen so much already for feedback I don't feel encouraged to add anything else to be fixed or changed in this single PR. Formatting wise, it seems fair enough. Generally I like the current direction this is going. |
Discussed this with @littledan and @leobalter and we agreed it makes sense to merge this and continue to iterate on the open questions. I'll open up new issues to discuss the points brought up by @ljharb and others, and we'll add them to the agenda for the next decorators meeting. |
Updates the README with a more detailed breakdown of the API, along with
a number of tweaks to the APIs including:
prop
keyword and class element typeinit
keyword and class element typeaccess
property to the context object for private elements