-
Notifications
You must be signed in to change notification settings - Fork 0
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
Declarative own-state objects. #3
Comments
@ljharb Do you have any idea why this problem wasn't targeted when |
This is a pretty radical departure from how properties that need to be initialized to objects per-instance have always been handled up till now, even in pre-ES6 code that using a coding style that puts all properties on the prototype, e.g.: function MyClass() {
this.obj = {}
}
MyClass.prototype.primitive = 1
MyClass.prototype.obj = null Up till now, classes have mainly just been sugar for what we previously did with constructor functions and prototypes (and of course the more conventional version of that would initialize all non-method properties in the constructor, in contrast with the above example, but that's beside the point I want to make now...) If you're going this far to change the behavior of prototype properties initialized to objects, I feel like you'd be better off going all the way and giving classes that use these new property declarations actually different semantics than objects as they exist in the current version of the language. I am making a devil's advocate argument; I don't think this is a feasible option nor a good idea at this point in JS's evolution. But what you're suggesting introduces magic and side-effects that developers would not be expecting at all, so in a way it would be better to just be up-front about it rather than offering a hybrid solution that pretends to compile down to familiar old constructor and prototype patterns when really it doesn't for special cases like this. And this isn't even considering the fact that a large number of developers have already been using public property declarations (mostly in Babel 6, in the default loose mode with [[Set]] semantics—but I digress) and have a different expectation for the same syntax, namely that it only creates an instance property, not a prototype property and certainly not a proxy. And then there are performance considerations, even with a native implementation of this idea... While I like how this would avoid the foot-gun of accidentally shared prototype properties and might be somewhat persuadable on this, I think there are much simpler solutions to this problem. IMO the current class fields proposal offers the simplest and best solution (methods always go on the prototype, fields always go on the instance, unless decorators change the placement). Alternatively I would be open to a hybrid solution equivalent to my above code sample, where properties initialized to objects receive special treatment...but affecting how the constructor works in that way goes against the stated goals of this proposal. |
Haven't I been one of the loudest voices saying this? Instance-properties makes the same violation by trying to eschew the constructor with a pseudo-declarative statement inside the class definition, and that causes more problems than it solves.
This isn't going far at all. This kind of solution hits the fly with a swatter no bigger than the fly itself. The problem preventing developers from using public properties for this purpose is that public properties don't duplicate values to the instance on write when the value is an object (unless the entire object value is replaced). This technique just makes that happen. It only happens if the value is a non-primitive, non-function value. I've even considered adding the additional exception of not using this technique if the object is frozen, with the idea that freezing the object means that it is not expected to ever be modified and that the developer is willing to take the risk.
I'm fairly certain this would be easily explained. It only took 8 words to explain it to my desk-mate at work: "It makes objects copy on write like primitives." He's fresh out of college with only about a year of JS experience.
One of the beautiful things about this approach is that the effective result is nearly exactly the same as what you'd get from instance-properties. As soon as an attempt to modify the object on any level is made, an instance-specific copy is made and the modification applied to it. From that point on, it's no different than an instance-property at all. So in most cases, no code changes will be needed.
Yes and no. Truth is, the way ES is defined means that there's going to be complexity somewhere when trying to solve this problem. With instance-properties, the complexity becomes semantic problems. Over the past few years, the proponents of class-fields have had to make some difficult decisions because of this. With my approach, the complexity becomes 2 issues only:
I'd prefer it if there were some way that the engine could accomplish this without using a proxy, so that the original value could live on the prototype, potentially be modified before class instantiation, and still produce a unique copy as soon as a modification request happens. In comparison to instance-properties, it produces almost exactly the same result with 4 exceptions:
Other than that, the results are identical to that of instance-properties. For me, the value of 1, 3, & 4 along with losing the inconsistency of instance-properties in a class definition, making the [[Set]] vs [[Define]] debate irrelevant, completely restoring the viability of decorators in all contexts without sacrificing inheritance features, etc... (all the semantic problems with instance-properties), all while getting rid of a foot-gun, are worth the small amount of added complexity. |
Wow. That was long winded. After saying all of that I say this: That's not the only solution I've come up with, but it is by far and large, the most complete one. |
@ljharb The idea above should directly address airbnb's issue. But if it is not suitable, there's another way, but it's definitely a code change. class A {
let a = {}; // This object is instance specific since this `let` statement is evaluated in an instance closure.
get a { return a; }
set a(v) { a = v; }
}; Something else occurred to me. What if we take @mbrowne's suggestion of class A {
let a = {}; // This object is instance specific since this `let` statement is evaluated in an instance closure.
prop a(a);
}; The definition would be something like;
where the identifier name must be an instance variable of the class. |
So you’re saying that the code on the RHS of the
|
Exactly.
The definition I gave above was a syntax fragment. Doesn't ES define a
I just hadn't thought that far ahead yet. I was just throwing out an idea to see where it landed. As long as the expression in the parenthesis is valid for both the RHS and LHS, I don't see why it shouldn't work. |
This is a key point. It just seems like unnecessary overhead to require the creation of a getter and setter, even if there is a concise syntax to do so, simply to achieve an effectively public property. |
I've been thinking about that this whole time, but the first suggestion I made really is the only way to make that possible without causing a bunch of semantic problems like class-fields has. I realize that the second suggestion isn't exactly what @ljharb is asking for because it requires making an accessor property. However, given a shorthand for defining the accessor property, and given the fact that an engine can optimize this into direct access to the instance variable when the accessor is referenced, I don't see any real downside to the second approach. |
The spec can’t assume optimizations; if the optimization is necessary to be performant, then it should be required. |
I don't think of the optimizations as necessary for performance. I'm just aware that an extra tick or two can be gained via optimizations. The simple accessor property will already be fairly efficient even without optimization. Sure, not quite as fast as direct access, but for a clean approach that doesn't introduce unwanted semantic issues, and still gives you everything else that you need and most of what you want, this is a good and viable approach. What is airbnb doing that requires a publicly accessible instance-initialized structure? |
React components, primarily. |
What are the odds that when some version of private appears, that these react components will still need to have a public object initialization? |
100%, since the interface react requires needs to be public for react itself to access it. |
That might not necessarily be true if I included a means of handling protected in this proposal. I've already thought it up, and it fits right in as just an extension of what this proposal is already doing. However, I don't want to add that here. The only reason I brought it up is because the 1 interface that react has (that I'm aware of) which works like you're describing is If that is a viable possibility, then I'm willing to add my protected (keyword: |
Protected is imo inherently inappropriate for JavaScript, and it would be a mistake to attempt to include it in this proposal. state is the only instance property; altho there’s many static ones. It will remain public for the foreseeable future, it’s not a viable possibility to make that use case (not a problem) go away. |
As I stated before, I don't want to add it to this proposal, but... Here's a topic where I'd like to have another one of those offline conversations. From where I sit, instance-properties are equally as out-of-place in ES as you seem to think protected is. What's more, while the implementation I'd provide for protected carries no semantic oddities, instance-properties threaten to either break nearly landed future proposals or break existing functionality, with no apparent happy medium. In a language like Java, there's no doubt that React's I get that you don't think protected has a place in ES due to your binary view of accessibility in ES. However, following that kind of thinking to it's logical conclusion, private object data that is not internal to the engine also has no place in ES, yet that's what we're all trying to add. The only thing in ES that even resembles private space is a closure. Closures are only viable when a function somehow returns a function. So only functions have access to closures, not non-function objects. We're trying to extend the language to do something people want but can't do easily. That means adding capabilities to the language that aren't really part of its paradigm. Such was the case for |
Tangent: I strongly disagree with their reasoning because I think it's throwing the baby out with the bathwater; you shouldn't just throw out classes just because there's a learning curve. It's important to note that React will probably always continue to support classes: "There are no plans to remove classes from React." I would like to say I'm 100% confident that the team at my company will continue to use classes for non-trivial stateful React components no matter what, but I have to admit that if in the future, React adds a bunch of new useful features to function components and starts treating classes like second-class citizens (which may or may not happen), then we might consider switching to all function components. |
@rdking I have verified that React (as currently implemented internally) needs to be able to replace the |
(I just realized that React's need to reassign state might not change anything about your proposed solution of using protected state to resolve this. But perhaps what I said is still informative :) ) |
@mbrowne What you've said about React is indeed informative. When I looked at the code for state, it does indeed appear to be something that needs to not be an own property by good coding practices. In fact, a good portion of the complaints React is using to justify their Hooks idea is predicated in part by an absence of a solution to protected in ES. |
@ljharb Why do you think protected is "inherently inappropriate for JavaScript"? I'm not a big fan of it either because it conflates access control and inheritance, but there are times when restricting access only to an inheritance hierarchy is exactly what you want to do...for example a library that wants to give user-written subclasses access to certain properties or methods without making them public. I could certainly live without protected in JS but what is the inherent problem with it? |
@mbrowne whether you want to do it or not isn't the point; there's no way to robustly share privileged access while fully denying it to others short of declaring all classes/functions in the same scope. "private" and "public" in JS aren't "access modifiers", they're indications of reachability - a binary status. A thing is either reachable, or not. |
@ljharb it sounds like you're describing existing mechanisms for whether or not a variable is reachable in the current version of JS. Why do we need to be limited to current features? Obviously JS wasn't built for access modifiers from the ground up, but would it be impossible to add proper support for them? The whole reason for TC39's existence as I understand it is to extend and improve the language... There may be plenty of reasons not to add this feature but I don't understand your argument; it sounds like, "We can't add this feature because the language doesn't currently have the prerequisites needed for this feature." |
@mbrowne it's not about the ability to add support for it; it's that it's impossible to do it in a robust way, as far as I know, so that you can dynamically subclass a base class forever, and have that base class be able to share "protected" data with all subclasses, but deny access to it from non-subclasses - because I'd thus always be able to make a subclass and explicitly expose to the world the ability to read the data. JS isn't strictly typed - and strict typing is what tends to prevent things from accessing "protected" data in other languages. |
@ljharb Ah, that makes more sense now, thank you. What about object properties that are accessible only within the same module? (So not talking about |
I'm not sure because no such proposal exists - but I'm pretty confident that the current class fields proposal, nor any alternatives, would have any impact on that in either direction. |
Yes, and the class fields proposal does not break with that, despite your claims that it does. It’s a new feature, and it’s certainly a new semantic, but that a) isn’t bad, and b) isn’t breaking anything. |
...despite requiring a semantic contradiction to work
...despite the fact that a base class can mask prototype properties defined in subclasses. I've already accepted that we'll just have to agree to disagree about this. I've also accepted that despite having made a very rigorous analysis that disagrees with the board, you guys are going to do whatever you want. So class fields is going in, and that will be that. A great many developers will be happy that it did. This proposal, however, is not about public fields. This proposal is about an implementation of private data that is both semantically flexible, and syntactically palatable. Although I'd hate it, it's easily possible to merge the public fields proposal with this one. The very nature of how this proposal operates means that initialization of the instance can happen at the same time as initialization of the private members. I'm not so willing to "die on a hill" over a small bad that I'd be willing to let a bigger problem slide by. |
For this class-members proposal, do you think it would be an acceptable compromise to put public properties both on the prototype and on the instance—at least for properties initialized to objects, if not all of them? |
Can it be done? Yes. However, you have those who have the same mindset as @ljharb who think it is somehow harmful for a data property to exist on a prototype. |
Truth be told, I would much rather see special semantics built up around proto so that any attempt to modify the object currently assigned to it causes a copy of the entire object plus its modification to appear as an own property, but without the Proxy and property splitting I used as a proof-of-concept. It seems like this is something that should have been done from the beginning. |
I don't see how you could make such a change while maintaining backward compatibility. I think we have seen plenty of examples over the years of people doing ill-advised things that rely on quirks in the language, and however ill-advises they might have been, new features shouldn't break such code. And even if you could maintain backward compatibility, unfortunately it would still be too surprising if there were such a big difference between prototypes of objects created by classes and those that were not. I too wish this foot-guns never existed in the language but we can't change it. Regarding properties on both the instance and prototype, I certainly think that would be better than simply putting all properties on the prototype without doing anything to prevent the foot-gun. It seems you and @ljharb have totally incompatible goals, but at least that would be some sort of compromise, if you're interested in making this proposal more widely acceptable to those who want public property declarations. |
Just to test my understanding, would something like the following still be a violation of the why not fields premise of this proposal:
|
In my mind, no. When I think of fields, I'm thinking of imperative property declarations in the class definition. What you've put in this example looks like what happens under the hood in other languages. They tend to build internal templates instead of prototype objects. Those templates are cloned to create the instance objects. If this is what you're intending with that example, then no. There's no conflict. A template is a definition and makes perfect sense in a |
That's the thing I can't fully comprehend. I still have yet to see any real advantage to fields that isn't already available via other, more expressive approaches. I also don't see any problem with using the constructor that couldn't be solved with an explicit, in-constructor fix. He mentioned that the reason super isn't injected into class definitions provided by the developer is because this is to enable some legacy feature where base constructors won't be called when the developer wants to return a custom object. For those cases, I'm partial to thinking that factory functions are a better fit than classes, though I admit that classes make the prototype manipulation far simpler. It just seems to me that the default for classes that extend should be to have a missing super defaulted in and that some syntax be provided to stop that from happening when that's the desire... but that ship has sailed. In the end, I really want to understand what real benefit is to be had by defining instance-specific properties outside the constructor that wouldn't be gained by simply fixing the object-on-prototype foot-gun directly. Since it is a behavior that is avoided by all discerning developers, there shouldn't be many if any code bases affected. The correction could also be placed behind a If something like that were done, then there would be absolutely no reason to avoid putting properties of a class on the prototype. At first write attempt on any part of anything on the prototype, it would first be copied to the instance. That would mean that all prototype properties would effectively be instance properties, and that the prototype only holds default values. Unless I'm missing something, this is essentially what we all want from a class. |
I think that's the only one of your proposed solutions to this issue that actually seems viable.
I think there are those who would still want to avoid putting data properties on the prototype even if the foot-gun didn't exist, because they don't want a prototype chain lookup for every property access. But I am not in that camp so I can't really speak for them. |
...of course, if the copy-on-write behavior for objects were behind a flag, the next question is, how would it work by default? I think putting properties on the prototype by default but leaving the foot-gun would be disastrous...however well-known the foot-gun may be (which will probably be less common knowledge over time as the next generation's main exposure to OO in JS is via classes), contrary to your expectations I think most people would be surprised that Also, I wonder how feasible it would be to have two different semantics depending on whether your proposed strict flag is on or off...sounds like a tough sell to the committee and implementers, and it might not even be possible technically without breaking something or adding too much overhead. |
A hard sell? Yeah, probably. But it is the best solution I've come up with so far. I'm not done thinking about the problem, but a surgical solution like this is what's needed. Overhead? I don't think it would be so much. |
This proposal now includes a form of declarative own state. Still don't think it really makes much sense, but it's not like I can't see the utility. |
So the I think the simplest solution would be to just have instance properties and require the use of a decorator to change an instance property to a prototype property (the way class-fields does it), but we've already been over that many times and clearly you're against it. Alternatively I suppose it could be the other way around (default to prototype, use decorator for instance) and consider it an acceptable downside that Object.keys, etc wouldn't work, as long as you implemented copy-on-write for objects using a new kind of strict mode. I guess this (current) compromise solution where you try to please all parties would be OK and I could get used to it, but it does seem odd and the choice of keywords (why is |
Yes. The
While I understand your point of view, the simple fact that both [[Set]] & [[Define]] semantics lead to issues for the language is proof to me that neither is a good choice. The best place to define any property that every class instance should have is the prototype. That suffers none of the issues in either [[Set]] or [[Define]] semantics on the instance. The best place to define instance-specific data to initialize those properties is in the constructor. These are realities that should not be so easily overlooked just because developers made sloppy mistakes in coding while extending a class.
I'm offering a solution for that as well. From where I sit, when
I also think it was the absence of the ability to declare public properties in ES has grown from a toy-like scripting language when it was first released, to a full general purpose language as it is today. Anything added to it should further that general purpose standing. Class-fields, public-fields, and to a certain extent, decorators, all take a very large step in the wrong direction. The proposals I'm offering can send things in the right direction. As a point of note: haven't you noticed that no-one can come up with any reason why this proposal is inferior to class-fields? Or has some critical flaw that can't be circumvented? The fact is, they won't be able to find it because it likely doesn't exist. This proposal was created by taking the best concepts from class-fields, private-symbols, and classes-1.1, as well as accounting for as many of the flaws and foot-guns from those proposals as could be reasonably accounted for. Then on top of that, the fine details have been massaged so as to ensure that proposals like decorators, the wants of board members like @ljharb, and the issues raised by those who have voiced a rational option, could all be accounted for in one go. Sure, it has things like |
This isn’t accurate; the same issues exist because the prototype might have a setter on its prototype chain. |
@ljharb It's accurate. If the base class declares something, and a derived class declares something by the same name, either it's a mistake by the developer, or the intent is to override the base. That's normal. It's no different than if a derived class declares a new version of a function declared by the base. The difference with instance properties is that they break the inheritance chain in both directions. It should never be possible for a base class to overwrite what a derived class has set up. |
Let me say that a different way: |
Something that would be nice (but wouldn't be accepted due to existing code) would be if all properties on a prototype constructed by |
Thanks for the clarifications.
If you're only talking about meeting the technical requirements (as the committee sees them), it seems that the proposal in its current form now meets them (and exceeds them in some ways), thanks to your additions/changes to classes 1.1. But if we're talking about the full picture, including developer expectations, syntactic consistency, and avoiding unnecessary over-engineering of the spec, then clearly there are still counterarguments against this proposal and valid reasons to prefer class fields, however much you disagree with those reasons. But I'd rather not get into a fresh debate about the basic syntax (i.e.
You could still easily accomplish your desired behavior with a combination of class fields and decorators, could you not? For example: @placeAllPublicPropertiesOnPrototype
class X {
...
} And it seems to me that the use cases where prototype placement is actually necessary and preferable—or would even make a difference (assuming the object property foot-gun is correctly handled)—are rather special and rare... If you're talking about modifying prototypes of already-instantiated objects, I'm not sure that's even a good idea in the first place if you want your code to be highly readable and predictable. |
I'm afraid not, at least, not without a forest of decorators, some of which could only be implemented in the engine itself. The core problem I have with class fields comes down to 3 different things:
It seems so trivial, doesn't it? That's what makes it so easy for TC39 to dismiss. The problem is that each one of those first 2 issues causes a forest of other issues that no one will notice until they need to do something that brings them face-to-face with those issues. I've been developing and writing languages for a long, long time. I've long since developed the skills to see the issues just from the proposal description. @hax has done a fairly good job of detailing these issues across several threads. Each one by itself is dismiss-able, but the sheer quantity of issues needing to be dismissed is just too much. So if it were just the 3 issues with no forest of side effect issues, I wouldn't even have begun arguing, let alone writing this counter proposal. That's not a fight I would have thought worth the picking. However, class-fields is bad for the language. If I were a TC39 member, I would have veto'd it several times over by now. To use one of @ljharb euphemisms, "I'd die on a hill" before letting class-fields reach stage 4. Sadly, I have neither a seat on nor the political clout to persuade TC39. |
To be clear, I was talking specifically about the desire to place data properties on the prototype, which was the context of your statement that I quoted. Many of the other issues you've raised relate to private members which are a separate issue. |
I apologize. The fact that class-fields has all of these topics merged into 1 proposal, compounded by the fact that they've all got numerous issues makes it hard for me to to keep these topics and issues separated in my mind. Even keeping that in mind, most of my original post doesn't change. Sure, decorators can be used to put properties on the prototype. However, class-fields itself still suffers from the very same issues I spoke of before. I don't see it as worth while to create a forest of decorators just to avoid the pitfalls and bad design of class-fields. Compound that with the fact that a decorator cannot solve the ASI issues with property declarations at all, and I can't help but come to the distinct conclusion that class-fields is a rushed proposal that while well thought out, has gone too far in the wrong direction and will irreparably damage the language if it reaches stage 4. Notice how even if I stay on topic, I end up in the same place? The fact that I want properties to live on the prototype is little more than a desire to ensure that the language doesn't slowly drift into being class-based instead of prototype based. That's one of the bigger issues I have with class-fields. Those who are backing it do not seem to have a clear image of how to map the class metaphor into a prototype-based language without changing the basis. |
It's possible to mitigate some issues by introducing some decorators and linter rules (to enforce using such decorators). For example, we can create a linter rule to enforce using class X {
@field foo
@field bar
} It can solve the ASI hazard issue. We can also make the But is such solution "easily"? I don't think so.
The truth is, such solution can never be "easy" and just transfer the cost from the committee to the community. |
@mbrowne
Each of those items is equally important to me, and this proposal reflects that. The simple truth is that this proposal inherits from every other proposal that's been made for the given feature set. The particulars of the design are just the side effect of trying to eliminate issues without losing functionality. |
@ljharb @mbrowne
If I understand what's going on in #1, the 2nd problem being targeted is the need to declare a property initialized with an object, and having that object be an instance-specific copy each time. Is this what you guys really want? If so, this is something good that I see as worth doing, but I have a much better approach than the problematic implementation that exists in class-fields. It doesn't introduce "fields" of any kind. It doesn't require trying to make a declarative thing from something inherently imperative either.
Here's the idea:
Since this proposal resolves all property initializers at the time of
class
definition evaluation, should any property be initialized with an object, that objects will be wrapped in a Membrane with a handler that traps any attempt to modify any portion of the object. When any such trap triggers, the property is duplicated onto the instance with the original value, and the modification operation is forwarded onto the instance.Basically, this:
would become this:
So what do you guys think?
The text was updated successfully, but these errors were encountered: