Skip to content
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

Closed
rdking opened this issue Nov 3, 2018 · 96 comments
Closed

Declarative own-state objects. #3

rdking opened this issue Nov 3, 2018 · 96 comments

Comments

@rdking
Copy link
Owner

rdking commented Nov 3, 2018

@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:

class A {
  x = 0;
  y = {lat: 1, long: 2};
  constructor() {
      this.y.lat += 3;
  }
}

would become this:

var A = (function() {
  var handler = {
    duplicateAndForward(op, target, prop, ...args) {
      var retval = false;
      var obj = {lat: 1, long: 2};
      this.owner[this.root] = obj;
      for (var p of this.path) {
        obj = obj[p];
      }
      return Reflect[op](obj, prop, ...args);
    },
    defineProperty(target, property, descriptor) {
      if (!this.owner) {
  	    retval = false;
      }
      else {
        retval = this.duplicateAndForward("defineProperty", target, property, descriptor);
      }
      return retval;
    },
    deleteProperty(target, property) {
      return this.duplicateAndForward("deleteProperty", target, property, descriptor);
    },
    get(target, prop, receiver) {
      var retval = Reflect.get(target, prop, receiver);
      if (retval && ['function','object'].includes(typeof(retval))) {
        retval = Proxy(retval, {
          owner: this.owner || target,
          prop,
          path: [].concat(this.path, [this.prop]),
          __proto__: handler
        });
      }
      return retval;
    },
    set(target, prop, value, receiver) {
      return this.duplicateAndForward('set', target, prop, value, receiver);
    }
  };
  return class A {
    x = 0;
    get y() { return new Proxy({lat: 1, long: 2}, {owner: this, root: 'y', prop: "y", path: [], __proto__: handler}) }
    set y(value) { Object.defineProperty(this, "y", {
      enumerable: true,
      configurable: true,
      writable: true,
      value
    }); }
    constructor() {
      this.y.lat += 3;
    }
  }
})();

So what do you guys think?

@rdking
Copy link
Owner Author

rdking commented Nov 4, 2018

@ljharb Do you have any idea why this problem wasn't targeted when class was first being decided? Or is this the reason class was released without data property support?

@mbrowne
Copy link

mbrowne commented Nov 4, 2018

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.

@rdking
Copy link
Owner Author

rdking commented Nov 4, 2018

Up till now, classes have mainly just been sugar for what we previously did with constructor functions and prototypes

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.

If you're going this far to change the behavior of prototype properties initialized to objects, ...

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.

But what you're suggesting introduces magic and side-effects that developers would not be expecting at all...

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.

And this isn't even considering the fact that a large number of developers have already been using public property declarations

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.

I think there are much simpler solutions to this problem.

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:

  • the property became an accessor pair
  • the object was wrapped in a proxy.

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:

  1. The value of the property is available for all base class constructors.
  2. The property's initial value should not be used as a WeakMap/Map key since it may be replaced in the future.
  3. The value exists on the prototype.
  4. If we allow for a prototype closure as well, we can easily make it so that the usual ability to change the values of a prototype's properties for these kinds of fields is upheld.

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.

@rdking
Copy link
Owner Author

rdking commented Nov 4, 2018

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.

@rdking
Copy link
Owner Author

rdking commented Nov 4, 2018

@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 prop and modify it just a bit so the above could be re-spelled like this:

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;

DefaultAccessorDefinition[Yield] :
  `prop` PropertyName `(` IdentifierName `)` `;`

where the identifier name must be an instance variable of the class.

@ljharb
Copy link

ljharb commented Nov 4, 2018

So you’re saying that the code on the RHS of the let would be ran per-instance - presumably at a specific time, like “at the top of a base class constructor, or after super” - but it’d be private. (note that this would make it a “private field”)

prop propertyName(something) would define a public property called “propertyName” (how would you handle computed names, and symbols? presumably with brackets), and the something inside the parens would be only a private field name or an in-scope identifier? Why could it not be any expression, including referring to identifiers in the outer scope and calling instance methods? Restricting it seems like it greatly reduces the usefulness, and does not in fact cover the actual use case - which is “any code” evaluated per instance - without forcing a private field to be created solely to make its value public.

@rdking
Copy link
Owner Author

rdking commented Nov 4, 2018

So you’re saying that the code on the RHS of the let would be ran per-instance - presumably at a specific time, like “at the top of a base class constructor, or after super” - but it’d be private. (note that this would make it a “private field”)

Exactly.

how would you handle computed names, and symbols?

The definition I gave above was a syntax fragment. Doesn't ES define a PropertyName like this?

PropertyName[Yield, Await]:
  LiteralPropertyName
  ComputedPropertyName[?Yield, ?Await]

Why could it not be any expression, including referring to identifiers in the outer scope and calling instance methods?

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.

@mbrowne
Copy link

mbrowne commented Nov 4, 2018

without forcing a private field to be created solely to make its value public.

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.

@rdking
Copy link
Owner Author

rdking commented Nov 5, 2018

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.

@ljharb
Copy link

ljharb commented Nov 5, 2018

The spec can’t assume optimizations; if the optimization is necessary to be performant, then it should be required.

@rdking
Copy link
Owner Author

rdking commented Nov 5, 2018

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?

@ljharb
Copy link

ljharb commented Nov 5, 2018

React components, primarily.

@rdking
Copy link
Owner Author

rdking commented Nov 5, 2018

What are the odds that when some version of private appears, that these react components will still need to have a public object initialization?

@ljharb
Copy link

ljharb commented Nov 5, 2018

100%, since the interface react requires needs to be public for react itself to access it.

@rdking
Copy link
Owner Author

rdking commented Nov 5, 2018

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 state. If I provided some kind of protected interface support, then react could change state to be such a protected element (given that it's not meant to be accessed publicly, but needs to be shared with Component subclasses. If that happened, then public initialization with an instance-specific object would no longer be needed and the problem would just go away.

If that is a viable possibility, then I'm willing to add my protected (keyword: shared) idea to this proposal. Since shared members would still be non-property members, they would still benefit from per-instance initialization.

@ljharb
Copy link

ljharb commented Nov 5, 2018

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.

@rdking
Copy link
Owner Author

rdking commented Nov 5, 2018

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 state would have been declared protected. I realize that's not an argument in favor of adding something like protected to ES. However, it does show that the concept of classes in ES is still not complete, even after adding a mechanism for privacy.

React's state property shouldn't be public because it's not meant to be accessed outside the inheritance hierarchy of Component. At the same time, it cannot be private since only Component itself would have access. It's only public because "there is no other choice." React isn't the only library/framework with that issue. In fact, if you went to the same library/framework authors that were questioned about facets of class-fields and asked, I'd be willing to bet that they'd tell you that a large percentage of the fields they currently mark with an _ should have a protected-like status instead of simply private.

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 Symbol, let, generators, WeakMap, and many other features. It won't hurt ES to complete the basic class model by adding a way to share privacy in a limited fashion.

@mbrowne
Copy link

mbrowne commented Nov 5, 2018

Tangent:
While I don't think it changes the principles being discussed here, since there are plenty of other libraries that rely on public instance properties and will continue to want to do so, I think it's worth noting that Facebook is planning to stop using classes completely for new React components they write going forward:
https://reactjs.org/docs/hooks-intro.html#classes-confuse-both-people-and-machines

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.

@mbrowne
Copy link

mbrowne commented Nov 5, 2018

@rdking I have verified that React (as currently implemented internally) needs to be able to replace the state object from the outside, e.g.: someComponent.state = newState. This is just the nature of React's FP approach to state management, where the end result of setState() is a new state object, rather than mutating the existing one. (I think this allows them to optimize things better internally, similarly to how using PureComponent optimizes things in userland by taking advantage of immutability. Also similar to redux of course.) The need for a publicly writable state property could be avoided if React used Object.defineProperty to set the new state rather than a simple assignment, but I kind of doubt that the React team would be inclined to make such a change.

@mbrowne
Copy link

mbrowne commented Nov 5, 2018

(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 :) )

@rdking rdking self-assigned this Nov 5, 2018
@rdking
Copy link
Owner Author

rdking commented Nov 5, 2018

@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.

@rdking rdking removed their assignment Nov 5, 2018
@mbrowne
Copy link

mbrowne commented Nov 5, 2018

@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?

@ljharb
Copy link

ljharb commented Nov 5, 2018

@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.

@mbrowne
Copy link

mbrowne commented Nov 5, 2018

@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."

@ljharb
Copy link

ljharb commented Nov 5, 2018

@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.

@mbrowne
Copy link

mbrowne commented Nov 5, 2018

@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 protected anymore, but rather a separate concept of internal module state.) I brought this up a while back in the class-fields repo and someone pointed out that there's no way to implement this given the current modules spec. But would it be possible to add such a feature in the future?

@ljharb
Copy link

ljharb commented Nov 5, 2018

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.

@ljharb
Copy link

ljharb commented Nov 15, 2018

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.

@rdking
Copy link
Owner Author

rdking commented Nov 15, 2018

It’s a new feature, and it’s certainly a new semantic, but that
a) isn’t bad, and

...despite requiring a semantic contradiction to work

b) isn’t breaking anything

...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.

@mbrowne
Copy link

mbrowne commented Nov 16, 2018

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?

@rdking
Copy link
Owner Author

rdking commented Nov 16, 2018

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.

@rdking
Copy link
Owner Author

rdking commented Nov 16, 2018

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.

@mbrowne
Copy link

mbrowne commented Nov 16, 2018

I would much rather see special semantics built up around proto

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.

@mbrowne
Copy link

mbrowne commented Nov 17, 2018

Just to test my understanding, would something like the following still be a violation of the why not fields premise of this proposal:

class MyComponent {
  publicInstanceTemplate {
    state = {
      foo: 1
    }
  }
}

@rdking
Copy link
Owner Author

rdking commented Nov 18, 2018

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 class.

@rdking
Copy link
Owner Author

rdking commented Nov 18, 2018

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.

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 'use strict'-like statement so as to ensure that code that really does use the foot-gun feature in some useful way isn't broken.

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.

@mbrowne
Copy link

mbrowne commented Nov 18, 2018

The correction could also be placed behind a 'use strict'-like statement so as to ensure that code that really does use the foot-gun feature in some useful way isn't broken.

I think that's the only one of your proposed solutions to this issue that actually seems viable.

Unless I'm missing something, this is essentially what we all want from a class.

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.

@mbrowne
Copy link

mbrowne commented Nov 18, 2018

...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 x = ... syntax would put something on the prototype. So it would be making an existing bad situation with prototype properties far worse...even veteran JS devs would likely be affected by it if they just tried to follow their intuition when using this new feature.

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.

@rdking
Copy link
Owner Author

rdking commented Nov 19, 2018

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. __proto__ is already a special internal slot. Adding semantics to accessing it shouldn't be that difficult. And when you consider how small that painful proxy implementation above is, the equivalent functionality in engine code seems like it should be even smaller.

@rdking
Copy link
Owner Author

rdking commented Dec 20, 2018

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.

@rdking rdking closed this as completed Dec 20, 2018
@mbrowne
Copy link

mbrowne commented Dec 21, 2018

So the inst properties were added to make this proposal more acceptable to the committee and those who want public instance properties (myself included)...but if I understand correctly, you have another proposal in mind (something like a strict mode for prototype properties) that would allow prop to cover most use cases for public properties, is that correct? I guess the current proposal makes sense if you want to help prevent the committee from dismissing it out-of-hand due to lack of public instance properties. But setting that aside, if I were looking at this proposal for the first time, I would find it odd that there is both prop and inst, especially since they're both "properties".

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 prop a prototype property?) doesn't seem very logical/consistent. Not that I have a better suggestion, if there need to be dedicated keywords for both kinds of declarations...just thinking out loud here.

@rdking
Copy link
Owner Author

rdking commented Dec 23, 2018

@mbrowne

...if I understand correctly, you have another proposal in mind (something like a strict mode for prototype properties) that would allow prop to cover most use cases for public properties, is that correct?

Yes. The prop should already cover most use cases, except where there is a need to initialize a public property with an object. I still feel that use case is best covered in the constructor given the way __proto__ currently works. If accepted, proposal-safe-prototype would allow the foot-gun issue to be selectively removed.

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.

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.

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'm offering a solution for that as well. From where I sit, when class was introduced, maybe even before that, someone should have proposed something like proposal-known-keys to augment the current set of object reflection methods. Object.keys and the like were only ever meant to handle own properties. However, the real power of a prototype-based language can't be seen or used until the properties on the prototype chain can be fully used as if they were pre-initialized own properties.

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 prop a prototype property?) doesn't seem very logical/consistent.

prop is a prototype property because (imo) a property common to all instances of a class will exist on the prototype. I know this view isn't popular, but just consider the fact that a class method is nothing more than a prototype property that just happens to contain a function. I believe that if something like proposal-safe-prototype had been added to ES before class, then class itself would have already had public properties, and those properties would be on the prototype. I think the main reason this wasn't done was because no one came up with a workable solution for the foot-gun.

I also think it was the absence of the ability to declare public properties in class that caused developers to become accustom to properties declared in the constructor. Now they have grown accustom to that, developed habits and patterns around that, and are looking for a way to simplify that way of doing things. I get it, but look at the cost. Attempting to advance this is helping some at the cost of hurting others.

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 inst in it that I don't like, but you and others seem to want it badly enough that even though I know it to be generally a bad idea, I allow for it in as un-opinionated a way as I can come up with. In the face of a proposal like this one, the only reason to continue forward with class-fields is because of the view that too much effort has already been put into it. I don't believe I'm the only one who sees things this way, but if class-fields becomes part of the language, the language itself will likely have been irreparably damaged.

@ljharb
Copy link

ljharb commented Dec 23, 2018

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.

This isn’t accurate; the same issues exist because the prototype might have a setter on its prototype chain.

@rdking
Copy link
Owner Author

rdking commented Dec 23, 2018

@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.

@rdking
Copy link
Owner Author

rdking commented Dec 23, 2018

Let me say that a different way:
It should not be possible for the initial state of a derived class to be overruled by the initial state of any of its bases.

@rdking
Copy link
Owner Author

rdking commented Dec 23, 2018

Something that would be nice (but wouldn't be accepted due to existing code) would be if all properties on a prototype constructed by class evaluation would be defined as non-configurable so that something like a decorator @override would be required in a derived class before it could declare a property with a name that already exists in the base. The implementation of @override would simply need to detach the existing __proto__ of the prototype object, define the property, then re-attach the __proto__. In this way, there would be no such thing as accidental overrides.

@mbrowne
Copy link

mbrowne commented Dec 23, 2018

Thanks for the clarifications.

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?

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. let/const) of this proposal at this point, especially not without the participation of more members of the committee...I doubt it would accomplish very much.

Now they have grown accustom to that, developed habits and patterns around that, and are looking for a way to simplify that way of doing things. I get it, but look at the cost. Attempting to advance this is helping some at the cost of hurting others.

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.

@rdking
Copy link
Owner Author

rdking commented Dec 23, 2018

@mbrowne

You could still easily accomplish your desired behavior with a combination of class fields and decorators, could you not?

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:

  1. it places a premium on prototype evasion.
  2. it uses an easily avoided, egregious syntax
  3. Even though something new is being created, it is being designed specifically so that it suffers from an issue that could easily be avoided. (referring to Proxy & internal slots)

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.

@mbrowne
Copy link

mbrowne commented Dec 24, 2018

I'm afraid not, at least, not without a forest of decorators, some of which could only be implemented in the engine itself.

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.

@rdking
Copy link
Owner Author

rdking commented Dec 24, 2018

@mbrowne

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.

@hax
Copy link

hax commented Dec 24, 2018

You could still easily accomplish your desired behavior with a combination of class fields and decorators

@mbrowne

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 @noFootgunField decorator (or let use @field for short) before all public fields.

class X {
  @field foo
  @field bar
}

It can solve the ASI hazard issue.

We can also make the @field using the semantic of [[Define]] property on prototype but [[Set]] for initializer, as @rbuckton proposed, which solve the [[Define]] vs [[Set]] issue mostly.

But is such solution "easily"? I don't think so.

  1. You need decorator, and it still stage 2 and not very stable -- for example, the feature of changing placement may be dropped as @littledan suggested, which would dismiss our @field solution. On the other side, we may dismiss @littledan's proposal using our @field as a valid and important use case, but it just make decorator proposal much complex and unnecessary powerful and risky. IMHO, every time we postpone the issues to decorators just make the situation worse and we may only find even decorators can not save our ass in the last minute (for example, current decorator-based protected solution may be broken on super call, and I also doubt the performance of it).
  2. You need linter to enforce the coding style, you need develop new linter rules and convince ESLint/TSLint community to accept the PRs.
  3. You need convince your teams to get consensus about this coding style
  4. Extra education cost
  5. Community may still have many divergences even they agree most of it, for example, some may choose also enforcing @field before private field for consistency (even it's unnecessary), some may dislike the name @field and use @own, @inst or @prop, and there may be many @prop decorators with different semantic (for example, @rdking may use @prop for prototype, but I may use @prop for accessors 🤣, and other may use @prop for own property)...

The truth is, such solution can never be "easy" and just transfer the cost from the committee to the community.

@rdking
Copy link
Owner Author

rdking commented Dec 24, 2018

@mbrowne
Adding on to what @hax just posted, the purposes of this proposal are to provide something that:

  1. has an easy-to-reason-about syntax for private and public data properties.
  2. has an easy-to-reason-about mental model.
  3. has no ASI hazards.
  4. does not implement new features at the risk of parting with the prototype-based design of the language.
  5. has little to no opinion on how a developer should write code.
  6. avoids all avoidable issues present in every competing proposal.
  7. does not create any new foot-guns unless absolutely necessary.
  8. does not break any reasonable assumptions, language usage patterns, or existing code.
  9. leaves ample room for future expansion.
  10. reasonably accounts for the features of both competing and non-competing existing proposals.

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants