proposal-class-fields icon indicating copy to clipboard operation
proposal-class-fields copied to clipboard

A new proposal, maybe late maybe not.

Open aimingoo opened this issue 6 years ago • 55 comments

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 avatar Aug 21 '19 07:08 aimingoo

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

aimingoo avatar Aug 21 '19 07:08 aimingoo

What if I want a local of the same name?

jhpratt avatar Aug 21 '19 07:08 jhpratt

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

bakkot avatar Aug 21 '19 07:08 bakkot

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

aimingoo avatar Aug 21 '19 08:08 aimingoo

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

aimingoo avatar Aug 21 '19 08:08 aimingoo

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

jhpratt avatar Aug 21 '19 08:08 jhpratt

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

aimingoo avatar Aug 21 '19 08:08 aimingoo

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.

jhpratt avatar Aug 21 '19 08:08 jhpratt

@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 avatar Aug 21 '19 08:08 aimingoo

@jhpratt hahaha, i know you will 👎

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

aimingoo avatar Aug 21 '19 08:08 aimingoo

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.

jhpratt avatar Aug 21 '19 09:08 jhpratt

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 avatar Aug 21 '19 09:08 aimingoo

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

aimingoo avatar Aug 21 '19 09:08 aimingoo

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

ljharb avatar Aug 21 '19 14:08 ljharb

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.

bakkot avatar Aug 21 '19 15:08 bakkot

@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

aimingoo avatar Aug 21 '19 19:08 aimingoo

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.

jhpratt avatar Aug 21 '19 21:08 jhpratt

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

rdking avatar Aug 22 '19 00:08 rdking

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

aimingoo avatar Aug 22 '19 04:08 aimingoo

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.

ljharb avatar Aug 22 '19 17:08 ljharb

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.

rdking avatar Aug 23 '19 04:08 rdking

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.

ljharb avatar Aug 23 '19 05:08 ljharb

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.

aimingoo avatar Aug 24 '19 08:08 aimingoo

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

ljharb avatar Aug 24 '19 13:08 ljharb

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

rdking avatar Aug 26 '19 00:08 rdking

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

ljharb avatar Aug 26 '19 03:08 ljharb

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.

bakkot avatar Aug 26 '19 04:08 bakkot

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.

rdking avatar Aug 26 '19 13:08 rdking

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.

bakkot avatar Aug 26 '19 14:08 bakkot

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

rdking avatar Aug 26 '19 14:08 rdking