tc39 / proposal-private-methods

Private methods and getter/setters for ES6 classes
https://arai-a.github.io/ecma262-compare/?pr=1668
345 stars 37 forks source link

Yet another approach to a more JS-like syntax #28

Closed zocky closed 6 years ago

zocky commented 6 years ago

I realize I might be beating a dead horse, but it seems to me that I'm far from being the only one who intensely dislikes the use of # for private fields/methods. So here goes:

1) Use the private keyword to introduce private field, methods, setters, getters, exactly as with static. 2) Use bare field and method names without any qualifiers to access private fields and methods within the class's methods, just like we do now in factory functions, i.e. treat them like variables. 3) Use private.name to explicitly access the private fields of this object, if there are conflicting names, just like we do with super. Also use private["name"] for non-ident and calculated field names. 4) Use private(otherObject).name or private(otherObject)["name"]to access the private fields of another object of the same class. For symmetry, private(this).x is the same thing as private(this)["x"] private.x or private["x"] or just x if there's no variable x in scope. 5) In methods, private and private(otherObject) are not valid constructs without being followed by .field or ["field"] and throw syntax errors.

AFAICT, this achieves all the requirements, without introducing a wildly asymmetrical new syntax for private fields, AND gives us a natural way of reading code in human language, solving the naming problem that was brought up on other threads.

maple3142 commented 6 years ago

Is this a valid syntax?

class A {
  private a=1
}
const a=new A()
class B{
  m1(){
    return private.a
  }
  static m2(){
    return private.a
  }
}
new B().m1.apply(a) //1?
B.m2.apply(a) //1?
private(a).a //1?
mmis1000 commented 6 years ago

@maple3142 obviously No.
Private field can only be access in the class {...} quote, that's what the only purpose it exist. Even two class has a private field with same name, they are not the same one And in your example, B does never declare any private field with name a, so it is a syntax or runtime error.

zocky commented 6 years ago

@maple3142

private is lexically bound, just like variables in closures. It will never point to anything else than to the collection of private fields of the object that was created with the class definition. And in any case, you cannot use private(a) outside the class definition of whatever class a belongs to.

All of this is a bit hazy and somewhat conceptually incompatible with a prototype-oriented language like javascript. But that this is equally problematic with any approach to private fields in javascript, including the variation with '#' sigils.

Despite proposing this particular syntactic solution myself, I'm leaning towards the conclusion that the idea of having a rock-solid data protection of private fields with javascript classes is conceptually wrong. There's too much prototype, apply, etc. baggage in javascript "class-like" logic. People who want that kind of thing should probably start a new language that compiles directly to WASM and doesn't allow prototype walking or applying functions to random objects at all.

maple3142 commented 6 years ago

But according to @bdistin 's v8 screenshot, isn't private related to this context?

@mmis1000 What if class B has private a, but still use new B().m1.apply(new A())?

littledan commented 6 years ago

The disagreement about what the scoping should be in this thread (with multiple interpretations that are considered clearly correct) seems to point to the difficulty of using private.x rather than this.#x.

bdistin commented 6 years ago

I disagree @littledan , it's just as confusing an unexpected with this.#x for a 20 year Javascript user who has never touched java (btw @mmis1000).

And that example is misleading @bakkot as the following is true: img

If changing the this of the called function is ok to get the private field of a containing class, you should be able to get that field even if the nested class declares it's own version of that field; just like we can do above with public fields.

edit: I will go a step further. If it weren't for discussing private in human terms, rather than # Sigil, we wouldn't have identified what I can only describe as a spec failure of private. Logically and objectively only the left or right should be correct on the screenshot I posted last night.

While I still believe that the left is a breach of hard-encapsulation (because you are getting private members of a class that isn't itself) and since hard-encapsulation is the main goal of private in javascript the left should be incorrect, I recognize that's my opinion. I am open to the idea that the left could be the correct result, but if the left is correct, that means the right cannot be.

mmis1000 commented 6 years ago

@bdistin if you wish for that to happen, you will need to tell the compiler which of #a you referred to, because the #a of each classed are obviously different things, You can't ask runtime to guess that.
Unlike java, js does not provide a way to tell the compiler what did this value to use as before it run.

class A {
  #a = '1';

  init() {
    class B extends class A {
      #a = '2';

      log () {
        console.log(this.#a)
      }
    }

    new B().log()
  }
}

new A().init()

If you wish the inner class can access outer class's private property regardless of whether it is shaded. how should we tell the engine to do in this case?

If you are in some strong typed language, you just can just case this to A, and the engine will realize it should access which field, but in js, we don't do that.

It isn't impossible to do that, but the current syntax(both #a and private.a) does not have enough information for the engine to do it correctly(TypeScript may do, but we are not talking about them. (they not only reject to do that to access outer class's private property prevent confusing, they also error out if the inner and outer class have the same private field name.) Make a same field name in a method to have different meaning is just too crazy.

littledan commented 6 years ago

Sorry clicked close by accident.

bdistin commented 6 years ago

@mmis1000 There is no ambiguity in your example, the console logged will be '2'. But if you call new B().log.call(this) the console should log '1', not an error, just as if you didn't declare the #a in class B.

edit: That is assuming accessing containing class private is correct.

jridgewell commented 6 years ago

But if you call new B().log.call(this) the console should log '1', not an error,

Incorrect, this will throw. Declaring #a inside the inner class has changed the meaning of this.#a, it no longer refers to the outer #a declaration.

bdistin commented 6 years ago

the inner class has changed the meaning of this.#a

If you are going with this syntax, then #a should always be relative to the this; should it not?

mmis1000 commented 6 years ago

@bdistin I though, the way of symbol lookup is a different issue than this issue for.
And no, this issue does not change how this proposal works internaly, it is just an alternative syntax. You may be even possible to implement your own babel plugin to transform the syntax this issue proposed to the sucking sigil syntax. Isn't it better to open another issue to discuss about the symbol lookup problem?
Although I am distrust about the TC39 Team, I don't even think them will hear about this.
They already ignored 90% of the developer's opinion to choose a syntax no one wishes.
And they are likely to continuing do that.

bdistin commented 6 years ago

I apologize to the OP for going so off topic. I have created a new issue for the separate issue with Javascript's specification of private fields.

hax commented 6 years ago

I think @bdistin 's idea is still related to this (private.x vs this.#x) issue.

The confusion is coming from the this.x semantic, aka, property lookup semantic --- which always look up indistinguishable string x in the dynamic scoped this.

But #x is statically resolved, and it's not a property(string) at all, so this.#x (the hybridization of this.x and #x) is potentially confused.

This is why proposal like classes 1.1 suggest use a totally different operator for private state access (this->x or this::x) which I believe is the best way to avoid such confusion.

But even we still use ., I feel private.x is better than this.#x, because I guess most programmers see private.x as a whole -- private is a keyword, and there is no individual private object/reference like this. So private.x will be easily understand as a meta property like function.new which never imply property lookup semantic.

I feel it's much easy to teach and learn private.x is resolved statically, compare to teach and learn a new rule of already complex this semantic --- it seems many tc39 members believe this is too complex and refuse to add any even useful feature related to this. Though I don't agree your attitude to this, but at least please don't apply double standards in private proposals.

zocky commented 6 years ago

@littledan

The disagreement about what the scoping should be in this thread (with multiple interpretations that are considered clearly correct) seems to point to the difficulty of using private.x rather than this.#x.

Actually, this can be very easily clarified, by stating that private === private(this), and then applying the exact same rules for allowing access to private.foo as you would for this.#foo in the sigil version.

simonbuerger commented 6 years ago

Best argument against using the # has to be the precedent if you wanted to add more access modifiers in future. What if we want both private and protected class methods. Or internal (I'm borrowing from C# access modifiers here) Or allow multiple access modifiers "private protected" or "protected internal"? To go with the current proposal will you have this.#£a or this.~a or this.~#a? Just typing that out now made me shudder 😨

If not then how would we explain to new JS developers that some access modifiers are keywords (static, maybe others in future) and some are this new strange prefix syntax?

littledan commented 6 years ago

Thanks for all of your comments on this thread. In the end, TC39 is moving ahead with the proposal described in this repository, which remains at Stage 3 after a long year of reconsidering various alternatives. See the README for more details.

simonbuerger commented 6 years ago

Really appreciate the work done by tc39, but it ultimately feels like you're saying that you've taken a year to pretty much disregard all the strong opposition and alternatives put forward to the syntax of this proposal from the development community.

Saying you considered them but ultimately decided your way was the best doesn't feel like enough of a technical explanation to me. Is there anywhere we can see more of the additional discussion (other than here)?

In a language that has heavily favoured semantically clear keywords in the past - import, export, class, static, break, continue, etc. etc. this seems inconsistent.

bakkot commented 6 years ago

@simonbuerger The issues on this repo, on proposal-class-fields, and on proposal-private-fields contain most of the discussion (check closed issues especially). The private syntax FAQ gives a high-level summary of a lot of our thinking, and https://github.com/tc39/proposal-private-fields/issues/14 in particular is a long thread discussing alternatives and their feasibility or lack thereof in great detail.

simonbuerger commented 6 years ago

@bakkot thank you for taking the time to share that with me for the context. I can see the thorough debate that went on. I guess it may take me a good long while to get past the general "ickyness" of the whole this.#privateMethod thing ;)

simonbuerger commented 6 years ago

As someone coming from a strong html and CSS background, semantics of a language/language feature is the most important consideration, and quite frankly # carries no semantic meaning whatsoever. Ok, I'm done.

daniel-nagy commented 5 years ago

@zocky

We also declare static fields with static x but can't access them with this.x, but rather have to use this.constructor.x. (It would be nice if we could access them with static.x, but that's a matter for a different discussion.)

Static members are properties of the constructor...

This is completely valid and already works in Chrome today.

class Example {
  static foo = 'test';

  static bar() {
    return this.foo;
  }

  baz() {
    return Example.foo;
  }
}

console.log(Example.bar()); // test
console.log((new Example()).baz()); // test

I don't understand why we can't just use the keyword private for private member variables. Private members should be accessible from this within the class. Class authors are free to use whatever prefix they want to distinguish private member variables, or no prefix if they don't want to.

Maybe I'm missing something but I don't see what's wrong with the following.

class Example {
  private _foo = 'test';

  bar() {
    return this._foo;
  }
}

const example = new Example();

console.log(example._foo); // undefined
console.log(example.bar()); // test

We should also have the protected keyword IMO. If we use # for private members, do we need yet another identifier for protected members?

bakkot commented 5 years ago

Maybe I'm missing something but I don't see what's wrong with the following.

See the FAQ.

daniel-nagy commented 5 years ago

@bakkot

So the following would be impossible to add to the language?

class Foo {
  private test = 'test';
}

class Bar extends Foo {
  // creating a public member `test` that masks the private member `test` in class Foo
  test = 'bar';
}
class Point {
  private x = 0;
  private y = 0;

  equals(point) {
    // accessing private members from another instance of the same class.
    return this.x === point.x && this.y === point.y;
  }
}

I would say these are nice to haves but I wouldn't be that upset without them. For example, you could just do the following.

class Foo {
  private _test = 'test';
}

class Bar extends Foo {
  // no long conflicts with private member `_test` in class Foo
  test = 'bar';
}

class Point {
  private _x = 0;
  private _y = 0;

  get x() {
    return this._x;
  }

  get y() {
    return this._y;
  }

  equals(point) {
    // no longer an issue
    return this.x === point.x && this.y === point.y;
  }
}