tc39 / proposal-class-fields

Orthogonally-informed combination of public and private fields proposals
https://arai-a.github.io/ecma262-compare/?pr=1668
1.72k stars 113 forks source link

Do we still have a chance to stop private fields (with #)? #142

Closed SCLeoX closed 5 years ago

SCLeoX commented 5 years ago

It is clear to me that this "consensus" in the committee is against the majority of the community other than tc39.

I just want to know, what people like me, who really do not want this private fields thing (with #) to be added into JavaScript, can do to prevent it from happening.

hax commented 5 years ago

@mbrowne

what actual problems would be solved by dropping the public fields proposal aside from helping to enable a syntax for private instance variables that you prefer?

As I said again and again, this proposal choose #x because of the syntax duality of x = 1 vs #x = 1 and this.x vs this.#x. The duality also affect many decisions like both do not use keywords for declaration. If we drop public field, the syntax of private will never constrained by the duality. We could drop sigil, use keyword, consider shorthand syntax, etc.

slikts commented 5 years ago

… is the tool for OO while React actually use FP and hate OO …

This is veering off topic, but this perception is simply wrong; there's nothing inherent about classes that would prevent them from constructing pure objects, and closures themselves can be conteptualized as objects. The only issue there is the misunderstanding of classes or OO as being tied to shared mutable state.

rdking commented 5 years ago

@hax That's not quite right. The problem is because of the access notation. The logic in the FAQ regarding that is actually correct. As much as I like argue about this proposal, I cannot argue the reasoning for choosing #. It's the approach to private access that's wrong, not the access specifier.

No matter how it's done, the access to private data has to use a notation different from <obj> '.' <field> because that means public. Since the field in this proposal, even when it's private, is a property of the instance, they're deciding to replace '.' with '.#', and using # as a prefix for declarations instead of using a keyword solely to create a visual symmetry, ugly though it may be.

The problem with all of this is the "fields are properties of the instance" part. To keep consistency with the way ES works and not require trade-offs, they shouldn't be properties of the instance or its class at all. Making them non-properties eliminates the cause of the logic in the FAQ and allows for a cleaner syntax. It also allows for achieving the same features with no trade-offs.

... but developers, having been exposed to the bad idea behind React's implementation and the similar feature in TypeScript, seem to want fields regardless of what they have to give up to get it.

mbrowne commented 5 years ago

I was going to bring up the same important point that @slikts just made:

The only issue there is the misunderstanding of classes or OO as being tied to shared mutable state.

Just to be explicit about this, there is nothing preventing you from only writing classes that produce immutable objects. Yes this is veering off-topic but I agree it was worth at least briefly mentioning, since it's such a common misperception that OO and FP are completely incompatible.

Back to the main discussion, there's a more important point: public fields would be just as useful and driven by legitimate motivations even if React did not exist. @hax might be willing to accept the idea that even effectively public properties should still be required to have getters and setters, but most of us are not. There simply is no benefit of forcing the use of getters and setters (even if there's a shorthand syntax for creating them) for cases where you do want a public mutable property.

rdking commented 5 years ago

There simply is no benefit of forcing the use of getters and setters (even if there's a shorthand syntax for creating them) for cases where you do want a public mutable property.

Sadly, it was that kind of thinking that led to the SQL injection problem becoming such an issue a little over a decade ago. The point of using getters and setters to provide publicly mutable data is to ensure that the data is never malformed before the critical actual value (presumably stored in a private member) is overwritten and lost.

mbrowne commented 5 years ago

@rdking I'm very much in favor of getters and setters, I just think they're useless overhead in some cases. If you think a getter/setter pair like this is protecting you from something:

get() { return this.#x }
set(v) { this.#v = v }

...then you're kidding yourself.

The point of using getters and setters to provide publicly mutable data is to ensure that the data is never malformed before the critical actual value (presumably stored in a private member) is overwritten and lost.

You can still ensure this. If this need exists when you're first writing your code, then you would write a getter/setter in the first place. If you start with a public property and such a need arises later, you can easily refactor to use accessors at that time. And thankfully due to the nature of accessors in JS, you can do this without breaking any code that consumes your class.

rdking commented 5 years ago

@mbrowne Guess what. There's exactly nothing wrong with what you just said. I agree with it. Accessor properties are just a syntactic convenience over getter/setter functions. And yes, if you write default, unchecked accessors, then that's useless extra code. But as I've stated many times in many different threads, that's not where our fundamental disagreement is.

Let's be honest about something. On objects, public, mutable properties are pretty much all we have. We can make them non-mutable through various efforts, sure, but they're always public if they're part of an instance. The only way to make them private is to hide the object in a closure.

With the introduction of a native form of privacy, regardless of its form, there will be choice. With that choice (and ignoring the need for protected that will almost certainly raise it's head) the need for a class instance to have mutable public data properties will be vanishingly small. Most of the need for mutable public properties will actually come from the need for protected. If a viable protected solution were provided in parallel with private, the need to provide unchecked mutable public data properties would all but disappear.

Having said all of that, the main example constantly pointed out by "fields" proponents is the React state. But look at what's happening. React developers are trying to push "hooks" to replace the problematic state. Then there's react-redux for taking state control away from components and hiding it behind "actions". The truth is that this rare case of a public instance property needing to be initialized to an object is vanishing. The only other examples come from Babel and TypeScript. However, those are cross-compilers that compile back to normal ES with initializers in the constructor as expected.

There's just no semantic benefit to fields that justifies the trade-offs. On a case by case basis, the developer normally decides such things when writing. By forcing that choice into the language, it takes power away from the developer. In the end, the cases where it is useful to have an unchecked mutable public data property in the presence of private storage are vanishingly small. You'd call it an edge case. Judicious use of the prototype and constructor are more than sufficient to handle those cases. For all others, private data and either accessors or getter/setter pairs are the proper tools for the job. The only part currently missing from the language is the syntax for private storage.

ljharb commented 5 years ago

Only shipping private fields, but not public fields, would be forcing that choice into the language - by making private more ergonomic than public.

As far as I'm concerned, getter/setter pairs are almost never the proper tools for the job.

rdking commented 5 years ago

@ljharb This isn't the thread for me to go into everything that's wrong with private fields, but I did hint at some of it here in the 3rd paragraph.

hax commented 5 years ago

@slikts

This is veering off topic

Why an analysis of the validity of the main use case would be off topic?

but this perception is simply wrong; there's nothing inherent about classes that would prevent them from constructing pure objects

Yes you can use classes to construct a pure object, you even could never use this in the constructor, juse return pureObj! But we should understand such tolerance (with the weird semantic for return primitiveValue) is mainly for the compatibility of the old function as constructor behavior. We should never recommend such confusing usage in real world engineering.

The good practice is simple: use factory pattern if you only want pure objects, use classes only you need real OO features (for example, inheritance).

If you only need pure objects but use classes, it's ok, but you should never complain the concomitant such as this binding, mandatory super call in constructor, etc.

And in React case, it even not need pure objects. The essential requirements of a React "component", is a render function return a virtual dom, which will be called whenever the prop/states changed, and it also need some way to expose the React state/lifecycle management API.

That's what my factory pattern example satisfy, simply and directly.

On the other hand, React class-based component use base class (React.Component) to expose the APIs (state, setState, componentDidMount, etc.) in a clumsy way, force the component author distribute the component logic into the separate methods and properties and rely on this to cross-reference them, then suffer the this binding problem, and pay the burden of super() in constructor while the component authors have no idea why they need write that.

In OO paradigm, objects pass/recieve messages to/from other objects. This is why we need receiver (aka.this).

In React case, you never send messages to components from the outside! All React components are essentially renderers managed by React framework, only the framework can be notified (by setState), and decide whether and when to recall the renderers. So why renderers need a receiver? Bundle the renderers and states/handlers/effects to use same receiver just cause problems (see Motivation of React hooks). We should understand the this binding trouble is the surface symptom, never the cause of the disease!

And in OO paradigm, we have to call super() in constructor because it ensure the internal state and logic of the base classes are completed before the subclass can access them.

In React case, there is actually no real inheritance requirement, you extends React.Component just to use React APIs (setState) and expose the lifecycle callbacks. This is very like the early days usages in Java frameworks, because you have only classes in Java language that time. But modern frameworks move to annotations. This is also why Angular never suffer super() in constructor --- use decorators to connect to framework APIs instead of abuse subclassing.

Back to the early day of React (still pre-ES6 era, no classes in that time), when it was open sourced, some guys decide the API should be React.createClass. (The original API name in facebook internal code is not createClass, may be createComponent...) I remember there were some discussions that time, suggest registerComponent instead of createClass. If that way, the React team might be much wisely when decide whether adopt ES6 class...

Note, I'm not blaming React team, React is a very great and successful project. But everyone can made mistakes, the important part is we should be objective to check which is the root cause of the issues. And I believe the problems of React class-based component is not "missing public field" but "use classes in wrong way". Add "public field" can not really solve the issues of React, only redesign, like factory pattern, or React hooks, can lead to brighter future.

The only issue there is the misunderstanding of classes or OO as being tied to shared mutable state.

I agree exposing mutable state is bad. Actually it's generally bad in programming, not limited to classes or OO. But my previous analysis about React didn't relate to mutable state. Not sure how I can respond to your comment.

hax commented 5 years ago

@rdking

That's not quite right. The problem is because of the access notation... No matter how it's done, the access to private data has to use a notation different from <obj> '.' <field> because that means public.

What I mean is, if no public field, the syntax decision of private part could be much "free" even we can't use <obj>.<field>.

For example, the shorthand syntax for instance variable is very attractive --- just as let x(declaration) vs x(access) in the blocks, we can also use <keyword> x(declaration) vs x(access) in the classes. We may need this::x in rare cases, but most time we just use x, this is much better than this.#x as ergonomics.

PS. On the other side, the syntax of public field also affected by private field (this is what "duality" mean). Because #priv = x declaration have no keyword, you also don't want keyword in public field declaration, even keyword can easily solve [[Set]] vs [[Define]] issue.

hax commented 5 years ago

@mbrowne

but most of us are not.

Who is "us" here? πŸ€ͺ

There simply is no benefit of forcing the use of getters and setters (even if there's a shorthand syntax for creating them) for cases where you do want a public mutable property.

"Most of us" πŸ˜‰ agree exposing public mutable property considered harmful, so such cases are bad.

Even someone disagree such cases are bad, getter/setter is just wrapper, never disallow you to expose mutable property if you really want, and with shorthand syntax you will never notice the difference. So even there is "no benefit" in such cases, there is also "no harm" in such cases.

And it have huge benefit in all other cases.

Actually most other OO languages force the getter/setter wrapper. Some by convention (Java), some by syntax (Scala). Have no idea why JS can/must not.

Classes is the tool mainly for OO paradigm. So allow me to ask:

Why breaking OO good practise (simple syntax lure into exposing public field) could be GOOD for the programmers who want do OOP in JS?

Why enforcing OO good practise could be BAD for the JS programmers who may not familiar with OO good practise? (Note, we JS programmers don't use it much before, not because it's not important, just because without private field, such practise is impracticable.)

hax commented 5 years ago

@mbrowne

public fields would be just as useful and driven by legitimate motivations even if React did not exist

Yes it's possible. But I hope at least "some of us" could agree the current usage of public field in React class-based component is not a valid case/motivation for public field, so the whole requirements of public field is weaker if I could exclude React --- which is the biggest framework in JS land.

Then we can check other cases/motivations. (First we need documents to explain them!)

hax commented 5 years ago

@ljharb

Only shipping private fields, but not public fields, would be forcing that choice into the language - by making private more ergonomic than public.

If you are doing OOP, make private more ergonomic than public is just a very GOOD thing. Even we consider other paradigms like FP, the information hiding (encapsulation) is generally a GOOD thing.

Of coz, there are many reasonable use cases for plain objects which only need public properties. I think we should use factory for them. Classes is the wrong tool for such cases. We can't blame the users choose the wrong tool, but we should never land a new feature to encourage the wrong usage! If we really think the wrong usage mean important use case, we should develop a separate feature for it (just like we introduce arrow functions to free old function from callbacks, introduce classes to free old function from constructors), for example, we may introduce non-inheritable, no private, struct {} for such cases (just an idea, not means I really advocate it.)

As far as I'm concerned, getter/setter pairs are almost never the proper tools for the job.

Not sure what "job" your are talking about. If you are talking about the plain object cases, I think classes are never the proper tools for such job, so we can't blame getter/setter. If you are talking about the cases which you really need to expose a internal state of an instance, getter/setter is the most proper tools for the job.

Of coz you could use getXXX/setXXX methods in such cases, unfortunately in a language already have getter/setter, such pattern just cause divergence and no real benefit.

Insisting on public own property instead of getter/setter also have no real benefit. Actually, the semantic of data properties and accessors are carefully aligned when standardizing ES5 --- you can't write/create a data property even the property is defined on the prototype if it's writable: false, just like there is only getter on the prototype.

Note, I never say getter/setter is silver bullet. If exposing a public property is bad in concrete cases, exposing it by getter/setter is equally bad, vice verse.

The reason why I suggest getter/setter instead of public field, is it follow the ES6 classes semantic -- all definitions are on prototype! The ES6 classes is designed as such for the good reasons, I hope the champions of current proposal could revisit the design notes of the old but solid ES6 classes and learn something.

There is a chance that we can define something on each instance instead of prototype, if it is private --- which never conflict between base/subclass. Besides that, there is no hope. Because redefining own property in subclasses is just weird and could break the invariants/constraints of base class. (Java/C# allow public field because subclass can never redefine the field of base class, they are always individual fields even they have same name.) That's why getter/setter wrapping private state could work, even you redefine getter/setter in subclasses, the invariants/constraints of base class could be retain if the base class doesn't rely on the wrappers internally (i.e. always access the private state directly).

mbrowne commented 5 years ago

The reason why I suggest getter/setter instead of public field, is it follow the ES6 classes semantic -- all definitions are on prototype!

This is a mischaracterization of the intent behind ES6 classes (at least as far as I can gather; I wasn't involved in the original discussions about classes). Methods and accessors are the only types of declarations that are available in classes so far (due to the max-min design approach); just because they go on the prototype doesn't mean that there can't be new kinds of declarations that affect the instance. Here's a visual illustration:

https://github.com/rdking/proposal-class-members/issues/3#issuecomment-438524232

mbrowne commented 5 years ago

If you are doing OOP, make private more ergonomic than public is just a very GOOD thing.

FWIW, I strongly agree with this, and if we were designing a new language I would push strongly for making private the default. I just think it's a very bad idea to try to retroactively apply this philosophy to a language like JS where public has always been the default. Imagine that we were introducing type annotations at the same time as class fields. Would you think it's a good idea to require everyone who uses fields to use type annotations instead of optional type annotations? ...And then say if you don't want to use a type annotation, just create them the old way? I realize this is a flawed analogy but I think that adding private without also adding public declarations would be just as big of a break with the established semantics of the language.

hax commented 5 years ago

@mbrowne You could ask the guys who involved the original discussions, like AWB or BE (both against current proposal πŸ˜‚) whether my interpretation is correct. The max-min design is not inborn, actually the definitions of own property must have been discussed in last two decade (starting from ES4 -- which have more Java-like class and real public/private/protected fields), you could try search "data property" or "instance property" in esdicuss mailing list.

hax commented 5 years ago

Here's a visual illustration:

rdking/proposal-class-members#3 (comment)

I don't think I'm on the right green side. 😝

In the old article Prototypes as classes – an introduction to JavaScript inheritance, Axel Rauschmayer wrote:

Subclassing: A new constructor D extends an existing constructor C in two steps: Inherit prototype properties: Let D.prototype have the prototype C.prototype. Inherit instance properties: Let constructor D call constructor C as a function (no new operator!) and hand in this so that C adds its properties to the new instance of D (without creating a new instance itself).

Note: in ES6 classes, "Let constructor D call constructor C as a function (no new operator!)..." is just super(...args).

Alex obviously see the classes in "right (blue)" side, but the title of the article is still "Prototypes as classes".


As my last comment said, there were discussions of making initialization of data property more declarative before, at least one suggested syntax is class X { a: 1; b: 2 }, dataProp: initialization syntax is chosen because it's same as object literal (YES, object literal use [[Define]] semantic instead of [[Set]]!), it's just same as current syntax except : vs =.

But such syntax/semantic has been dropped, and the committee have the conclusion that definition ([[Define]] semantic) of data property is footgun in that time. (I can't find the original meeting notes, hope someone could find it.)

[[Define]] vs [[Set]] difference is subtle, for example {...o, x: 1} won't trigger setter if there is setter of x on o, but Object.assign(o, {x: 1}) will. If we really want to move something from the constructor outside, we should stick on [[Set]] semantic, because no real world source code use Object.defineOwnProperty in constructors. And another reason I already pointed out, that [[Define]] own prop just ruin the prototype inheritance because it will shadow all accessors on both base classes and subclasses.

The original public field proposal use [[Set]] semantic, so use = instead of : is logical, and it's actually not "public field proposal" but "data property initializer proposal".

Even [[Set]] is much better than [[Define]], it still have many issues:

  1. x = 1 syntax just cause divergence of property syntax. A common typo is forgetting this. for this.x, and x = 1 declaration just amplify it in a large degree. My class { name='hax'; greeting='hello ' + name} is a good example. You could show this code to the average programmers and check how many people could find the problem in first place. Note you may refactor public field with initializations in constructors frequently, it's easy to forget add/remove this. for any place. IDE/linters may help, but I don't think must-use-with-IDE/tools design is acceptable.
  2. [x] = [y] syntax is very unfortunate that same statement have totally different semantics in class body with other places. Unbelievable!!
  3. class A extends B {x = initcode; constructor() { somecode; super(); othercode; }} -- initcode is actually executed after super() and before othercode, this is very magical and not easy to get and remember for average programmers, and make the debugging much harder.
  4. [[Set]] semantic make decorator much complex even decorators already complex enough! 🀣
  5. ...Don't remember, but previous points already enough for me...

Point 1,2,3 always there whichever you choose [[Set]] or [[Define]], and the authors of this proposal decide to ignore them anyway. And I sincerely hope you (and @rdking ) can find the solution of them.

hax commented 5 years ago

@mbrowne

I think that adding private without also adding public declarations would be just as big of a break with the established semantics of the language.

I truly understand why many want public declarations for data property. I get my conclusion not because I want make those who want public declarations unhappy, just because I realize the cost of adding it is unaffordable. Dropping public field could also make the language follow the good practice of OO. But you can treat it as a individual factor. Even without consider OO, current syntax/semantic of public field is too terrible.

rdking commented 5 years ago

I've said it once before, and I'll say it till I'm blue in the face: If all of the logical conclusions to a hypothesis lead to a contradiction or unviable outcome, then the hypothesis itself is wrong. Therefore, the concept of "fields" is itself wrong in ES. The attempt to do things as they are done in other languages won't work properly ever simply because the hidden instance template in other languages is an accessible, 1st class object in ES. As long as that is the case, "fields" lead to contradictions. If it's part of the class, it's on the prototype.

mbrowne commented 5 years ago

there were discussions of making initialization of data property more declarative before, at least one suggested syntax is class X { a: 1; b: 2 }

As someone pointed out recently, that syntax would break TypeScript and Flow, and prevent the possible future addition (however unlikely) of native type annotations. However, := syntax could work.

Of coz, there are many reasonable use cases for plain objects which only need public properties. I think we should use factory for them. Classes is the wrong tool for such cases.

This is just your opinion. Even without any libraries or frameworks, there are legitimate use cases for public instance properties. For example (imagine that we have a @readonly decorator that works the same as readonly in TypeScript):

class Person {
  // Suppose this decorator works the same as `readonly` in TypeScript
  @readonly id
  firstName
  lastName

  constructor(attributes) {
    Object.assign(this, attributes)
  }

  get fullName() {
    return this.firstName + ' ' + this.lastName
  }
}

class User extends Person {
  username
  password
}

I think this is a pretty classic use case for classes and inheritance. (And the readonly decorator is just to demonstrate that only properties that should be mutable are actually publicly writable; this example could of course be expanded to include private fields and accessors that use them.)

mbrowne commented 5 years ago

@hax

Point 1,2,3 always there whichever you choose [[Set]] or [[Define]], and the authors of this proposal decide to ignore them anyway. And I sincerely hope you (and @rdking ) can find the solution of them.

@rdking is exploring possible solutions in his class members proposal and yes, I am participating in those discussions, but it's not because I don't think this proposal is viable. In fact even with Define semantics which I strongly disagree with, I still think this proposal is a much better overall solution for class properties and private state than any other alternatives.

BTW, your strategy of writing long comments in every active thread here within a short period of time is a little overwhelming and repetitive and probably not very effective...as I just pointed out in the other thread I think most of the committee members have already tuned out. If I shared your opinions I would probably be tempted to do the same thing but I don't think it's having much impact. Maybe I should stop writing long responses too (at least for a while) because I'm sure plenty of people have tuned me out too.

hax commented 5 years ago

@mbrowne

Whether : or := is not important, the [[Define]] semantic on own property is just bad for class if you consider inheritance.

The @readonly case just prove that define own prop is bad --- I can just write class User extends Person { @readwrite id } to redefine it and break the constraint of base class.

On the other hand, as classes 1.1 proposal, with the almost same code (except missing keyword), id is instance variable (aka. private), and @readonly is a decorator to generate only getter. Then the subclasses can never redefine id to break the constraint. Even subclasses can redefine the getter/setter, it never can access id of base class.

I also notice you write get fullName() { return firstName + ' ' + lastName }.

You forget this. πŸ˜‰

You have been warned (https://github.com/tc39/proposal-class-fields/issues/142#issuecomment-439955360)

So such method can only be valid if you use classes 1.1 😜 Actually it's a little evidence that classes 1.1 shorthand syntax is just the most natural syntax.

(Classes 1.1 ask for keyword, it's easy to find and add all missing keyword automatically, but it's very hard to find whether there is a missing of this.)

hax commented 5 years ago

BTW, your strategy of writing long comments ...

I have no strategy at all. I already know I'm wasting my time here. πŸ˜‚

All problems are obvious, and the champions just choose ignore them. The only strategy I will take is tell the story to community though I was very unwilling do that as I have explained. But it seems we have no other choice. I will give a speech about it in the tech conferences in China and let's see what will happen.

rdking commented 5 years ago

@hax It's worse than that. They're not ignoring them. They consider those things acceptable losses or comparatively insignificant problems.

mbrowne commented 5 years ago

@hax

The @readonly case just prove that define own prop is bad --- I can just write class User extends Person { @readwrite id } to redefine it and break the constraint of base class.

This is just a side issue. It could be remedied by a future proposal offering a native readonly feature that can't be overridden by a subclass.

bakkot commented 5 years ago

I would assume any readonly field decorator would also imply non-configurable, in which case a subclass could not in fact overwrite it, so long as fields are placed on instances.

rdking commented 5 years ago

@bakkot Why that assumption? That would be like saying property defined with writable: false also automatically gets configurable: false, which is demonstrably not true.

ljharb commented 5 years ago

The words β€œread only” mean β€œit can only be read” - it wouldn’t make any sense to be able to write to a readonly thing.

hax commented 5 years ago

Of coz, you can make @readonly decorator with configurable: false. But what about other decorators?

For example @observed. Subclasses could intentionally or accidentally redefine it. Shall we also make it configurable: false?

Basically almost all decorators have the same problem.

And configurable: false cause problems. For example, reactive framework like Vue use getter/setter to track the dependencies. You can tell the users why @readonly or any arbitrary decorator would break Vue?

Instead configurable: false-like passive response, I hope you could understand the root cause --- the own property is shared by the whole class hierarchy, which means whenever there is another [[Define]] in a subclass, it will potentially break the invariant and constraint of the one on baseclass.

On the other side, private state never shared, getter/setter on prototype never shared. So they could keep the invariants and constraints of all definitions.

So it's not the problem of decorators, but the problem of the public field ([[Define]] own prop).

And you insist on public field, then the problem just spread to decorators, make all authors/users of decorators suffer.

mbrowne commented 5 years ago

I agree that [[Set]] would simply work better overall, including for decorators even if the spec would be a little more complex. Vue is an interesting case...did you see that the author of Vue has a strong preference for [[Define]]?

https://github.com/tc39/proposal-class-fields/issues/151#issuecomment-431869393 (And earlier comment, https://github.com/tc39/proposal-class-fields/issues/151#issuecomment-431130535)

hax commented 5 years ago

did you see that the author of Vue has a strong preference for [[Define]]?

Yes. Actually we discussed it privately several times. I think he prefer [[Define]] with good reasons, for example, decorator usage (you can try to convince him [[Set]] is also workable for decorators 😁), and it should be [[Define]] because all class elements in class body are definitions. (I have no position to against this πŸ˜† ) About the dark side of [[Define]], he understand it but think such problems are inevitable (with the prerequisite that we must have public field).

So , you see the real problem, it's a dilemma, you have to choose one, [[Define]] or [[Set]]. You prefer [[Set]] not because [[Set]] is a perfect solution, but just because you believe [[Define]] is too bad. He prefer [[Define]] not because [[Define]] is a perfect solution, but just because he believe [[Set]] is unreasonable.

About me, half of I support you, this is why I object to [[Define]]. Another half of I support Evan You, this is why I object to [[Set]].

The truth is, it's a mistake that we are forced to face a dilemma. Let's drop public field (own prop definition), only use [[Define]] on private and prototype, then everything is ok.

ljharb commented 5 years ago

Then the much larger group of people who want public fields will object. Dropping a feature solely because an imperfect choice has to be made would doom most additions to the language.

rdking commented 5 years ago

The main reason the choice is imperfect is because it is a decision that is supposed to be made on a case-by-case basis as the developer codes. Having it made by the language is what presents the problem. There are only 3 possible solutions to this:

  1. Go ahead and break something (pick either [[Set]] or [[Define]])
  2. Use what we already have (drop public fields)
  3. Split the definition and still leave the choice to the developer

For me (and probably many others), 1 is a non-starter. I'd much prefer 2 as I still can't see the benefit of this new syntax. But I would be willing to accept 3 without further fuss. It would require either creating a new operator to handle the define case, or putting a keyword in the definition somehow. 3 is probably the best compromise there is.

ljharb commented 5 years ago

I suggested exactly that over a year ago, and the committee chose to go with the current proposal. What we already have isn’t good enough - moving forward is the better option.

hax commented 5 years ago

@ljharb

Then the much larger group of people who want public fields will object. Dropping a feature solely because an imperfect choice has to be made would doom most additions to the language.

Does the "larger group of people" really want public fields? Or just something which satisfy their use case like React usage of public field? As I chattered many times, syntax sugar of wrapping private to getter/setter just work for them, the only difference is x = 1 to <keyword> x = 1.

I know you don't like getter/setter, but could we be more practical? Is there any important use case which such sugar can't satisfy but current public field could satisfy?

Actually, such simple solution just solve most issues we worry about, no need # anymore, no ambiguity of class { x = init; y = x + 1 } and class { [x] = [y] }, much simple and consistent semantic (just use [[Define]]) and simplify decorators world a lot. (decorators have been complex enough!)

Of coz there may be something we still not very satisfy, like need of :: in some cases, like branding check. But do these trade-off worse or more controversial than current trade-offs? I don't think so. And we still have possibility to solve them! (On the other way, current proposal has been discussed several years and it's hard to believe it could be better.)

I would very like to ask, what is the most practical issue you think classes 1.1 based solution have? If we could solve your concern, would you (personally, let's temporarily pretend there is no TC39 πŸ˜‚) like to give it a chance?

hax commented 5 years ago

@ljharb

moving forward is the better option

Is there any possible exit of such conclusion? For example, a much larger negative community feedback??

ljharb commented 5 years ago

Is there any important use case which such sugar can't satisfy but current public field could satisfy?

The use case is "creating own data properties". Getters/setters on the prototype don't feed into Object.values/keys/entries/assign , and as such, imo they are simply not an option, unrelated to my personal distaste for accessors. The need is for something a) declarative in the class body, b) per-instance, and c) own enumerable data properties. Public fields in the current proposal provide that. Classes 1.1 does not provide public fields in any form, and in fact would preclude them ever being added - as such, I think it does not warrant even as much exploration as it's already been given.

hax commented 5 years ago

@ljharb Could you give some more concrete use cases instead of "we need creating own data prop by definition on class body". It's not use case, it's the design (which we already know the one of root cause of all problems).

ljharb commented 5 years ago

@hax my use case is to be able to omit the constructor, yet still define enumerable own data properties that show up in the typical object reflection methods (most all of which only look at own properties, and most of those only look at enumerables). If I would be happy with an accessor, i'd manually do #foo = initial; get foo() { return this.#foo; } set foo(v) { this.#foo = v; }, but that wouldn't meet my needs. I'm not sure what more explanation you need, since "works with Object keys/value/entries/assign" is a pretty reasonable requirement on its face.

rdking commented 5 years ago

@hax This is the first time @ljharb has actually given the full use case.

@ljharb Correct me if I'm wrong. The problem you have with prototype-based class properties is that you cannot get a complete list of the enumerable set without walking the prototype tree. Does this sound right? It looks like we all want a and b, and your desire for c is because prototypes don't easily give a list of every enumerable property on an object.

My question to you: What if someone proposed something like Object.allKeys that, when given an object parameter, returns all enumerable public properties available to the object. Combine this with a solution that prevents the object-on-prototype foot-gun by allowing copy-on-write semantics for object properties of prototype objects. Would this eliminate the want for avoiding the prototype?

ljharb commented 5 years ago

@rdking my use case is for interoperability with the methods used by the existing JS ecosystem, which can't be changed without a time machine. So no, there is no chance the need to avoid the prototype can be eliminated for me.

rdking commented 5 years ago

So even if a parallel set of methods that were prototype-inclusive were provided, they wouldn't be able to provide an adequate substitute? Is it because you're interfacing with 3rd party code that is unlikely to be modified?

mbrowne commented 5 years ago

I think the use case @ljharb brought up (which is an important one) could still be achieved if properties were added to the prototype (not saying that's a good idea) as long as they also exist as enumerable properties on the instance (which would of course just override the ones on the prototype). There's a bigger issue which is that getters and setters are non-enumerable by default. In addition to it being needlessly verbose to write getters and setters in the first place for this case, it certainly wouldn't be acceptable to have to use a decorator every time to make it enumerable...so much complexity just for a public enumerable property?

mbrowne commented 5 years ago

So even if a parallel set of methods that were prototype-inclusive were provided, they wouldn't be able to provide an adequate substitute?

Double standard...you've frequently talked about the importance of maintaining established conventions of the language, and yet here we are...

mbrowne commented 5 years ago

@ljharb

Classes 1.1 does not provide public fields in any form, and in fact would preclude them ever being added

How so? Just because of the author's opposition to public fields as stated in the docs? Although @zenparsing is against fields it seems technically very possible to add public fields as they exist in this class fields proposal to the classes 1.1 proposal (as @rdking already admitted). My main opposition to that is the duality between public and private declarations it would introduce and the resulting ergonomic issues. But it does seem that it would meet the same fundamental requirements as the current proposal.

hax commented 5 years ago

@mbrowne Actually the usage of own properties in classes is not "convention" but the "inevitable" because we never have private before, the difference is subtle but important.

@ljharb Object.keys() or similar APIs is designed for plain objects, and never automatically adapt to classes as whole due to the essential part of prototype. The only reasonable usage of such APIs in classes is to reflect the data properties.

With private available, the data properties could be converted to private states and use getter/setter (whatever wrote manually or generated automatically by some shorthand syntax). So the real problem is current private fields never provide the reflection mechanism like Object.keys/values/entries. This issue is also raised by objectors several times. I think we should and can solve that issue.

I would like to ask @ljharb , if we can add some reflection mechanism for private, does that solve your use cases?

mbrowne commented 5 years ago

@hax Classes already work quite well for this purpose and wanting a more declarative way to define public own properties is entirely reasonable. Consider:

class Base {
  constructor() {
    this.x = 1
  }
}

class Sub extends Base {
  constructor() {
    super()
    this.x = 2
  }
}

const s = new Sub()
Object.keys(s)  // ["x"]

I'm not sure what use cases you're referring to where Object.keys etc. would break due to the use of inheritance since those methods are usually used to enumerate data properties and not methods. But even if there are such use cases, it's not a strong argument, because there are still valid use cases like the one above as well as classes that don't use inheritance.

ljharb commented 5 years ago

@hax no, a reflection mechanism for private would go against my use cases, as it's not actually private if it is in any way observable from outside the class - but we're talking about public. What Object.keys and friends were designed for is irrelevant - what I'm talking about is what they're used for.

hax commented 5 years ago

PS. I still want to see use case in real "concrete form", aka. the code sample. For example, I never see Object.keys() usage in React class-based component, it's hard for me to understand, why handler = () => { this.setState(...) } is a data property which you want to reflect. πŸ˜‚