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

Proposal for an alternate syntax regarding private fields #277

Closed iczero closed 4 years ago

iczero commented 5 years ago

I would like to propose:

Example

class Example {
  private a = 5;
  protected b = 2;

  getA() {
    return this.#a + this.#['b'];
  }
}

Reasoning

Possible drawbacks

I understand that this is already a stage 3 proposal and is unlikely to have changes, but I believe these changes would fix some of the problems raised by others while still retaining many of the benefits from the original proposal.

angelhdzdev commented 5 years ago

I just asked this in a Discord channel and they linked me here lol.

It was bugging me why using '#' to declare private fields, if we have the 'static' keyword for static fields and methods... it's just not intuitive and breaks consistence.

Having the public, private, protected access modifiers will make Javascript more appealing to people coming from static typed languages.

Your proposal looks like the right direction. I hope this gains more votes. Cheers!

ljharb commented 5 years ago

Please read https://github.com/tc39/proposal-class-fields/blob/master/PRIVATE_SYNTAX_FAQ.md which should address many of your bullet points.

iczero commented 5 years ago

@ljharb I have already read through the entire thing. It does not address any of my bullet points, except for the symmetry one in the drawbacks section.

@angelhdz They linked you to the wrong place, but thanks.

ljharb commented 5 years ago

@iczero https://github.com/tc39/proposal-class-fields/blob/master/PRIVATE_SYNTAX_FAQ.md#why-arent-declarations-private-x talks about why the private and public keywords aren't used; https://github.com/tc39/proposal-class-fields/blob/master/PRIVATE_SYNTAX_FAQ.md#why-doesnt-thisx-access-the-private-field-named-x-given-that-thisx-does indicates that dynamic private access is explicitly disallowed; to name a few.

Personally, additionally, I would highly object to anything that even implies that protected is a feature that can be used, as I feel that the feature itself is inherently inappropriate for the JS language - but I don't want to get into that yet again on github, so please feel free to reach out to me on another medium if you wish to discuss that further.

jridgewell commented 5 years ago
  • Private-type fields are declared using private, protected, etc

See https://github.com/tc39/proposal-class-fields/blob/master/PRIVATE_SYNTAX_FAQ.md#why-arent-declarations-private-x

  • A new operator .# is introduced allowing access to private-type fields

This is required, so 👍

  • Dynamic access can be done using .#[name]

See https://github.com/tc39/proposal-class-fields/issues/74. This is something that can be added later, but we don't feel it's necessary now.

iczero commented 5 years ago

@ljharb @jridgewell I don't see how using private instead of prepending the field name with a # is a drawback. Considering how private fields are to be accessed using the .# operator (and such asymmetry is intended), I do not believe that it would be confusing.

jridgewell commented 5 years ago

Because every other language that has private x accesses the properties with this.x. We believe that new users (who are very likely to come from another language that supports classical private) will be confused.

iczero commented 5 years ago

New users would only really be confused once, and after they learn the relatively simple semantics of the .# operator, they shouldn't be confused anymore. Compared to the current state of having a field starting with a # but actually not really being a real "property" in the traditional sense (can't be indexed by [], etc), using .# differentiates between accessing ordinary properties and private fields.

iczero commented 5 years ago

In response to the dynamic access (https://github.com/tc39/proposal-class-fields/issues/74#issuecomment-359074305), the .# operator would explicitly access private fields, while the normal [] operator would get properties, preventing possible confusion.

One of the benefits for dynamic access to private fields is to reduce redundancy:

handleInput(name, value) {
  this.#['input_' + name] = value;
}
shannon commented 5 years ago

@iczero I hope I can save you a bit of time and frustration here because several of us have gone through this same conversation with the TC39 mebers multiple times. They won't change their mind on this point.

iczero commented 5 years ago

@shannon I'd think it's worth a shot.

bakkot commented 5 years ago

New users would only really be confused once, and after they learn the relatively simple semantics of the .# operator, they shouldn't be confused anymore.

Most people on the committee do not share this expectation, and you are unlikely to convince us otherwise.

iczero commented 5 years ago

@bakkot I'd like to think developers aren't stupid.

(except for myself for believing that I would in fact be able to convince committee members)

iczero commented 5 years ago

Specifically compared to the property-but-actually-not-property-private-field thing of the current proposal where the field technically is prefixed by # but is actually stored in a different place than everything else and can't actually be indexed by normal methods, the .# operator would make private access explicit, thus reducing confusion.

angelhdzdev commented 5 years ago

I think this is a bad move. They lured us, the made us fall in love with Javascript thanks to ES6 and the class syntax, the static, the constructor(), the getters and setters, and we said _"wow, I see where this is heading to, and I love it!" only just to be told now "Nope, we not adding private/protected/public because reasons, LOL".

It's like something that is broken and you keep "patching it" instead of taking it to the repair shop, or buying a new one.

So basically the members of the "committee" don't care about what most people think because in practically every other language we have these things...

K then. Keep making Javascript look like a frankestein language and keep making people make fun of it and don't take it seriously.

bakkot commented 5 years ago

It's not stupidity to read code which looks like

class Foo {
  private x;
  setXToSecretValue() {
    this.x = 10;
  }
}

and not notice that it does something completely different than almost identical code would do in a language like Java.

Yes, people should not write this code. But some people will. And readers will be confused by it. A language which makes the this.x = 10 line do anything other than assign to the private field x is poorly designed.

Anyway, this has all been covered at extremely great length elsewhere, including in the FAQ. I don't think there is much more that can be said here.

iczero commented 5 years ago

@bakkot I read the FAQ and many other posts, and what I gather is that you do not expect users to be able to learn the syntax of (in this case) the .# operator for access to private fields.

For preventing errors, an eslint rule can easily be created preventing the creation of normal properties when a private field with the same name already exists, unless explicitly declared beforehand.

Considering that beginners to javascript are unlikely to use private fields anyways, I don't see how it would negatively affect those learning the language. For more experienced developers, they would quickly realize that their so-called "private" fields are actually very much in fact public, and would learn of their mistakes.

I would specifically like to compare this solution of making private access explicit with the previous solution of including the # in the name. The latter solution causes much more long-term confusion (such as why said private field can't be indexed dynamically using this['#a'], how the private field is actually stored in a completely different place compared to normal properties, among others) while the former would be a one-time, mostly harmless (how much harm, realistically, can a field accidentally made public do), and relatively simple concept to learn. The current solution tries to pretend that private fields are somewhat "normal", whereas in reality, they are completely different from normal properties and should be explicitly denoted as such, and any attempt to cover up this difference simply makes it harder to understand, and by extension, to learn.

Additionally, possibly the greatest drawback of the current solution is that it makes it so that the # character is not available to use in future, likely much more major language features, which is a complaint many people have had over this proposal. I believe that the current proposal of private fields is relatively minor in comparison to other possible improvements in the future, and should not take a symbol which could be used in major new features. Private fields do not have a major effect on how anyone writes code. One simply replaces _ with #. I personally think the benefits (of which there are many) outweigh the costs (one-time confusion) when it comes to making private access explicit with .#.

rdking commented 5 years ago

@iczero

(how much harm, realistically, can a field accidentally made public do)

💀 🎏 Get it? Don't make statements like that. You're inviting trouble.

I've actually proposed this exact same thing back in stage 2. It was one of 6 different possibilities I tried to sell to get out of the no-[] policy. They didn't bite back then either.

@bakkot

It's not stupidity to read code which looks like ... and not notice that it does something completely different than almost identical code would do in a language like Java.

How about I throw that argument back at you. It's not stupidity to read code which looks like

class Base {
   a = 10;
}
class Derived extends Base {
  get a() { return Math.random() * 10; }
}

let a = (new Derived).a;

and not notice that it does something completely different than almost identical code would do in a language like Java.

6 of one, half-dozen of the other, right? Not really. Both clobber the developer's expectations. However, this one does so in a way so surprising that almost no one would think to check for it.

bakkot commented 5 years ago

not notice that it does something completely different than almost identical code would do in a language like Java

That code would not be legal in a language like Java.

rdking commented 5 years ago

That code would not be legal in a language like Java.

class Application {
  public static void main(String[] args) {
    Derived d = new Derived();
    System.out.println(d.a());
  }
}

class Base {
  public int a = 10;
}

class Derived extends Base {
  public int a() {
    return 42;
  }
}

Go run it!

bakkot commented 5 years ago

That's... not a getter. There is no ambiguity; the invocation at the call site makes it explicit.

Anyway, this is pretty off topic. You are by this point extremely well aware that I do not consider that cornercase to be nearly as likely to come up, nor as severe if it does, as the one above. I am aware you disagree. Neither of us is likely to convince the other. This particular case does not have much bearing on the choice of syntax for private fields. Please forgive me from stepping away from this tangent at this point.

ExE-Boss commented 5 years ago

Could we at least use both the # sigil and the keyword?

class Foo {
    private #field;

    constructor(arg) {
        this.#field = arg;
    }

    get field() {
        return this.#field;
    }
}
iczero commented 5 years ago

@bakkot Either way, javascript (or rather, ecmascript) is fundamentally different from java and that should probably be the first thing someone learns when coming to javascript from. Or they're going to be tripping on a lot more things than accidentally making a field public.

Further elaboration on my previous points:

iczero commented 5 years ago

@exe-boss I'd think the sigil could be optional. If people think it is clearer with the sigil, then they can use it.

The main difference I am suggesting is specifically the .# operator instead of making the # part of the field name. Seeing as private fields are almost completely different from properties, it makes no sense that they should be accessed the same way.

bakkot commented 5 years ago

@ExE-Boss That was discussed at some length and ultimately rejected mainly on the basis that it's entirely redundant as soon as you've learned that #-prefixed fields are private, which you have to learn when using the feature anyway. (Personally I am neutral-to-positive on that suggestion, but many people were opposed and I don't think there's much inclination to revisit that discussion unless there's something which has changed since the last time we had it.)

@iczero I stand by this comment.

iczero commented 5 years ago

@bakkot And I stand by my comment that the issue you raised is substantially less confusing to anyone trying to learn javascript than to understand the weird property-but-not-really-but-it-looks-really-similar private fields thing that is what is proposed currently.

Additionally, if anything, private #a; would help reserve the # symbol for better use in the future.

iczero commented 5 years ago

Oh also, following the reasoning that private a could result in people trying this.a, I see no reason why someone learning the new syntax wouldn't regard the # in #a as simply being something to denote it being private, and then proceed to use this.a anyways.

iczero commented 5 years ago

Welp. It's been about 1 year and 12 hours since #150, and more than 2 years since #10. I will follow @shannon's advice.

MichaelTheriot commented 5 years ago

@bakkot And I stand by my comment that the issue you raised is substantially less confusing to anyone trying to learn javascript than to understand the weird property-but-not-really-but-it-looks-really-similar private fields thing that is what is proposed currently.

I agree, and raised similar concerns when I first became aware of this.

this.#prop syntax imitates dot notation property accessor, which is counterintuitive because these are not properties and do not behave like ECMAScript properties. E.g.:

I think this issue continues to resurface because the decision to proceed with this.# syntax vs the various others that have been proposed is unclear, and intuition expects different behavior here.

E.g. I have seen other suggestions and even a proposal demonstrating this->prop syntax and it still remains unclear to me the reasons against it, years later.

The current proposed syntax is not intuitive to many for multiple reasons. It would be great to comprehensively clarify why it is still considered ideal among others proposed.

rdking commented 5 years ago

I've also been trying to push that same view, that TC39 has taken a stance not consistent with developer expectation on how ES is generally used. However, instead it seems that they're taking the conventions of compiled languages with class trying their best to make those the conventions of ES. The conflict between that and the nature of ES not only causes mental model issues, but actual conceptual and semantic issues in the implementation. Yet somehow they're considering the collection of those individually rather small issues to be of insignificant concern.

claytongulick commented 5 years ago

I've spent more hours than I'd like discussing this, back when it was stage two, and before the frustrating combination of privates with class properties, in general.

We have hundreds of posts from javascript developers who dislike the syntax.

The folks who are making these decisions have been very willing to discuss it and share their (very good) reasoning - reasonable people can disagree.

From what I've been able to gather, the needs of some specific library authors are being valued more highly than the general javascript developer community feelings on the matter. In a way, this makes sense, and as time has passed, I've become more acclimated to the syntax.

I would still prefer that privates be removed from stage 3, and alternates discussed. I'd really like to see a decorator type approach to access modifiers, so we're not chained to access modifier rules from legacy languages, and instead could really innovate with access modifiers in the developer space (i.e. - imagine an access modifier that only allowed modification based on a chain of trust).

ljharb commented 5 years ago

@claytongulick to be clear; "access modifier rules from legacy languages" don't apply here. There's only two kinds of access in JS: reachable, or unreachable. Closures provide ergonomic and performant unreachability for everything but class instances; private fields provide the same for class instances.

rdking commented 5 years ago

@ljharb

There's only two kinds of access in JS: reachable, or unreachable.

As much as you want that to be true, it doesn't express every possible combination that exists in ES. Even closures can't be claimed to be purely either reachable or unreachable. There's a missing factor: by what. For instance, any function that is both lexically created in and exported by a closure will have access to that closure. Any other function will not.

In essence, I'm saying the same thing you are, but I'm saying it from a perspective that reveals another facet of the behavior. It is this very "by what" facet that can be used to model access modifiers and the corresponding behavior. If it weren't for this fact, closures would not be useful for modeling private behavior at all. In fact, they wouldn't even be a useful construct.

MichaelTheriot commented 5 years ago

I do not think # should indicate private when symbols and unicode can be valid identifiers.

class A {
  〺o = 5;
  #o = 5;
  $o = 5;
}

All are valid, but one of these is not like the other.

IMO, a compromise can be reached from the few ideas floated around this proposal...

class A {
  public x = 5, y = 6, z = 7;
  private x = 5, y = 6, z = 7;

  something(v) {
    this->x = v; // updates the private x, not public x
  }

  read(obj) { // obj instanceof A
    return obj->z;
  }
}

Pros:

Cons:

Maybes:

The this.x and private x issue is something I think developers should learn. The alternative is tricking them with # syntax into other expectations. I think if anything -> distinguishes private fields so they would not make the mistake in the first place, provided they know how JS works?

iczero commented 5 years ago

I personally think -> may be confused from other languages, but simply using any other combination of symbols would fix that.

p3k commented 5 years ago

i’d prefer to have no private fields feature in ecmascript at all if the syntax should really be like #x and the access is not this.x.

rdking commented 5 years ago

Wow. That sounds like the same argument from the FAQ: private x implies this.x. Sadly, however, not using this.x notation for access is one of the few things they got right. Since they're not willing to seal class instances, it means the public surface is always mutable. This means that they can't use this.x notation for private access because it would collide with the available namespace for public properties. That would be asking for problems.

p3k commented 5 years ago

thanks for the attempt to explain but how many people at all could possibly understand the reasoning?

all they see is some fancy new syntax coming out of nowhere including an extra dose of fresh confusion, e.g. why is this['#x'] not working?

i hardly can imagine how much time and discussions this issue might have consumed already but still, maybe it does need some more iterations to come up with a decent approach…

rdking commented 5 years ago

Here we agree. #x as a declaration for the sake of symmetry with this.#x while omitting this['#x'] which is historically expected to be symmetric doesn't make sense at all. If symmetry is so valuable, why break one symmetry to form another? If anything, keep the classical symmetry in tact.

I and many others have thrown many different syntaxes at them to remove this pointless hurdle, but to no avail. The simplest I offered was to use # as a private access operator. The access notation would change to this#.x, translating to this.[[Private]].x where [[Private]] is some hidden container. The benefit is that symmetry is restored with this#['x'], preserving user expectations, and it doesn't loose much if any ground with the unfortunate declaration syntax #x since # is understood to mean private.

As you might guess, that and all other attempts failed, miserably.

ccjmne commented 5 years ago

@rdking, it is quite surprising and unexpected at first: I was quite intrigued and somewhat baffled myself, but you do know what the reasoning behind this is.

Besides, you yourself also (used to?) reckon (in your own words), that:

[...] the ability to dynamically add a private field [...] would be counter-productive and dangerous.

That ability would certainly be introduced by implementing the syntax you can't make sense of having had dismissed.


It's not universally perfect, and I think enough of us have spent enough time discussing different syntaxes to realise there is none that would please everybody, in all its possible usages.

I think you probably are experiencing a major case of deja-vu by reading my comment: it's because all these discussions have already been engaged, and none gave birth to the mythical unanimous, uncompromising, universally ideal syntax.

The committee in its entirety isn't ecstatic about the proposal in its entirety: the committee itself is a collection of individuals with their own (differing) experiences and their own (differing) priorities, concerns, expectations, etc... It, unfortunately, is about compromising: it became glaringly clear to me and many others that there won't be a way around it.

If you do come up with an alternative that hasn't been discussed before, and for which you can't find an opposing argument, I (and everybody else) would absolutely love to study it with you. This proposal, as it stands, is, albeit bitterly, doing a solid job at juggling with the many expectations and implementing a solution that is undeniably working.


My point is: you are pointlessly being excessively dramatic when you say that some of it doesn't make sense. I wish we'd keep the noise in each thread to a minimum so that the information actually pertaining to the subject of the issue wouldn't be so hard to come by. Maybe there could be a section of the main FAQ dedicated to explaining in what way each other alternative poses new problems. Would you care to do that for the ones you brought up?

p3k commented 5 years ago

@ccjmne i am sure you don’t want “noise” in the threads when quite a bunch of developers are unhappy with what you are doing with the “subject”.

i think even what you call “being dramatic” can be qualified as valid feedback, especially connected with very constructive arguments as done by @rdking.

nevertheless: why not get rid of the dot and just use this#x for the syntax?

(i know i am chiming in at a really late point in time, and maybe this was already suggested, so of course you just could ignore this contribution.)

ccjmne commented 5 years ago

@p3k Actually, I like this syntax! And I remember it being discussed... but I can't quite recall nor find the rationale behind it being dismissed.

It probably had to do with the lack of symmetry between declaration and access, the fact it gives the impression that # is a special operator for accessing the x property of this (while #x is entirely different from x and both can exist together on this), and maybe even some potential issues down the line with ASI paired with potential shorthand access #x (without this).

Please note I can't actually recall what the argumentative was, and while the ASI + shorthand comment is quite a stretch, the other argument was most probably a part of the reason for dismissal!

MichaelTheriot commented 5 years ago

nevertheless: why not get rid of the dot and just use this#x for the syntax?

Refer to: https://github.com/tc39/proposal-private-fields/issues/39

This would apparently conflict with a #var shorthand that may be introduced in the future, even though there is no current shorthand for this.var.

rdking commented 5 years ago

@ccjmne

That ability would certainly be introduced by implementing the syntax you can't make sense of having had dismissed.

Yes, I did make that statement and still stand by it, but you're wrong here. Having [] access doesn't imply the ability to dynamically create new properties. If the target object is not extensible, then no use of [] will ever create a new property. I would expect that an object holding private properties on behalf of some other owning object would not be extensible, and thus not allow [] to create new private properties. ... So I'm still baffled.

It's not universally perfect, and I think enough of us have spent enough time discussing different syntaxes to realise there is none that would please everybody, in all its possible usages.

We agree here. My only argument is that there are technically superior syntaxes that can be used. Satisfy the technical needs before the individual wishes and wants.

The committee in its entirety isn't ecstatic about the proposal in its entirety: the committee itself is a collection of individuals with their own (differing) experiences and their own (differing) priorities, concerns, expectations, etc... It, unfortunately, is about compromising...

You speak as though I'm unaware of these facts. Even when compromising, there should be a bar, a limit beyond which you cannot go to reach that compromise. If the limit is reached before the compromise is reached, then the target should be deemed unreachable. Compromising the general expectations of a developer for the structure of the language when it is not necessary to do so is that bar for me. Oddly, TC39 does not seem to have such a bar as this proposal makes such compromises.

If you do come up with an alternative that hasn't been discussed before, and for which you can't find an opposing argument, I (and everybody else) would absolutely love to study it with you.

Did that too. There are a couple of them sitting around in my github repo. Feel free to have a look.

My point is: you are pointlessly being excessively dramatic when you say that some of it doesn't make sense.

I say that from my perspective. For every argument they've made, I've found not only a technical flaw in their reasoning, but also a solution that satisfies the requirements they give without compromising the capabilities of the language. The only responses I get back when trying to present them with such solutions is that they "made a judgment call" or "just thought x approach was better" or something equally lacking in logical support.

All I'm saying is that there is no reason why they cannot have what they want while still giving me what I need. The two things are not incompatible or conflicting in any way. It is only their choices that have caused the conflicts. That thing that doesn't make sense (to me) is why they would choose to do so. When I request an understanding of their reasoning, they have nothing to offer. This is baffling to me. When I make long lasting decisions that are going to affect more than me, I make sure to remember why I chose as I did because I know people are going to question my reasons.

Maybe there could be a section of the main FAQ dedicated to explaining in what way each other alternative poses new problems.

I've requested this too. I would love to see this.

Would you care to do that for the ones you brought up?

If I know those reasons, I won't mind helping. However, getting those reasons has been one of my biggest issues.

p3k commented 5 years ago

This would apparently conflict with a #var shorthand that may be introduced in the future, even though there is no current shorthand for this.var.

oh so there is a master plan for ludicrous syntax in ecmascript good to know

bakkot commented 5 years ago

this.#x more clearly implies field access than this#x:

class A {
  x = 1;
  #y = 2;
  m() {
    return this.x + this.#y;
  }
}

The shorthand syntax is not currently proposed, and leaving room for it is (in my view) less relevant than the above consistency.

rdking commented 5 years ago

@bakkot Hello. Let me ask you some thing: Outside of this proposal, member access in ES is either obj. member or obj[ memberName ]. Since this implies that an access attempt can be recognized by the presence of either a . or [, and this symmetry is very important to how the language is used, and given that it is important for the notation to imply access as you stated above, then why does it make more sense to disrupt the useful existing symmetry than it does to cooperate with it?

//Disruptive notation
obj.#x; //but `#` is not a valid identifier character, nor is it an operator
//No valid corresponding [] syntax

//Non-disruptive notation
obj#.x; // `#` is now a postfix operator that extracts the private storage object
obj#['x']; // No disruption in well known, highly used symmetries.

The declaration notation can remain #x, as this is prefix notation. This would draw a clear and important distinction between declaration and access. Is there truly so much more value to be found in having #x pair with this.#x that it is worth disrupting how the language is used when there's an equivalent notation that won't cause this disruption?

ccjmne commented 5 years ago

@rdking Thank you for correcting me there, I did make an incorrect shortcut in assuming dynamic access would go hand-in-hand with dynamic assignment.

I only now do remember you having some other proposals in your own repos: I meant to take a look but never got around to — thanks for the reminder.

I can't help but wholeheartedly agree with your last point about having troubles accessing all the arguments in favour and in opposition to each and every aspect of the proposal and their diverse alternatives: I must admit I can't retain and appropriate the entirety of the arguments that have been shared, be it in this repo only.

MichaelTheriot commented 5 years ago

this.#x more clearly implies field access than this#x:

class A {
  x = 1;
  #y = 2;
  m() {
    return this.x + this.#y;
  }
}

The shorthand syntax is not currently proposed, and leaving room for it is (in my view) less relevant than the above consistency.

#this.y should work just as well and clearly implies private field access (at least, to me). Are there any documented reasons for/against prefixing the instance being accessed rather than the field name?

rdking commented 5 years ago

I've been watching this language since I first saw it in Netscape Navigator. In all that time, I've only seen 3 less-than-great ideas surface into implementation:

Of the 3, this is the only one where I can definitively say that the technical issues outweigh any usefulness.