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

A summary of feedback regarding the # sigil prefix #100

Closed glen-84 closed 6 years ago

glen-84 commented 6 years ago
Issue/comment 👍 👎 🎉 ❤️
Stop this proposal 196 31 20 45
Why not use the "private" keyword, like Java or C#? 132 7 12 5
Why not use the "private" keyword, like Java or C#? 💬 144 0 16 9
Why not use the "private" keyword, like Java or C#? 💬 51 0 0 0
Private Properties Syntax 33 4 0 0
Yet another approach to a more JS-like syntax 32 0 0 41
New, more JS-like syntax 32 4 0 0
Please do not use "#" 32 0 1 1
Proposal: keyword to replace `#` sigil 18 0 0 1
Why not use private keyword instead of #? 16 0 0 1
Explore a more JS-like syntax 7 0 0 0
The '#' looks awful, please change it 9 4 0 0
can we just use private keyword? 2 0 0 0
Do we still have a chance to stop private fields (with #)? 3 3 1 1
[Private] yet another alternative to `#`-based syntax 3 0 0 0
and yet another suggestion for "private but not closure" 0 0 0 0
Why don't use `private` instead of `#`? 0 3 0 0
Why "private" and "protected" future reserved keywords are not used? 0 0 0 0

Other feedback:

The TypeScript team are not fond of the syntax either.

My suggestion to rename the proposal: https://github.com/tc39/proposal-private-fields/issues/72 (the idea is mainly about making it a lower-level feature, and making room for a cleaner syntax at a later date)

@littledan @bakkot @ljharb – I apologize in advance, as I know that you must be thinking "Oh g*d, not again!", but I just wanted to put this all in one place, and allow the community to share their opinion in the form of reactions. It would be great if you could keep this open for a while (30 days?), or perhaps even indefinitely to prevent other users from submitting even more issues on the same topic.

To the non-committee members reading this: These proposals are now at stage 3, so it's unlikely that a syntax change will occur, but please feel free to up-vote this issue if you feel that the sigil (#) prefix is not a good fit for the language, or down-vote the issue if you believe that it's perfectly acceptable.

glen-84 commented 6 years ago

What do you think? Is your proposal something that you can see yourself using (and enjoy using)?

As you'll know, my first preference is dot access, but since:

  1. It's practically impossible in today's JavaScript (well, without affecting performance, from my [limited] understanding)
  2. The committee don't seem willing to re-split private access into a separate proposal, pull the plug entirely, reclassify, support both hard and soft private, etc.

... I would most certainly prefer writing (and reading!) the syntax from my "proposal".

I would probably leave the "optional public" aside (or just as a note on integration with TypeScript), since it may distract from the main idea.

Are you suggesting making it required, or removing it entirely?

I personally think that it makes the example look cleaner and balanced, but I will follow your advice if you think that removing it (for now) makes sense.

glen-84 commented 6 years ago

@zenparsing I forgot to mention you above. I've created https://github.com/tc39/proposal-class-fields/issues/107 FWIW.

bdistin commented 6 years ago

I am confused on why the private keyword is somehow not as good as using the # sigil prefix, as it's more feature complete than what the sigil prefix can provide. Let me explain:

Description public/this private #sigil
Define a property (constructor) this.property private.property this.#property
Define a property (field) property private property #property
Access a property this.property private.property this.#property
Dynamically access a property this['property'] private['property'] Not Possible
Define a method method() private method() #method()
Call a method this.method() private.method() this.#method()
Dynamically call a method this['method']() private['method']() Not Possible
Get a property from another instance of this class instance.property private(instance).property instance.#property
Dynamically get a property from another instance of this class instance['property'] private(instance)['property'] Not Possible
Run a method on another instance of this class instance.method() private(instance).method() instance.#method()
Dynamically run a method on another instance of this class instance['method']() private(instance)['method']() Not Possible
Destructure const { property } = this; const { property } = private; Not Possible

Not only is using private more consistent with the existing this and super paradigms, it leaves the possibility to be consistent with future (and IMO far more important features) protected properties/methods, and friend classes. Substitute protected in wherever you see private, and you have a fully functional protected language spec, just that you could use protected.properties and protected.methods() in subclasses, and protected(instance).property and protected(instance).methods() in friend classes or other instances of this class.

And that's all before repeating endless arguments on how the # Sigil is harder to read/understand by new devs, or devs coming from other languages... (I even had to double check a few entries on my table, since the # Sigil paradigm is so against what is existing/predictable in this language.)

ljharb commented 6 years ago

That means private has an implicit this; private(obj) returns an object, conceptually? Can this object be passed around? Can i have symbols on it?

It’s private fields, not a private object - to me, your suggestion is not intuitive, consistent, or viable.

bakkot commented 6 years ago

A fun bit of history: the private(obj).x syntax was discussed as a strawman back when private fields were first being developed in earnest in 2010/2011. Here's Brendan's summary from the meeting notes:

The private(this).x, private(other).x syntax in the wiki'ed proposal is an intentional non-starter: too verbose, wrongly suggests that private variables are properties of some "private data record" object.

Anyway, @bdistin, apart from @ljharb's objections, please see the FAQ.

bdistin commented 6 years ago

Good point, the # Sigil cannot have Symbol properties or methods, unlike my proposal. Making it even more consistent with existing paradigms.

To me, # Sigil is not intuitive, consistent, or viable...

zenparsing commented 6 years ago

@bdistin Thanks for taking the time to write out that table.

A couple of thoughts:

bdistin commented 6 years ago

It does offer the complete dynamic flexibility you would expect from a flexible and dynamic language. Even if that means you can do some strange things, that's the strength of javascript.

It doesn't feel odd no, they are separate namespaces (private and public). You would expect to both be able to have private foo and public foo and be able to access both as appropriate. Additionally, hopefully in the future, we will have a protected namespace with its own rules governing access.

It's explicit and clear what is being written. When I verbalize if someCondition and someOtherCondition, it makes sense to use if and the && symbol does it not? When I verbalize I want to get a private instance property, it makes sense to get the private(instance).property. In what way does instance.#property make sense when said out loud?

Additionally, how would you access private static methods/properties with the Sigil proposal? Although quite verbose, it still makes clear sense that private(this.constructor).property, private(this.constructor).method(), private(this.constructor)['dynamicProperty'], private(this.constructor)['dynamicMethod']() would work. (Where this.constructor could also be the class name).

If I were to guess this.constructor.#property, this.constructor.#method() but again no dynamic access. Which removes the strength of Javascript, and is as unreadable/clear as every other use of the # Sigil

allenwb commented 6 years ago

A fun bit of history: the private(obj).x syntax was discussed as a strawman back when private fields were first being developed in earnest in 2010/2011.

Oh, it goes back much further than that. Adding class definitions and such were first seriously discussed within TC39 in 1998. By 1999 the first generation draft ES4 specification had evolved to include the concept of name spaces that could be applied to property name. So, you might say something like obj.private::x (note that in some contexts the private name space qualifier could be inferred). This name space concept eventually found its way into ActionScript and the second generation ES4 design. I don't have a direct reference at hand but I'm pretty sure that I've seen the private(x) alternative mentioned in the late '90s notes.

I don't know what it says that we are still wallowing in a 20 year old design tar pit.

bdistin commented 6 years ago

Here is a fun example of using a private symbol method (which is impossible to do directly with the # Sigil):

const fouledOut = (a, { isFouledOut }) => a + Number(isFouledOut);

class Team extends Map {
    private metaData = new WeakMap();
    constructor(members) {
        super(members);
        for (const member of this.values()) {
            private.metaData.set(member, new TeamMemberMetaData())
        }
    }
    // assuming other overwrites to create TeamMemberMetaData on the set method/etc
    // assuming other methods to mutate WeakMap state per member
    private *[Symbol.iterator]() {
        for (const member of this.values()) yield private.metaData.get(member);
    }
    get score() {
        return [...private].reduce((a, { points }) => a + points, 0);
    }
    hasMoreFouledOutMembers(otherTeam) {
        const thisTeamsFouls = [...private].reduce(fouledOut, 0);
        const otherTeamsFouls = [...private(otherTeam)].reduce(fouledOut, 0);
        return thisTeamsFouls > otherTeamsFouls;
    }
}

class TeamMemberMetaData {
    constructor() {
        this.points = 0;
        this.fouls = 0;
    }
    get isFouledOut() {
        return this.fouls >= 5;
    }
    // assuming other methods to mutate state/etc
}

Without that dynamic access/ability to create symbol methods, you couldn't do for (const memberMetaData of private) or [...private]. You would have to give it a name, such as *#metadatas() (if that's even valid syntax? I hadn't even considered what private generators would look like with # Sigil...) and loop over this.#metadatas() which even just typing this is super confusing.

rdking commented 6 years ago

@bdistin The biggest issue with using private as you are suggesting here is that someone can do this:

class Example {
   private someData = 1;
   increment() { ++private.someData; }
   value() { return private; }
}
var e = new Example();
e.increment();
console.log(`e = ${JSON.stringify(e.value)}`);

With private or private(obj) simply returning an object, there's nothing stopping encapsulation from being broken. While I agree that the private keyword needs to be used for private field declarations, using it for access as well will lead to problems without careful consideration. Besides that, @trusktr made a library that more or less implements this suggestion w/ regards to member access.

bdistin commented 6 years ago

I don't follow the issue @rdking? Clearly, the author intends for the private namespace to be returned from the value method. Given the nature of private, you could never extend the class and expose private that way, since each class extension level would be a layer of encapsulation. (private in a parent class cannot be accessed in subclasses, and vice versa.) The only way that a class can expose it's private or private members would be intentional design.

So while you may have an opinion that class authors shouldn't do that, why should the language have an opinion on what you can and can't use a tool for? Sure, linters probably would, and should make rules to give authors the individual power to enforce that opinion. It wouldn't even bother me if a lint rule like that was enabled by default.

class Dict {
  #data = something_secret;
  get(key) {
    return this[key];
  }
}

(new Dict).get('#data'); // returns something_secret

It is quite understandable why this is a problem, and a reason to exclude dynamic access with the # Sigil. Which is exactly why I think the # Sigil is "not intuitive, consistent, or viable..."

class Dict {
  private data = something_secret;
  get(key) {
    return private[key];
  }
}

(new Dict).get('data'); // returns something_secret

But this should 100% work, is intuitive, it's consistent with existing paradigms and completely viable; as this is intentionally designed to access and return private members. Regardless of your personal opinion on whether the author should do that or not, the language itself should have no opinion.

class Secret {
  private data = something_secret;
}

Given this class, there is no way to ever get the private data from it. You cannot extend this class to expose private. It's perfectly encapsulated.

Note that wouldn't be true of protected, however, because that's the nature of protected vs private.

Edit: The FAQ even covers this:

What do you mean by "encapsulation" / "hard private"?

It means that private fields are purely internal: no JS code outside of a class can detect or affect the existence, name, or value of any private field of instances of said class without directly inspecting the class's source, unless the class chooses to reveal them. (This includes subclasses and superclasses.)

It seems classes explicitely choosing to reveal private or private members is perfectly acceptable according to the FAQ. Infact, nothing in the FAQ conflicts with what I have written in these few comments; while at the same time I have objectively provided a more feature complete, clear/easy to understand, and consistent design. Even if that means trudging up 20 year old ideas.

mbrowne commented 6 years ago

I have yet to see an alternative proposal that doesn't introduce problems as bad or worse than the # syntax. Moreover, I think # is an elegant solution given the constraints.

shannon commented 6 years ago

@mbrown, @ljharb, @littledan I've been following this proposal for quite some time now. I think until the rest of the community has visibility into this mysterious list of requirements the TC39 has for this proposal, these threads are going to drag on and on while people try to make sense of what they do see. The threads are beyond way too long to try to piece together the requirements on a "case by case basis". The requirements need to be prominently displayed in the proposal so we can all get on the same page about the ultimate goal of TC39.

rdking commented 6 years ago

@mbrowne Can you take a look at #104 and tell me if it is also equally bad? I'm fairly certain I did a decent job of removing many of the issues surrounding the currently proposed use of # without introducing new issues. I've just not received any feedback beyond a fairly terse refusal. Also, the fact that the "elegance" of the current solution has to be qualified by the "constraints" which themselves have not been given probably means that there's likely something wrong with the constraints. When 1 + 2 !== 3, it's time to review what each of '1','2','3','+', and '=' means.

bdistin commented 6 years ago

As far as #104 I think the # being on the left does solve the namespace/dynamic access problem but only makes the # Sigil usage even more confusing than it is already. Especially when you have:

class foo {
  private bar = some_secret;
  baz(instance) {
    return instance#.bar;
  }
}
  1. There is no parity between how the variable has to be initiated, with how it's accessed, and given the # Sigil is on the left, there is no way you can use the # Sigil to initiate the private member (that I can see). Which is inherently confusing.
  2. Method baz is passed instance, but it appears like an undefined variable is being accessed when you call it instance#. Making the above code especially confusing/unclear.
mbrowne commented 6 years ago

I agree with @bdistin and also @littledan's (first) comment on #104. It might be functionally OK but it certainly seems like it would be more confusing to newcomers.

mbrowne commented 6 years ago

(continued @rdking) When I referred to "constraints" I was talking about the inherent constraints of the JS object model that need to be respected to preserve backward compatibility and maintain consistency of the model. In other words, factors that are pretty much set in stone that we can't ignore.

mbrowne commented 6 years ago

For those who really dislike the sigil (#) syntax, perhaps a more productive approach would be to start a new proposal for a different syntax for other access levels, e.g. soft private, protected, internal. I suspect that in practice, there will be many situations where hard private isn't the best solution anyhow. Encapsulation is important of course (and I am a big fan of it), but sometimes total inaccessibility is a problem because you really need reflection or plugin capabilities, and you can't always foresee all the ways in which consumers might need to extend your class or inspect its state. In object-oriented design, encapsulation isn't really about hard private members; it's more about documentation and preventing common mistakes. Most of the constraints that have been pointed out by the champions of this proposal are specific to hard private, not soft private. I'm not sure what the best terminology would be -- it's probably not softPrivate -- but for example, I think this would be viable as a part of some future proposal:

class Demo {
    softPrivate x = 1
    protected y = 2

    foo() {
       console.log(this.x, this.y) 
    }
}

Decorators would be another option (and will be available sooner). As to the advantage of keywords over decorators, I think it's mainly a question of which syntax is preferable (I'm undecided at this point)...depending on the specific capabilities of current and future versions of decorators of course.

While I think the # syntax for (hard) private fields is a good solution, I do have a small concern about this proposal which is that hard private could be overused in the absence of any less restrictive access modifiers and since decorators are still in stage 2. But it's better to have some built-in access control than none, and of course hard private is literally impossible to implement in what I would call a satisfactory way without native language support. So all things considered, I'm strongly in favor of this proposal moving forward in its current form.

rdking commented 6 years ago

@bdistin I get where you're going with the constraints problem, and thanks for reviewing the suggestion I made. As for your points:

  1. private field here is just a declarative synonym for #.<field> or #[<field>] for calculated field declarations. The sigil itself essentially means .[[PrivateFieldValues]]. In the case of a declaration, the left object is the class or object being declared. It's not a short-hand but a requirement. In this way, private bar = some_secret means foo.[[PrivateFieldValues]].bar = some_secret; This (at least to me) is far more consistent and easier to explain than any attempt to explain why #bar is the full field name and at the same time a private field declaration. Convention over configuration doesn't exist in ES's syntax. The .# notation introduces exactly that.
  2. Given what I said above about # meaning .[[PrivateFieldValues]], what is there to be confused about instance.[[PrivateFieldValues]].bar?

To prove this to myself, I just showed the following code to a new-hire sitting next to me. I had him choose between the existing proposal (proposal 1) and my suggestion (proposal 2). I told him that the 3 declarations are equivalent and that the 2 proposals are for how to introduce private members into JS.

//Java
class foo {
  private int bar = some_secret;
  int baz(foo instance) {
    return instance.bar;
  }
}

//JavaScript Proposal 1
class foo {
  #bar = some_secret;
  #['bar2'] = 'x'; //Error
  #[Symbol('bar')] = 'y'; //Error
  baz(instance) {
    this['#bar'] = 0; //New public member named '#bar'
    return instance.#bar;
  }
}

//JavaScript Proposal 2
class foo {
  private bar = some_secret;// #bar = some_secret
  #['bar2'] = 'x'; // new private member 'bar2'
  #[Symbol('bar')] = 'y'; //new private member Symbol('bar')
  baz(instance) {
    this#['bar'] = 0; // works as expected
    return instance#.bar;
  }
}

After I answered his questions about the meaning of the # in each case (my personal biases removed), he chose proposal 2. When I asked him why, he said proposal 2 was easier to understand. and made more sense.

At this point though, I guess it doesn't matter unless either @littledan or @bakkot decide to reconsider.

rdking commented 6 years ago

@mbrowne I'd be in favor of writing such a proposal as well. Here's the problem: this current proposal is already in stage 4(I think?). The likelihood of it being suspended while other proposals are reviewed is next to none. If this proposal is accepted, then all counter-proposals will be effectively dropped as it won't matter anymore. What's more is that many other suggestions have been offered and refuted either on technical merits (good reasoning) or presumptive issues (questionable reasoning).

It really is true that the number of possible alternative formulations that won't fail for technical reasons is already very low, but when you start factoring in all of the unfounded (but not entirely unreasonable) presumptive issues that people bring up, it is almost a certainty no other proposal will survive as long as the current one has without actually testing the validity of the presumptions. I get the impression that, due in part to how long TC39 has been working towards this feature, that they are no longer willing to incur the delay needed to test their presumptions. As such, I have little hope that TC39 would even be willing to accept an alternative.

bdistin commented 6 years ago

@mbrowne I too am concerned about overuse of private, given the absence of less restrictive access modifiers. But I am far more concerned with the lack of consistency between # Sigil, current JS paradigms, and future paradigms (such as protected/hidden/soft private). Nothing would be worse than ending up with:

class Demo {
    x = 0; // a public x
    softPrivate x = 1; // a softPrivate x
    protected x = 2; // a protected x
    hidden x = 3; // a hidden x
    #x = 4; // looks like a public #x, but is really private x

    foo() {
       console.log(this.x, softPrivate.x, protected.x, hidden.x, this.#x);
       console.log(this['x'], softPrivate['x'], protected['x'], hidden['x'], 'I cannot access the private x dynamically because the # Sigil makes private accessors the blacksheep of all class accessors and javascript as a whole');
    }
}

Especially because it's easily solved by using the private keyword:

class Demo {
    x = 0; // a public x
    softPrivate x = 1; // a softPrivate x
    protected x = 2; // a protected x
    hidden x = 3; // a hidden x
    private x = 4; // very clearly a private x

    foo() {
       console.log(this.x, softPrivate.x, protected.x, hidden.x, private.x);
       console.log(this['x'], softPrivate['x'], protected['x'], hidden['x'], private['x']); // Woohoo, works!
    }
}

I agree that an official proposal should be written, but that proposal should include private as part of it's consistent syntax, instead of forcing private in with class fields and being stuck with it's inconsistent, unintuitive, and feature gimped form it is currently in.

Edit: Devil's advocate: You don't really expect that each of the half a dozen possible accessors would have its own Sigil (do we even have enough ASI compatible Sigils left for that), or that dynamic access would be impossible on anything but public, do you?

mbrowne commented 6 years ago

@rdking

this current proposal is already in stage 4(I think?). The likelihood of it being suspended while other proposals are reviewed is next to none. If this proposal is accepted, then all counter-proposals will be effectively dropped as it won't matter anymore.

I don't see a separate proposal for other access levels as I mentioned as in any way incompatible with this proposal. This proposal can move forward and additional keywords for other access levels could still be introduced in the future. The fallacy of sunk costs is definitely something to watch out for, but I don't think that's what's going on here (because I don't see this proposal as preventing future, complementary proposals).

mbrowne commented 6 years ago

Especially because it's easily solved by using the private keyword

Unfortunately this is not a viable solution for hard private, as has been discussed at length and as mentioned in the FAQ. Let's give the committee members a chance to publish the design considerations they said they're working on so we can all understand the rationale more fully.

rdking commented 6 years ago

@mbrowne I get it, but I can see that possibility. If the current proposal is implemented as-is, the possibilities for implementing other kewords like protected, internal, etc will be limited. Just as the (presumptive) argument against using private foo without this.foo is blocking the use of private in the current proposal, and the (technical) argument against using this.foo for private fields is due to the merit? of completely hiding private members so as not to affect descendant classes is blocking use of this.x and thus private x in the current proposal, this proposal will limit how protectedmembers can be implemented and make internal even more difficult.

bdistin commented 6 years ago

The FAQ's only reasoning for being against private is a false assumption that the left-hand side must be this. In fact, your own example above wrongly assumes the same making impossible syntaxes. Notice in the examples I provided, I corrected your assumption that a softPrivate x cannot be this.x, as you would need to be able to distinguish the x member of all public, private, protected, softPrivate, hidden, etc.

What I have proposed as accessing with private.x / softPrivate.x / protected.x both solves the false assumption you and the FAQ have made, as well as providing a consistent and feature complete accessor spec that can be reused for all accessors past present and future.

In fact, nothing I have suggested goes against any design concerns listed in the FAQ. And it's a far clearer, predictable, and most importantly scalable solution than the # Sigil. Unless as stated above tc39 intends on finding another half a dozen ASI compatible Sigils to use for other/future access modifiers, and intends on blocking dynamic access on all non-public accessors.

mbrown commented 6 years ago

Why am I receiving these emails? I'm not a member of the Ecma International, Technical Committee 39 - ECMAScript.

cheers Matt Brown (not Browne)

On 2018-06-29 10:43 AM, bdistin wrote:

The FAQ's only reasoning for being against private is a false assumption that the left hand side must be |this|. Infact, your own example above wrongly assumes the same making impossible syntaxes. Notice in the examples I provided, I corrected your assumption that a softPrivate x cannot be this.x, as you would need to be able to distinguish the x member of all public, private, protected, softPrivate, hidden, ect.

What I have proposed as accessing with private.x / softPrivate.x / protected.x both solves the false assumption you and the FAQ has made, as well as providing a consistent and feature complete accessor spec that can be reused for all accessors past present and future.

Infact nothing I have suggested goes against any design concerns listed in the FAQ. And it's far more clear and predictable solution than the # Sigil.

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/tc39/proposal-class-fields/issues/100#issuecomment-401409842, or mute the thread https://github.com/notifications/unsubscribe-auth/AAJ46glJxsATlIQkL1RHHXCrtmIYjMlxks5uBlkfgaJpZM4T1gR6.

shannon commented 6 years ago

@mbrown That's my fault I incorrectly @'d you instead Matt Browne. I apologize for that X-(

mbrown commented 6 years ago

No worries, thanks for the clarification

On 2018-06-29 10:56 AM, Shannon Poole wrote:

@mbrown https://github.com/mbrown That's my fault I incorrectly @'d you instead Matt Browne. I apologize for that X-(

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/tc39/proposal-class-fields/issues/100#issuecomment-401413323, or mute the thread https://github.com/notifications/unsubscribe-auth/AAJ46pU5_I4DWnXMdqZc0xkIWbqTRE50ks5uBlw4gaJpZM4T1gR6.

glen-84 commented 6 years ago

@mbrowne,

Encapsulation is important of course (and I am a big fan of it), but sometimes total inaccessibility is a problem because you really need reflection or plugin capabilities, and you can't always foresee all the ways in which consumers might need to extend your class or inspect its state. In object-oriented design, encapsulation isn't really about hard private members; it's more about documentation and preventing common mistakes.

I agree, but the committee members don't. Regardless, there's no point in discussing that here, other than to say that the current proposal doesn't really consider how other modifiers might look in the future, especially if they also require a sigil prefix.

Decorators would be another option (and will be available sooner). As to the advantage of keywords over decorators, I think it's mainly a question of which syntax is preferable (I'm undecided at this point)...depending on the specific capabilities of current and future versions of decorators of course.

I don't like the idea of using decorators for such integral parts of the language. You'll have keywords for static/const/let/etc., and then suddenly protected is a decorator ... seems out of place to me.


Your softPrivate keyword could just be private (and would match other languages with that keyword + reflection capabilities), and hard private could continue to use #. Again, committee doesn't like this ... too many options for new developers.

mbrowne commented 6 years ago

@glen-84

I agree, but the committee members don't.

I'm not sure that's really true. I don't think I've seen committee members argue against other access levels...the main argument I remember seeing is that hard private is important to library authors.

I don't like the idea of using decorators for such integral parts of the language. You'll have keywords for static/const/let/etc., and then suddenly protected is a decorator ... seems out of place to me.

I agree that consistency important. In the long term I think that it would be better to have actual keywords for soft private and protected. The only good reason to favor decorators would be if people find access control to be better managed in userland for some reason. But I think it's much more likely that decorators will just provide an interim solution and allow JS developers to explore what access control solutions work best. The combination of this proposal and decorators (with possible future additional capabilities for decorators) will provide good tools to do that. But I agree that it is weird for things like static to be native keywords, and # for hard private, and then decorators for all other access levels. Hard private is in some ways special - not only an access level, but potentially a security feature as well - but still... Anyway, that is why I suggested that a separate proposal for other access levels would be good. I don't think that it's necessary to delay this proposal though, because I don't agree that the introduction of the sigil notation is painting ourselves into a corner. (I was originally concerned about that but was convinced otherwise in a previous thread.)

Your softPrivate keyword could just be private (and would match other languages with that keyword + reflection capabilities), and hard private could continue to use #. Again, committee doesn't like this ... too many options for new developers.

If it were just me, I would be fully on board with this idea. I think hard private is something special and unusual (most languages don't have it), which is part of the reason I don't mind the unusual syntax for it. IMO soft private makes more sense a standard access level for most use cases. But I'm on the fence about using just private as the keyword for soft private, because the # fields are referred to as "private fields", so this could be confusing to people...

Come to think of it, maybe the official name for # should be something more descriptive to help disambiguate in the future...e.g. "hard private fields", "securely private fields", "fully private fields"...

PinkaminaDianePie commented 6 years ago

the main argument I remember seeing is that hard private is important to library authors.

And that's why this proposal is a disaster. Hard private is much worse than # itself, and it will be terrible if this proposal will progress to stage 4. The committee are guys from top world companies and they didn't understand all the problems other people have. They don't know what is it if you have few days to complete the project no matter of what and your only option is to take few libraries and glue them up, and if some of these libraries are not supported anymore, monkey-patching is the only solution. For these guys always possible to start maintain needed libraries or rewrite them from the scratch. They have enough money and capacity. But for a lot of people monkey-patching is the only solution for dealing fast with outdated libraries.

Who needs hard private? Top companies, who can produce good quality libraries and who can be sure that no one will needs reflection. Who will use hard-private? A lot of bad developers, who just heard somewhere that encapsulation is good practice so everything will be hard-private by default. So the average developer will gain nothing from that, but he will lose the possibility to deal with outdated code fast. But who cares? The committee doesn't care, they will say something like "stop working on such jobs where you forced to monkey-patch something, grow and go to better companies", but such suggestions won't solve anything - people won't become better developers in no time, people won't stop using outdated libraries, but they will lose time and money. Noone cares. Soft private could be enough because most of the people won't use reflection just because they can, soft private is already strong signal, people will use reflection mostly to workaround something when they don't have the capacity to start maintaining an external library. But what more important for these abstract "library authors" is to reduce noise in top repositories, in cases if some "hacker" changed some private field, broke something and put an issue to their repo. This is what they need. They just don't understand that there is 99% of people with different skills and capacity. Or they just don't care. So this proposal will help them but will give nothing good to us, average developers.

mbrowne commented 6 years ago

@PinkaminaDianePie I worked as a solo developer on low-budget projects for many years (through 2016) and while it's true that sometimes you just have to find a quick, less than ideal solution, I think you are blowing things out of proportion. I share your concerns about hard private to some degree, but on the other hand, if a library author has a good engineering reason to use hard private to prevent misuse (which sometimes becomes widespread if it's the easiest way to implement something) and to allow for freedom to change something later that's supposed to be very internal, then I think hard private is a good solution for that. And it's not true that forking a library or just making changes to a local copy has to be a big deal...if literally all you're doing is exposing a couple internal fields, it's a stretch to say that's maintaining your own version of the library...very little maintenance overhead. And more importantly, engineering decisions for the core language should not be made on the basis of what makes libraries more easily hackable, especially when we're talking about open-source libraries.

PinkaminaDianePie commented 6 years ago

if a library author has a good engineering reason

if. Keyword. But most of the developers ain't that good. Even library authors. And it also can be that you can't predict if you need to use soft private instead of hard. No one can. Even top companies like Google make such mistakes, like this situation with final class for the marker on android's google maps. Developers weren't able to create their own markers, change styles and there were tons of shitty workarounds to do something with that. And it was Google, now imagine average developer in the average company. Hard private will be a disaster. If hard private will be shipped, it will be a default access modifier for most of the libraries. Just because somewhere in university people heard that encapsulation is good, that's it. I can already imagine eslint rule "hard-private-default", which people will use everywhere. Dealing with soft private through reflection is not the easiest way, and if a reflection of soft private is the easiest way of interaction with the library - for sure this library is not that good and no one had any engineering reason when this library was implemented. I could believe that you worked on low budget projects, but they probably were not that bad as it could be. In a lot of companies, even big one, senior developers have no idea even about such things as rebase, because they never contributed in any open source projects. In such companies, maintaining anything external would be extremely hard. And such companies and such developers are much more common than you think. As for decisions for the core language - js was hackable for years, and it was one of its huge benefits. One of the reasons why it became so popular - maximum functionality with minimal restrictions. If you would target only features which needed to top world's companies but could make life worse for the average developer - it's not also so good way. Top companies are not only users of JS and targeting only them, making assumptions like "if everyone will be skilled enough", is for sure wrong way. There will be a lot of not that good developers, but they will use hard private fields, make libraries with them, and other average developers will use these libraries, just because they have no choice. Introducing hard private won't make average developer skilled enough to predict all use cases and maintenance of their libraries in the future.

bakkot commented 6 years ago

As for decisions for the core language - js was hackable for years, and it was one of its huge benefits.

I've never really understood this argument. JS has never offered the ability to introspect on closures, which is a more fundamental feature than class fields and which is the privacy model on which this feature is based.

PinkaminaDianePie commented 6 years ago

I've never really understood this argument. JS has never offered the ability to introspect on closures, which is a more fundamental feature than class fields and which is the privacy model on which this feature is based.

What if I will tell you that not even every JS developers know about closures? :) Some people use JS for years and use only OOP-related features. That's why JS was hackable - most people used objects with underscored properties like _privateData but not closures. And right now some people will change it to hard private.

glen-84 commented 6 years ago

@mbrowne,

I'm not sure that's really true. I don't think I've seen committee members argue against other access levels...

I was referring to them (most of them, at least) disagreeing about soft private being the better option (or at least having it as an alternative).

Come to think of it, maybe the official name for # should be something more descriptive to help disambiguate in the future

Been there, done that. 😛

littledan commented 6 years ago

@PinkaminaDianePie For hackability, I'm wondering how bad it is to fork your dependency. In some situations, that's just necessary anyway--some function needs it's behavior to be tweaked just a little bit, and there's no other way to do it even with monkey patching or accessing internal APIs. Many libraries are on both npm and GitHub; npm makes it easy to reference a local fork through npm link. Would this meet the use cases you've run into?

PinkaminaDianePie commented 6 years ago

Would this meet the use cases you've run into

Partially. Right now I mostly do what have you proposed, but when I had about ~2 years of experience, forking and especially building the external library was too complicated. I know some developers, who never contribute to open source but have enough (5+ years) experience to work on real projects. For such people, forking could be also problematic. There are also some people in 3rd worlds country with a monthly salary of about $300, and for them, every minute, spent on anything external is too expensive. I can imagine tons of cases, where forking is much more complicated for people than "hacking" js. But I can't imagine an even single case, where hard private would be better than soft. Soft private already provide a strong signal, it's already complicated to access such fields (because you need to use reflection), but if someone really needs to access hard private, he will found a way, like forking library (and had much more bugs, because it will become outdated), or not monkey patching one field but replacing whole class if possible (what will force developers to use hard privates just everywhere). So for sure, hard private is not a solution to anything, but a problem to some developers.

mbrowne commented 6 years ago

I agree with most of what @PinkaminaDianePie said except for his assertion that forking or using a local copy of a library is an unacceptable solution, even for novice developers who are also short on time. As @littledan pointed out, there have always been some variables that have been inaccessible due to closures anyhow, and library authors already have the ability to use closures now to make things truly private if they want to. This proposal simply provides a more elegant way to do that that works together with classes. To me the big concern - and here is where I agree with @PinkaminaDianePie - is that hard private will become the default choice for non-public fields, and we won't fully experience and understand some negative consequences of that until the practice is already widespread.

Even after the release of decorators, I am sure many developers would mark their fields as private with # and not bother to decorate them even if the field should really be soft private and not hard private if they thought about it. # will simply be the path of least resistance, so anyone who has heard about the usefulness of encapsulation will tend to default to hard private. I'm sure some developers would only expose the field for reflection when some problem occurs or some use case they didn't foresee. This is fine for local dependencies but obviously more problematic for third party libraries in cases where the library author isn't thinking that much about extensibility (especially in early versions). I can see the other side of this argument as well but I do agree that the vast majority of the time, soft private would be just as suitable as hard private for the purposes of encapsulation, especially if accessing the soft private field is inconvenient.

While it wouldn't completely prevent these problems, one thing I think would help would be the ability to easily enable all the fields in a class for reflection. Maybe a class-level @enableFieleReflection decorator that's part of a standard decorator library. Or maybe even a class modifier keyword like inspectable class ...

rdking commented 6 years ago

@mbrowne Is the argument really that "hard private" will make things too difficult to force a 3rd party library do something it wasn't designed to do without modifying the library at development time? Is this really a good argument? That's like saying:

A clock radio isn't a telephone, but if I could just tweak the frequency tuner to a mobile band, I can make it act like one.

Why is this even considered a good thing? New functionality should be created by extending or forking. Runtime hacking just leads to unexpected security holes. Isn't plugging such holes one of the reasons for wanting "hard private" in the first place?

PinkaminaDianePie commented 6 years ago

A clock radio isn't a telephone, but if I could just tweak the frequency tuner to a mobile band, I can make it act like one.

But what if you will be on the desert island, without a phone but with radio? Will you try to tune the radio and send the message somehow or you will say that radio was designed for something else and it's not secure to change internals? But when you'll decide to try, you will understand, that radio developer put a hard lock on changing frequency, because of he afraid that you will try to use radio as a phone somehow, but he thinks that you need to extend it, not modify :D

This is the problem. People think that they can predict all use cases. But it's impossible, no one can predict that even top world's companies, so what about the average developer, who will use hard private by default? You think only about your use cases, about your company processes, about good practices etc. But you don't care about other people's cases, there are tons of not so good companies, tons of legacy code, tons of people who have only a few days to create simple version of the product and they don't have time or(and) knowledge to maintain fork of any particular library, they just need to finish product as fast as they can. You can think anything about quality, security or good practices, but you won't change the world - such people and such projects would still exist, and hard private will make their life harder. Without any kind of benefits.

ljharb commented 6 years ago

@PinkaminaDianePie code don’t have to address all use cases. The inability to predict how it will be used is exactly why it’s best to restrict, as tightly as possible, how it can possibly be used. If someone wants to use one of my libraries in a novel but impossible way, but is under such time constraints that they can’t file an issue explaining the use case (some things can be implemented and released in minutes, or hours, let alone in days), then they probably are best served either using another library (almost anything you could want has 3 or 4 alternatives on npm already), forking, or, implementing something themselves.

PinkaminaDianePie commented 6 years ago

@ljharb I have no doubts that you will do exactly like that. But why do you decide for others? Why do you force others to fork and maintain instead of monkey-patching? If some developer needs to implement some feature in 4 hours and the customer will pay exactly for this 4 hours, not for any overtime, will it be acceptable to fork and maintain external library? Implement by themselves? Spend additional 1 day without receiving any money for that? I wouldn't raise such concern, but I really worked in such kind of companies when I had fixed amount of money on the project, and any overtime was just my problem. And I had only 2 options: monkey-patch something and finish the project on time, or spend an additional day on creating the fork, understand how to build it, fix my issue etc. So such people need to lose money only because of that? Some libraries don't have any alternatives and sometimes npm doesn't have such kind of library at all - in that company we got used to spawning child process and run some linux utility like ImageMagick or FFMpeg. Don't try to predict other's people use cases, it's just impossible. Soft private is strong enough signal for everyone. If anyone will still access soft private variable via reflection, it means that result is totally on his responsibility - he probably knows what does he doing, but even if don't - you already provided him signal to stop. Such approach would be most flexible. You would provide signal, but leave opportunity to change behaviour in some corner cases which you can't even expect.

bdistin commented 6 years ago

New functionality should be created by extending or forking.

I believe the point being made is, with only the choice of full public and full private; developers will use full private for things that should semantically be protected, eliminating new functionality from being created via extending. The only choice would be to fork, or petition libraries to keep parts public until a proper middle ground is offered.

IMO, there is a time and a place for a Hard Private, but those few and far between. Most of the time, methods/fields should be protected or public, so that you can create new functionality through extension.

TBH, when I think about Hard Private, I think about securing API tokens. That's clearly something you don't want to be exposed at runtime. But, Hard Private is only half the answer to fix that. The question comes to, how do you even set the private field to the token? Obviously, you need some sort of public way to pass in the login/token; and whether you pass it in through the constructor, or pass it in through a public method, both are exposed when extending. So until we have something like final login(token) { //... (final methods/classes) where they cannot be extended without throwing errors I don't see a secure way to even set Hard Private fields.

ljharb commented 6 years ago

@PinkaminaDianePie Because if i wrote the code - i get to license it, and i get to decide what it’s api and semantics are - it’s my place to decide it for everyone else. (As an OSS maintainer, i would of course strive to do so such that it benefits everybody, but that doesn’t change that it’s my decision to make)

Soft private is not a strong enough signal - npm even broke node once by removing an underscored field that node was relying on. It is dangerous to rely on unintentional semantics or behavior - because it is likely to break out from under you, possibly in a silent and subtle way.

@bdistin imo, the thing that should be few and far between is making things public. If the author of the code didn’t intend the use case, the proper path is to ask the author to support it, and to wait for them to choose to do so explicitly - not to dangerously rely on accidental implementation details.

PinkaminaDianePie commented 6 years ago

@ljharb But you decide not only about your libraries, you decide for everyone else. If hard private fields will be shipped, it will become a default choice, so every library will use it. So if I will need to use an outdated library, I won't be able to monkey-patch it, but not because library author against it - he probably doesn't care if his library is outdated, it's because committee thinks that it's not secure. Just imagine, one guy who you don't know will try to quickly fix something in the outdated library of another guy which you also don't know, but he won't be able and lose some money because of you. That's what I mean when I saying that you decide for others - if hard private will be shipped, it can lead to problems in the code of people who we don't even know, and who don't know anything about us and about this discussions.

ljharb commented 6 years ago

@PinkaminaDianePie yes, it is the job of the author of code to decide how it works for everyone else. The problems you’re talking about already do happen without private fields - the solution is to file issues on the library that you’re having trouble with, to share the benefits of a solution with everyone - instead of to secretly hack around it and deny the rest of the community your use cases.

zenparsing commented 6 years ago

It seems to me that the debate isn't so much about whether hard private is a valuable thing, but rather about whether we ought to be directing users toward hard private with premium syntax (where the implications of that syntax might be misunderstood).

glen-84 commented 6 years ago

Soft private is not a strong enough signal - npm even broke node once by removing an underscored field that node was relying on.

Soft private != underscore-prefixed variable names, and Node should not have been relying on private-by-convention state.

If the Node developers had jumped through various hoops in order to access that field, it may have been a different story.


This issue is getting really off topic. Perhaps the Hard-private vs soft-private issue should be re-opened (or a new issue created) ... that is assuming that the committee is still open to feedback on the matter.