Skip to content
This repository has been archived by the owner on Jan 25, 2022. It is now read-only.

A new proposal, maybe late maybe not. #264

Open
aimingoo opened this issue Aug 21, 2019 · 55 comments
Open

A new proposal, maybe late maybe not. #264

aimingoo opened this issue Aug 21, 2019 · 55 comments

Comments

@aimingoo
Copy link

aimingoo commented Aug 21, 2019

Stop the class-fields proposal! strong recommend!

There is now a new proposal, no prefix '#', no FIELD, no newly concepts! please rate it.

Maybe we still have time to stop this disaster. say no accept! say no for class-fields proposal! see here #100 !

History of new proposal named private-property:

[2019.08.22]

[2019.08.29]

Thanks all.

@aimingoo
Copy link
Author

aimingoo commented Aug 21, 2019

The new syntax note:

class MyClass {
  private x = 100;
  foo() {
    console.log(x); // 100
  }
}

for protected property:

class MyClass {
  protected x = 200;
  foo() {
    console.log(x); // 200
  }
}

class MyClassEx extends MyClass {
  bar() {
    console.log(x); // 100
  }
}

for internal access:

class MyClass {
  internal private x = new Object;

  compare(b) {
    console.log(x === b[internal.x]); // true
  }

  static compare(a, b) {
    console.log(a[internal.x] === b[internal.x]); // true
  }
}

@jhpratt
Copy link

jhpratt commented Aug 21, 2019

What if I want a local of the same name?

@bakkot
Copy link
Contributor

bakkot commented Aug 21, 2019

This has been proposed and rejected several times. Please see the FAQ.

@aimingoo
Copy link
Author

@bakkot The new proposal have not that problem, reason for rejecting "private" is not sufficient!

@aimingoo
Copy link
Author

aimingoo commented Aug 21, 2019

@jhpratt
In current context, you know any name for class context, so next case is excessive or meaningless:

class f() {
  private x;
  foo(x) {
    // no! you know have a private 'x', why named arguments-x ?
  }
}

so, Need to be careful about protected property inhertied from parent. ex:

class f() {
  protected x;
}
class f2 extends f {
  foo(x) {
     // i unknow has 'protected x', or know it but can not change that!
  }
}

Easy!

To define alias with private as syntax:

class f3 extends f {
  private x as xxx;
  foo(x) {
    console.log(x);  // arguments x
    console.log(xxx); // protected x
  }
}

// more
class f4 extends f3 {
  foo() {
    console.log(x); // protected x from f, or f3
  }
}

@jhpratt
Copy link

jhpratt commented Aug 21, 2019

Nothing has ever prevented someone from creating a local with a valid identifier as a name. Why should that change?

@aimingoo
Copy link
Author

aimingoo commented Aug 21, 2019

@jhpratt

Have a private scope in context of class definitions, methods is sub-level block in the class context. when parent has 'x', sub-level block choice either override and new-name. same of:

function MyClass() {
   let x = 100;
   function MyMethod() {
      let x = 100; // override up-level
      let xx = 1000; // new name
   }
}

in MyClass private scope, all identifier is valid and known, MyMethod is free either override and new-name.

@jhpratt
Copy link

jhpratt commented Aug 21, 2019

Perhaps I should be clearer. If I can create a local binding (which I think you're saying you can), how would I then access the private member? That's an absolute must for any proposal.

Please provide a more thorough example and explanation.

@aimingoo
Copy link
Author

@jhpratt

Maybe, you need a newly name for exist private name:

class MyClass {
  private x = 100;
  ...
 
  private x as internal_own_x;
  foo(x) {
    console.log(x);
    console.log(internal_own_x);
  }  }

  // OR
  private get internal_own_x() { return x };
  foo(x) {
    console.log(x);
    console.log(internal_own_x);
  }

@aimingoo
Copy link
Author

aimingoo commented Aug 21, 2019

@jhpratt
hahaha, i know you will 👎

give a good idea to me? if dup identifier in scope or context, we can do what?

@jhpratt
Copy link

jhpratt commented Aug 21, 2019

I gave you a -1 because I don't believe you have fully thought this through, and that your proposal is not feasible. Your solution to my concern is hacked together. As I said in my previous comment, please provide a more thorough example and explanation.

This is a venue for serious discussion. If you're not willing to take my concern seriously, and are looking to complain about receiving a "thumbs down", I suggest you reconsider what you're here for.

@aimingoo
Copy link
Author

It's price for choice of identifier or class's fields. for identifier, impact is absolute exist in context of the class's definitions, but concept simple, and easy, and short code, these are good side.

@aimingoo
Copy link
Author

@jhpratt

no, I am discussing this issue seriously and formally. i accept any questions and things.

so, try testcases? I implement parser and interpreter base prepack and babel-parser. For syntax, I tried combined all case of private/protected/public. but, dup definitions is bound in one context. this is choice, not design.

@ljharb
Copy link
Member

ljharb commented Aug 21, 2019

@aimingoo how can i access the private data of another object with your proposal? One common example is static compare(a, b) { return a.#hashCode === b.#hashCode; }.

@bakkot
Copy link
Contributor

bakkot commented Aug 21, 2019

The new proposal have not that problem

Yes, it does. That FAQ entry addresses all proposals which use private x to declare fields and which do not use this.x to access them. Yours is such a proposal.

@aimingoo
Copy link
Author

@ljharb

Thanks. a big problem, key issue. good! 👍

Have some design principles of visibility of the solution:

  • not possible to access a private member of an object when his Class unaware. so,

  • not possible to directly access private scope outside of the Class declaration.

So we can only read them using protected or public method. ex:

class MyClass {
  private x;
  get x() {
    return x;
  }
  set x(v) {
    x = v; 
  }
}

Or using simple `public as` syntax to automatically add those access methods:

```javascript
class MyClass {
  private x;
  public as x;  // same above

Based on these, have two ways for your question.

Case 1

// class design based

class InternalMyClass {
  protected hashCode;

  static compare(a, b) {
    let getter = MyClassHelper.prototype.getHashCode;
    return getter.call(a) === getter.call(b);
  }
}

// access protected scope in child-class
class MyClassHelper extends InternalMyClass {
  getHashCode() {  // this.getHashCode()
    return hashCode;  // return this.#hashCode
  }
}

// publish the class only
export class MyClass extends InternalMyClass {
  private as hashCode; // update or not
}

NOTE: can access its private data by instance only. have not other way!

Have not public-interface to access private data of a/b, unless you design method to access them in class. so, need make a helper class to provide a interface for static method in above example.

And, MyClassHelper and MyClass are inherited from InternalMyClass, can access protected scope with same one inherited private-key.

Case 2

// or hijack skill

const ACCESS_HASHCODE = Symbol(); // any name or symbol
let internal_getter;
class MyClass {
   private hashCode = 100;

   [ACCESS_HASHCODE]() {  // public at MyClass.prototype
     return hashCode;
   }

   static compare(a, b) {
     return internal_getter.call(a) === internal_getter.call(b);
   }
}

internal_getter = MyClass.prototype[ACCESS_HASHCODE];
delete MyClass.prototype[ACCESS_HASHCODE];

NOTE: The public method send this to private scope, and we will got it in scope-chain when identifier resolving.

Okay, pls try test cases at here:

https://github.com/aimingoo/prepack-core/blob/proposal-private-property/test/private-property/private-access-in-class-methods.js

and here:

https://github.com/aimingoo/prepack-core/blob/proposal-protected-property/test/protected-property/classes-access-private-scope.js

You can checkout these branch and run test. @here

@jhpratt
Copy link

jhpratt commented Aug 21, 2019

You didn't really answer his question. How could you do static compare(a, b) { return a.#hashCode === b.#hashCode; } without leaking it via a mandatory public field? A simple code block would suffice to explain that.

@rdking
Copy link

rdking commented Aug 22, 2019

@aimingoo You're not going to get anywhere with this as along as:

  1. class members cannot directly and concurrently access the private properties of multiple instances of that class.
  2. developers don't have the freedom to add public properties of any name to the class, regardless of whether or not a private property exists with the same name.

The first one is absolute. There's no getting around it. For the second, there's a work around. It's perfectly fine for the lexical class definition to disallow duplicate property names. However, that needs to be a purely lexical limitation. Beyond the declaration, any member or caller needs to be able to add public properties to the class without restriction. Of course, as soon as you do that, you introduce the need to differentiate between private and public property accesss. Hence the various approaches that have been tossed around (.#, #./#[], ::, ->, private(obj), etc).

Dealing with that is what puts proposals against the artificial "private x implies this.x" constraint mentioned by the FAQ.

@aimingoo
Copy link
Author

aimingoo commented Aug 22, 2019

@jhpratt @rdking

Thanks, We are discussing a key issue, so please allow me to talk a little more.
 
Have problems at three side in for proposal class-fields:

  • for concept: what fields? property or not?
  • for implement: map or lex scope, or more... high cost and weight.
  • for syntax: this.#x is ugly.

I have discussed these before, such as #148 . but We need a solution to these problems, specific.

Now, my proposal class-property provides a solution to problems in two areas, but not all three. Really, "access private members of instances in its invalid area" is incomplete. But this is only at the syntax/grammatical level, I will explain this below.

The class-property have a core concept: private property is property in private scope, not fields, no conceptual conflict.

For implement, class-property request a reference to access private scope in a method. the private access trigger by method only, but the reference is simple. ex: {base: env_current_method_context, name: the_keyname, thisValue: the_instance}.

And base these, class-property proposal deliver a framework/scene, to resolving third problem. so that's incomplete syntax solution using identifier to access. Currently, the class-property have not newly or better syntax to replace .# in next cases:

class MyClass {
   compare(b) {
     return hashCode === b.#hashCode;
   }

   static compare(a, b) {
     return a.#hashCode === b.#hashCode;
   }
}

But, internal_getter.call(a) provides a basis or evidence for a grammar discussion at above. It explains that there is such a grammatical evolution as follows:

x_internal_getter.call(b)
  -> private(b).x
  -> (private b).x
  -> #b.x // enter private of b first, next got x
  -> b#x  // enter private of b, and access x

For syntax b.#x or b#x or other, need include two semantics, the enter and/next to access. In any case, the syntax return that reference {base, name, theValue} will enough in solution framework by class-property proposal.

I hope this proposal has the ability to end discussions on concepts and implementation levels, and recommend private member syntax like private x = .... But it does not include a syntax such as .# to access object instances other than <this> in method context.

NOTE: That (private b).x may be good. maybe... ^^.

@ljharb
Copy link
Member

ljharb commented Aug 22, 2019

Fields aren’t a conceptual conflict - it’s a new concept. Properties that aren’t public is also not a concept that exists. Inventing one or the other is the same thing - adding a new concept to the language.

“ugly” is subjective, and not everyone agrees with this, so it’s probably best to remove this consideration entirely unless comparing two semantically identical forms (which isn’t the case here).

Any proposal that does not allow for the static compare method i mentioned earlier is a nonstarter for me, and i suspect for other committee members too.

@rdking
Copy link

rdking commented Aug 23, 2019

Fields aren’t a conceptual conflict - it’s a new concept.

Indeed, fields are a new concept, but that concept is in conflict with the existing concepts of ES that carry a similar purpose. Ignoring private fields for a moment, public fields are instance properties provided by the class. However, ES is a language for which such "provided properties" are meant to be delivered by a prototype; hence a "prototype-oriented language". Whether you accept it or not, that's a conflict.

What's worse is that this very conflict becomes even more evident in the "trade-offs" that had to be made to arrive at the class-fields proposal, and the further trade-offs that will have to be made by both developers who wish to use fields, and developers who use libraries containing classes with fields. So yes, there are indeed conceptual conflicts between the "new concept" of fields and the existing concepts in ES.

“ugly” is subjective, and not everyone agrees with this

...but that's not the reason we shouldn't bother with this argument. In support of the "ugly" claim, I've yet to find a JS developer who didn't have an immediate negative reaction to the aesthetic of this syntax. While there are those who won't have that reaction on first blush, I'd wager they're in the minority. The reason we shouldn't bother with that argument is that, within the limitations of the approach taken, # was indeed the only rational choice. The problem isn't with the choice made, but with the artificial limitations that forced it.

Any proposal that does not allow for the static compare method i mentioned earlier is a nonstarter for me, and i suspect for other committee members too.

..and even those of us who do not wish to see class-fields reach stage 4. Concurrent access to the private data of multiple instances is an absolute necessity.

@ljharb
Copy link
Member

ljharb commented Aug 23, 2019

However, ES is a language for which such "provided properties" are meant to be delivered by a prototype; hence a "prototype-oriented language". Whether you accept it or not, that's a conflict.

As we've discussed many times, this is objectively false; "own properties" exist; and many paradigms exist that don't use class or prototypes at all. Properties are meant to be delivered by any means necessary, which does not require a prototype whatsoever (but surely includes prototypes as one of many mechanisms). The conflicts you're referring to simply don't exist.

@aimingoo
Copy link
Author

The conflicts you're referring to simply don't exist.

@ljharb

Conceptual conflict is absolutely existing.

The Object is defined as "object is collection of properties" in ECMAScript. So if Field is not a property, it must not belong to the concept set of "Object's member (collection elements of object)"; if Filed is property, then public field must be equal to property, there is a conceptual conflict.

This is the root of all existing contradictions.

NOTE: "an object is a collection of zero or more properties." ECMAScript Overview part.

@ljharb
Copy link
Member

ljharb commented Aug 24, 2019

@aimingoo and exotic objects also exist, and objects can have internal slots - they’ve never just been a collection of properties. Class fields merely expands their definition; there’s no conflict.

@rdking
Copy link

rdking commented Aug 26, 2019

"own properties" exist; and many paradigms exist that don't use class or prototypes at all.

... and we don't disagree here. Never have. However, if we ignore the fact that in ES we can construct a class that is an instance of itself, a class and an instance are 2 different things. A class is a factory that produces instances via a "template", and initializes the instance via a "constructor". Since the inception of ES, the means of sharing pre-defined properties and behavior has been to create an object containing those properties and behavior, and use it as a prototype for all subsequent structures being created. That's a class, with the template being the prototype object.

There is one other long-standing approach: object factories. This approach re-creates each property and method new so that, unlike with a class, it cannot be said that 2 different instances created from a factory are of the same type. Even V8 would internally treat such instances as being of different types.

So let me throw another log on the fire and say that this proposal also conceptually conflicts with the general concept of a class. In most other languages with classes, the entire structure of the class instance is known before even the first ancestor's constructor is run. This allows for situations like subclasses overriding functions that get called by ancestors during the constructor. If this proposal were to preserve the prototypal nature of a class, then that would still be the case as the prototype is applied before the constructor code using this can be run.

Understand, I'm not trying to spark yet another debate. I'm merely trying to point out that TC39 definitely took a conceptual left turn somewhere and ended up with a conceptually conflicted, trade-off ridden proposal with gotchas that cannot be conclusively described as worth while.

@ljharb
Copy link
Member

ljharb commented Aug 26, 2019

In fact, that was part of the motivation for using [[Define]] - so you didn’t need to know about superclasses to statically know the entire (modulo arbitrary modifications you make in the constructor, which always is both a possibility and something engines know how to handle) shape of instances (which engines know now, because fields appear statically and lexically in the class body).

@bakkot
Copy link
Contributor

bakkot commented Aug 26, 2019

In most other languages with classes, the entire structure of the class instance is known before even the first ancestor's constructor is run. This allows for situations like subclasses overriding functions that get called by ancestors during the constructor.

That is rather famously not true.

@rdking
Copy link

rdking commented Aug 26, 2019

Much like different browsers implement JS in slightly different ways, C++ has also been implemented in slightly different ways over the decades. This is something that used to work with Borland C++ and Microsoft C++ back in the 90's. While it's still true that the derived class would not yet be initialized, the v-table would be. So it's not that the call would go nowhere (segmentation fault), but rather that the call would be sent to a function that may be expecting an already initialized set of instance properties. That means that making such calls is generally bad practice, not that making such calls couldn't be done. It may be hair splitting, but it's an important distinction.

BTW, pointing out 1 language where that seems to not be true doesn't invalidate my statement. I didn't say "every language with classes". Further, even if you had successfully invalidated this one point, that doesn't lend any more credibility to the contradictions and conflicts embedded in class-fields, or in any way shake the arguments I've made regarding the nature of ES. In fact, the simple fact that you chose to attack a side example instead of the main point may appear to add more credibility to the point I made.

If you wish to discredit my argument regarding the difference between classes and object factories, and how fields blurs the distinction, thus causing conceptual conflicts, please attack that directly. Unless your argument is logically flawed, I won't counter.

@bakkot
Copy link
Contributor

bakkot commented Aug 26, 2019

In fact, the simple fact that you chose to attack a side example instead of the main point may appear to add more credibility to the point I made.

Lord. No. I just didn't want readers to be come away with a mistaken belief about a question of fact.

@rdking
Copy link

rdking commented Aug 26, 2019

Just as a note: The main reason for allowing such a thing is to handle cases where the base class knows how to contain and move information, but doesn't know the precise shape of it, while the derived classes know and need to be able to initialize that precise shape before the completion of the base class constructor. Admittedly, this is usually a sign that composition might be a better choice than inheritance. However, I'm of the mindset that a language developer shouldn't be in the habit of dictating what a language user should and shouldn't do (unless the particular practice is always catastrophic).

@mbrowne
Copy link

mbrowne commented Aug 28, 2019

@ljharb

In fact, that was part of the motivation for using [[Define]]

It would have been nice to know that back when many of us were struggling to understand the motivation for using [[Define]]. I realize it must be difficult to remember all the important points that were discussed about a topic and share them here, I just thought I'd let you know that I never saw that mentioned anywhere, even when I asked specifically if there were any other practical (and not just academic) issues in favor of [[Define]]. (Of course, it's possible this was mentioned in an earlier discussion that I wasn't a part of.)

@mbrowne
Copy link

mbrowne commented Aug 28, 2019

Back to @aimingoo's proposal... In addition to the technical problems, there's also a developer experience problem—the same one mentioned in the FAQ (which we've also discussed many times). If you can declare private x = 100 but you can't access it with this.x, that's going to confuse a lot of people.

P.S. A shorthand syntax might be fine (that's another debate), but the longhand syntax this.x should also work.

@ljharb
Copy link
Member

ljharb commented Aug 28, 2019

It was mentioned in many issues; it’s also in the notes.

@mbrowne
Copy link

mbrowne commented Aug 28, 2019

Ah, my mistake then for missing it—sorry.

@rdking
Copy link

rdking commented Aug 28, 2019

If you can declare private x = 100 but you can't access it with this.x, that's going to confuse a lot of people.

That's one view. Another is that if you can declare #x = 100 and x = 2, that's also going to confuse a lot of people.

Both are resolved in the same way, an explanation of the feature. Both require the understanding that private field is not a property of the object that owns it. That's sufficient to explain why you would need a redirecting operator to access a private field when the declaration is private x. However, when the declaration is #x, but # is not an operator, that requires an additional explanation.

Also, the claim that private x without this.x is going to confuse a lot of people is only true for a small subset of people, namely those coming from compiled OO languages with no ES experience. Telling them that private fields are accessed via a different operator would be sufficient to get them to understand how to use it. They'd come to understand why that is on their own as they learn the language.

Point being, there's a learning curve for both cases. Which one is steeper will depend on the person's development experience. As such, the forced implication between private x and this.x simply isn't sufficient justification for avoiding the more familiar (and, arguably, more aesthetically pleasing) syntax.

@aimingoo
Copy link
Author

@rdking @mbrowne @ljharb

If you can declare private x = 100 but you can't access it with this.x, that's going to confuse a lot of people.

Yes, as I said before, this is a big problem. So I took a moment to fix it.

Based on these principles:

  • not possible to access a private member of an object when his Class unaware. so,
  • not possible to directly access private scope outside of the Class declaration.

So hard define / explicit indication are necessary. And next, we can try this syntax:

class MyClass {
  internal private x = 100;

  compare(b) {
     return x === b[internal.x];
  }

  static compare(a, b) {
     return a[internal.x] === b[internal.x];
  }

Or checkout these testcases:

Okay. now, the private-property proposal add a part to support that conceptual syntax:

The (private a).x is syntax to depiction private scope access procedure of instances a, the procedure will return value of private member x of a.

The concept restricts (private a).x to be used only for prototype methods or static methods in class declarations, and only allows it to access the private domain of instances of the class in the context of the above methods.

NOTE: The obj.#x grammar is a implement of the conceptual syntax, because it is equivalent to (private obj).x.

this.#x or this[internal.x], or other, that's long, highly controversial topic. I will skip it and try more implements of new proposal. I need more ideas and suggestions, and tries.

Thanks all.

The new proposal at private property, try in real environment base on proposal-private-property branch @prepack-core.

@littledan
Copy link
Member

littledan commented Sep 14, 2019

Thanks for taking the time to think deeply about this problem and prepare this alternative. It's interesting to read about. However, I share the concerns mentioned by @bakkot and @ljharb above. I don't think this proposal is viable.

@rdking
Copy link

rdking commented Sep 14, 2019

I feel that:

  1. declaration of a data member should not begin with a symbol... ever
  2. the use of # for something not an operator is disturbing to both the syntactic and aesthetic flow of the language
  3. the FAQ-stated implication between private x and this.x is a forced import that is entirely unnecessary and irrelevant
  4. even in the spec for this proposal, the # token in a field definition occupies the same usage and purpose as the many other modifiers already available (get, set, static, async, *), yet it will be the only storage-modifying modifier that is not a keyword (* modifies the function being stored, not the method of storage)

Now, despite all of that, I'm sorry @aimingoo, but your proposal, even after providing this adjustment, still will not satisfy TC39, or even me. The meaning of your use of internal should be implicit to the meaning of private as it is never possible to access a private member externally. So adding an extra keyword just causes unjustifiable code bloat.

Consider this: while a.#x is one possible implementation of (private a).x. It can easily be show that it is not completely consistent with the conceptual syntax. On an operation level, the conceptual syntax makes the storage private to the instance, while the implementation makes the storage key private to the class lexical scope. Not at all semantically equivalent, but it does produce the same 1st order result (lets not talk about the 2nd and 3rd order problems it causes).

@aimingoo My point is that if you want to suggest something better to them, it must at least fit the 1st order result equivalence of the conceptual (private a).x syntax. In that way, your proposal doesn't quite get there.

@aimingoo
Copy link
Author

@ALL

In the past few days, it was the traditional Mid-Autumn Festival in China, so I did not reply to you in time. Thank you for your understanding.

@ljharb

and exotic objects also exist, and objects can have internal slots - they’ve never just been a collection of properties. Class fields merely expands their definition; there’s no conflict.

For exotic objects an internal slots, they do not change the external interface of the objects, A object is always accessed as a collection of properties. But in the description of class fields, the nature of the object's "consisting of something different" is exposed and becomes something that developers need to deal with. So it becomes a conceptual understanding, and it is different and contradicts of the properties.

@rdking
My main objection to class fields is not from grammar (eg a.#b) - although grammatical issues are indeed quite important and unbearable. My core point is that class fields create new concepts that don't fit the old ones, and even then, this sacrifice does not bring any valuable benefits, such as no visibility and accessibility for OOP at all. (protected and others) Consider a systematic solution.

class fields is definitely a thing that adds a plastic sheet to a hole and claims to have a fire cloak! The field does not solve the inheritance problem required for visibility (eg protected needs to observe private members in the subclass). Under the field scheme, this inheritance needs to be rebuilt (outside the prototype) or supplemented with other schemes, and the existing proposal throws the break into annotations/decorators, so the inheritance problem changes from one (prototype) In three places (prototype + visibility + annotation/decorators), how does this arson-prone solution go to the present step?

Based on "prototype inheritance" really can't solve the existing problems? Or "prototype inheritance" is not good enough for everyone to give up this path? ES6's class provides a good example of integrating prototype inheritance and class inheritance, although it is not perfect, but I think it is feasible.

Don't try to make things simple by adding things, usually it gets more complicated. And fields is.

@rdking

And, In the definition of conceptual syntax, I did make mistakes. I didn't realize that there needed a more complete and accurate definition. Thank you for pointing it out. I will fix it.

@ljharb
Copy link
Member

ljharb commented Sep 16, 2019

@aimingoo public class fields create properties, just like code in a constructor does; private class fields do not. There's nothing "different" that's exposed whatsoever - you can't even determine via reflection that a class has a field as compared to an Object.defineProperty call directly in the constructor. There is no contradiction here, and zero new concepts from the perspective of a consumer of the class - only from the perspective of the author of a class.

@trotyl
Copy link

trotyl commented Sep 16, 2019

@aimingoo

For exotic objects an internal slots, they do not change the external interface of the objects, A object is always accessed as a collection of properties.

The external interface of an object is already more than a set properties, like the call signature and constructor signature, they're part of the shape not defined by any property (it doesn't rely on .call or .apply as well):

interface Foo {
  prop: string // property signature
  (): boolean  // call signature
  new (): Bar  // constructor signature
}

The non-property signatures are de-facto behavior of JavaScript objects, even included in Web IDL.

Also, even without counting non-property signatures, an object can always have non-property state:

const foo = { value: 1 }
const bar = { value: 1 }
const isValid = new Set([foo])

Even foo and bar above have totally same properties set, they will still cause different behaviors, which doesn't make any difference with private fields.

// Removed some content due to misunderstanding

@rdking
Copy link

rdking commented Sep 16, 2019

@trotyl @ljharb The simple fact that a "field" is a delayed instance property definition that cannot be edited is already something far outside of anything else the language does. No other means of defining properties in the language waits before performing the described task. No other means of defining properties in the language completely hides the definition structure before use. No other means of defining properties in the language takes action on an object other than the primary products or parameters of the action.

The problem isn't whether or not it mimics existing techniques. It does that marvelously. The problem is that it is essentially snake oil. What it presents itself to be (codewise), and what it actually is are two entirely different things, and it causes problems that are core to the conceptual nature of a class. That's going to present problems for ES users coming from compiled OO languages, and ES users with no TypeScript or Babel experience. The subset of the ES community that I just described should not be blithely dismissed.

@ljharb
Copy link
Member

ljharb commented Sep 16, 2019

@rdking you’re also describing “code inside a function that creates a property” - you edit it the same way, by changing the source code. This is nothing new to consumers; and only a new way to write it for authors. The “other means” that already exists is called “a function”, and it does all these things if you wish.

I’m not addressing the mental model or intuition claims you’re making in this thread - merely that what it is is not conceptually new for consumers, and not conflicting.

@rdking
Copy link

rdking commented Sep 16, 2019

@ljharb This is where the hair splitting comes in.

you’re also describing “code inside a function that creates a property”

That function body is magically produced from the contents of the class definition's field descriptions. So in a very real sense, there's no function source to edit. Only a class definition. This is why I keep calling fields "pseudo-declarative" and "delayed instance properties". Such functions will be the only things in the entire language that generate an inaccessible, deferred action. ES doesn't actually have setTimeout/setInterval, and Promise returns you an object. This function, however, is completely hidden from the developer. It's not the class constructor, but is automagically run during the constructor's execution window with no possibility of altering its timing. Even super allows its call timing to be somewhat altered.

I’m not addressing the mental model or intuition claims you’re making in this thread - merely that what it is is not conceptually new for consumers, and not conflicting.

Look, even though I'm more than ready to shout it from the mountain tops that this is one of the poorest designs I've seen come out of TC39, there's actually nothing wrong with this proposal that cannot be avoided by simply not using it (which is what I'll continue to do). I just feel it a bit disingenuous to try to pass off what had to be done to make fields work as "nothing new to consumers". It's definitely new, and even conceptually, it's unlike anything that's been done in ES before. The only thing that's similar to what consumers are used to seeing is the instance properties it produces. But even in that, how common is it for someone to use Object.definePropert(y/ies) to add properties and set values in a class constructor?

Even where "fields" is relatively mundane, it still does things in an atypical way. That's what's going to lead to intuition and mental model failures. That's what's going to lead to bugs.

@ljharb
Copy link
Member

ljharb commented Sep 16, 2019

It’s new to class authors. It’s not new - or even really visible - to class consumers, Set vs Define notwithstanding (which is an issue unrelated to the concept of fields).

@mbrowne
Copy link

mbrowne commented Sep 16, 2019

The real issue is that the language painted itself into a corner when it introduced ES6 classes. Releasing ES6 classes was an amazing feat that involved balancing many competing wishes and concerns of TC39 members and the community. But the downside of the "maximally minimal" approach that allowed that proposal to get out the door was that apparently very few people fully thought through the ramifications for the addition of class properties/fields, namely the very limited design space and syntax choices. (To be fair, some of the limiting factors were due to other design choices baked into the language before ES6 classes were introduced, so it was a combination of things.) So we could argue until the end of time about the decisions made for this class fields proposal, but the fact is that every single possible solution would compromise on something.

Maybe the critics of this proposal are right that some alternative proposal (or even some slight tweaks of this one) would have been a better solution given the constraints, but the only reason it's such a contentious proposal in the first place is because these constraints mean that there's no way to make everyone happy; in fact there's probably no way to avoid making some people very unhappy. So let's not pretend that the language would be perfectly consistent with itself or perfectly intuitive to everyone "if we just did such-and-such". ES6 classes already introduced a mismatch between the syntax (or at least many developers' expectations of that syntax) and JS's existing object model, and that was probably unavoidable given the goal of more declarative syntax for "classes". Whether you believe that the class fields proposal makes that mismatch better or worse, the most someone can claim is "my compromise is better than your compromise," not "my solution is unequivocally the best and the other is a disaster". And a compromise that ensures that at least public fields are consumed in exactly the same way as before (as class fields does) has a lot going for it.

@rdking
Copy link

rdking commented Sep 16, 2019

So we could argue until the end of time about the decisions made for this class fields proposal, but the fact is that every single possible solution would compromise on something.

This is true. That's also why I'm so very interested in having TC39 come up with complete and thorough documentation of the concerns that factored into this proposal. Honestly, with any design project, a living document containing those concerns (Requirements Document) should always be created. Here, all we have is "the FAQ" which while indeed documents 7 or 8 of those concerns, doesn't come close to explaining why the approach ended up being what it is. Obviously, there were more concerns involved, more requirements than what is contained in "the FAQ".

The point of having such a document is that it gives everyone concerned a chance to weigh out the competing concerns in a manner that is clear to everyone. My perspective has always been that technical concerns outweigh emotional concerns for software development. That's why although I don't like the sigil, I won't argue about it. That's why I firmly believe [[Set]] is superior to [[Define]] for this proposal's purposes. That's why am adamant that the inability to use the purportedly invisible private fields in conjunction with Proxy is a deal breaker.

The flags I'm raising aren't about what I feel would be better, but rather about how expected (if not needed) functionality is being traded away. What we're getting in return has significantly less technical value to the class developer and class user than what we have to give up to get it. I will never believe that making something that was already simple more ergonomic is worth the cost of disrupting well understood development patterns. I will never believe that extending the coverage of an existing problem is a good idea when adding something new. And I am 100% certain that the visible semantics of this proposal can be achieved without violating these beliefs.

the most someone can claim is "my compromise is better than your compromise," not "my solution is unequivocally the best and the other is a disaster".

Given that ES6 classes were designed to be prototype-based like ES5-style classes, the attempt to pursue Java-like fields doesn't make sense. If they wanted to do that, then classes should have been designed with appropriate layering so that the data from ancestor classes is isolated to its own instance layer in the prototype chain. That was not done. So since the foundation isn't there, why does it make any sense at all to try to emulate Java fields?

I'm not claiming any one approach to be "unequivocally" superior, just that the approach taken is self contradictory in the face of the existing language structure, and that those contradictions are the source of the numerous problems with this proposal. Knowing that less contradictory proposals can be constructed (and even have been) is enough for me to claim this proposals technical inferiority. Note that my statements are always categorical, never absolute.

@aimingoo
Copy link
Author

aimingoo commented Sep 17, 2019

@rdking @ALL

to be continue, about conceptual syntax (private a).x

There is a basic conceptual question: For the so-called private property, is action of object to access a name in the private domain of its class, or action from its class and to access object (Own)private member?

For the 1st understanding, since the private property is actually owned by the class, the object just sees and uses it. So equivalent to class-opening its private domain. in this case, the class needs to create a scope of the class, and manage the visibility of the name (properties) in the scope, which is responsibility of class - A class is the creator of an object. This concept is correct, achievable, and does so in many other languages. But this does not apply to JavaScript. Because in JavaScript, the class is just the holder of the object prototype (MyClass.prototype), the class is not responsible for the inheritance of the object, nor is it responsible for the visibility of the object members. The class does not maintain any scope for the object. Especially the latter, which means that if you want to implement private/protection/... on the basis of this concept, you need to build a complete set of scope management based on inheritance relationship between multiple dimensions (actually maintain the class, the scope, instances of class). This is why the more complicated the discussion of Fields and private domain issues is: the responsibility for "implementation something(eg. inheritance and scope management)" is forced in that "declaration" syntax of class.

Then let's discuss the 2nd understanding. If the object itself holds a private property, then in principle the object itself can access it. The ability of an object to "access itself" is achieved through its own methods or prototype methods. So strictly speaking, accessing private properties in these two methods should be unlimited. For example (the following example attempts to illustrate that the "object method" should know that it is a private member of the action object when accessing x, because methods and properties are conceptually members of the object, and they are equal):

obj = {
  private x: 100,
  foo() {
    console.log(x); // own methods, unlimited
  }
}

// OR
class MyClass {
  private x = 100;
  foo() {
    console.log(x); // prototype methods, unlimited for instances
  }
}

a = new MyClass;

So, is the restricted when class accessing its object instances? In traditional languages, if this is the static lexical scope of the class, then no problem, you can let the owner of the class or class manage this as a scope. However, in JavaScript, there is actually no such thing as a "scope of a class". Instead, a class has its own private property (and a private domain) when it is treated as an object. There are still two choices here. One is that the class (as an object) continues to hold its own private domain, and the other is to move it into a "class lexical scope" that does not yet exist, and is managed by the class. The latter requires a new mechanism, which is costly, and as mentioned before, "an inheritance will rebuilding again."

So according to the existing language features, the more realistic approach is to let the class (as an object) hold its own private property. However, in this way, the behavior of accessing the private properties of a concrete instance needs to be explicitly indicated, such as the so-called this.#x. In JavaScript, classes (as function objects) also have their own private domain, which arguably illustrates the necessity of the (private a).x syntax. But, This is in conflict with the above description of @rdking:

use of internal should be implicit to the meaning of private

It is precisely because the class and the object have the same concept of the private scope, and base the second interpretation of the above, "the private domain is the object's own (Own)", so when class to access "private scope of object" must be Explicit internal access , not implicit and direct private access. To put it simply and bluntly, if we choose the second of the above two kinds of understanding, then we can’t avoid:

(as it is never possible) to access a private member externally.

And, indeed, even if we ignore the problem of class -> his instances entering the private domain during the visit, then we will encounter the exact same problem in protected. For example (this example shows that the private domain boundary of the instance is inevitable for the class):

class MyClass {
  protected x = 100;
}

class MyClassEx extends MyClass {
  static foo(a) {
    console.log((private a).x);
  }
}

So, it’s better to face it: because the private members of the object are their own, the private member access of class -> instance must be an explicit internal access!

Now we have come to our description of the conceptual syntax (private a).x. The syntax based on the 2nd understanding above.

  • The (private a).x is syntax to depiction access procedure of private scope of instance a. It means,
    • The object a has a private scope and x is a member in the scope.
    • The conceptual syntax (private a).x must express as a reference about x, to read (rhs) and assignment(lhs).

The conceptual syntax only indicates the existence of the private scope of a, and does not express how the private scope is managed, and does not indicate that concept when above reference is passed to the outside (class, instance, or domain of other entities).

Note: The obj.#x grammar is a implement of the conceptual syntax, because it is equivalent to (private obj).x in the case where the private scope is own by object a. But if the private scope is part of a class's scope, it is not equivalent to (private a).x.

Note: That based on the understanding of prototype inheritance, private property interprets member as "property". Which is an implementation choice, and the conceptual syntax here does not assume that x is private "object properties".

Finally, I have no intention of discussing any behavior that attempts to control the number of characters in source code by reducing or shortening the keyword. I explicitly prefer to be explicitly express (necessary) abstract concepts. The semantic clarity of the code text is very important, far more important than how much more or less it is in the number of bytes of code; the beautiful of the code formal or style is not achieved by controlling characters and symbols. But even so, for any implementation of (private a).x base on the pure concept syntax, I can finally accept. Whether it is this.#x or $.#x, or Is .#x or even PRIVATE_OF_CLASSS_OR_INSTANCE_AND_LONGGGGER_NAME(a).x.

What I want to emphasize is, the syntax style is not the core issue of the class fields proposal.

@rdking
Copy link

rdking commented Sep 18, 2019

@aimingoo

What I want to emphasize is, the syntax style is not the core issue of the class fields proposal.

Correct. Compared to the other issues with this proposal, the syntax isn't even worth considering a problem.

There is a basic conceptual question: For the so-called private property, is action of object to access a name in the private domain of its class, or action from its class and to access object (Own)private member?

Well, before I try to answer this, there were several major flaws in your analysis, not the least of which was:

However, in JavaScript, there is actually no such thing as a "scope of a class".

That statement isn't correct. Classes have a "lexical scope" just like every other structure in the language. That lexical scope and what is done with it has very real consequences on what can be done with a class. Try factoring that into your analysis and watch how it changes.

Getting back to the question, you asked an "a or b?" type question without realizing there's a "c". Here's the possibilities:

  1. class owns the private domain (data space)
  2. instance owns the private domain (data space)
  3. class owns a private domain for key names (name space) & instance owns a private data domain (data space)
  4. private domain for key names is external to class & instance owns private data domain.

Class fields is built on concept 3. In fact, most of the good proposals that were reasonable substitutes for this proposal used concept 3. There was 1 case of concept 4 (private-symbols). The reason for most proposals preferring concept 3 is because in an open access system like ES, if you can't protect the property names, then those properties are essentially public.

Case 1:
Even in JavaScript, a class is a type of object factory. A class is a combination of an initialization function and a prototype object. When that initialization function is run using new, a new object is created and granted the initialization's prototype object as it's prototype. Other than this, everything else you said was close enough.

Case 2:
Here, your misunderstanding about a class scope tripped you up.

The ability of an object to "access itself" is achieved through its own methods or prototype methods.

This cannot be assumed to be true. Consider an object with private data and an accessing function like obj in your first example. If someone later writes another function to access the private data and attaches it to obj at a later time, should it work correctly? What about if someone swaps out the prototype object with one that has methods expecting to access the private data? If it does, then the private data in obj is not truly private. So the 'ability of an object to "access itself"' should only be granted to methods that exist within the lexical scope of the object definition. Otherwise, no privacy will ever exist.

I could continue to argue the details here, but I think this should be enough for you to make some adjustments in your understanding.

@aimingoo
Copy link
Author

aimingoo commented Sep 18, 2019

I think, I can simply tell the idea of ​​my implementation, not to conceptualize it.

OVER.

@rdking Maybe we can discuss the implementation directly. In addition, for the implementation of the (private a).x syntax, you can refer to this:

https://raw.githubusercontent.com/aimingoo/private-property/master/wiki/images/Private-property-proposal-p4.png

I must explain that in the above implementation of conceptual syntax. Base on existing design of es6 class, the private domain (of instances and their class) is two separate, unconnected domains , which is why [[Internals]] is shared.

@rdking
Copy link

rdking commented Sep 18, 2019

Maybe we can discuss the implementation directly.

I don't mind, but it shouldn't be done here as it would no longer be about class-fields.

@aimingoo
Copy link
Author

@rdking Oh ha, welcome to new proposal. :)

@rdking
Copy link

rdking commented Nov 28, 2019

@rolivo The syntax issue is indeed minor. There are far larger technical and usability issues with this proposal that TC39 has decided to deem too small individually to merit stopping the proposal. However, the severity of some of those technical issues is greater than they have surmised. Further, the sheer number of technical issues compounded with breaks in usability and inconsistency with existing mental models makes it worth while to seek an alternate proposal IMO.

I am one of those developers that wants to have real OOP support. However, I'm not willing to break language conventions, common use cases, and even the concept of a class itself, just to wedge in one opinion on how it could be done. Sadly, the existing proposal does all of this.

As an alternative, I've offered my proposal-class-members, which after a little research into possibilities, I've recently revised. I've tried working out a babel plugin for it, but I find that between the highly integrated parser, and the content for the current proposal, I don't have enough understanding of how it works to add my proposal. In lieu of that, I've almost completed a SweetJS macro that implements it. I will release it when it is completed.

@aimingoo
Copy link
Author

@rolivo and @rdking The private-property proposal have a implement base on prepack-core at here, protected and public support are ready at here and public syntax. I will try babel plugin for this proposal, but this work will be postponed until about 1~2 month.

I think it should be based on existing concepts and ECMAScript components (include spec. types and design logic) to build a complete and clealy language experience that is consistent with traditional and typical OOP. This is the direction of my proposal.

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

No branches or pull requests

9 participants