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

[Suggestion] Consider to use time proof solution with private and protected keywords #315

Open redradist opened 4 years ago

redradist commented 4 years ago

Hi,

From my point of view it seems like mistake to use # for private fields, because it is not extensible ... If in future we would want to use protected field, it will make harder to implement it ... -# ??!

I think we should not reinvent the wheel and just to use time proof solution with private and protected keywords ;)

bakkot commented 4 years ago

Please see the FAQ.

rdking commented 4 years ago

@bakkot I know it's been discussed to death, that's why I'm not going to argue with you. However, if at all possible, I would like an explanation. From the FAQ:


Why aren't declarations private x?

This sort of declaration is what other languages use (notably Java), and implies that access would be done with this.x.


The part in bold is where I've always had issue. ES is a language where both a.b and a["b"] have always meant "public access". Why would the introduction of private x in such a language suddenly cause a.x to refer to private access? To my understanding, such an implication would only be possible if the presence of private x also restricted the possibility of a "public" x declaration. Since that is not the case (due to the desire for full encapsulation), on what grounds is it reasonable to claim the implication in the FAQ? I already understand the argument that this is how it is in other languages supporting the use of private x, however I believe that argument invalid as in those languages, the aforementioned restriction exists as well.


As I stated before, I will not argue. I'm only looking for the justification for this supposed implication.

ljharb commented 4 years ago

I think your questions are about “why would seeing this.x make someone think it’s private” - which i agree, It wouldn’t! To me, it’s that seeing “private x” would suggest to someone that they could access the private data with “this.x”, which of course they couldn’t, which would be very confusing.

rdking commented 4 years ago

@ljharb

think your questions are about “why would seeing this.x make someone think it’s private”

Um, no. My question is exactly as I stated it. It's about why "private x implies this.x", especially given that both a private member and a public member can be defined for the same instance object. Since it's already set in stone that this.x is public, and that will never change without breaking the language, why would any new notation suddenly suggest that this.x can be non-public?

ljharb commented 4 years ago

@rdking because i don't think people think about public/private in that way. I think if they see private x inside a class, they'll assume that this.x = privateData is setting the data privately. The risk of doing this is that private information could be easily made public, which can have disastrous encapsulation consequences.

redradist commented 4 years ago

@bakkot I know it's been discussed to death, that's why I'm not going to argue with you. However, if at all possible, I would like an explanation. From the FAQ:

Why aren't declarations private x?

This sort of declaration is what other languages use (notably Java), and implies that access would be done with this.x.

The part in bold is where I've always had issue. ES is a language where both a.b and a["b"] have always meant "public access". Why would the introduction of private x in such a language suddenly cause a.x to refer to private access? To my understanding, such an implication would only be possible if the presence of private x also restricted the possibility of a "public" x declaration. Since that is not the case (due to the desire for full encapsulation), on what grounds is it reasonable to claim the implication in the FAQ? I already understand the argument that this is how it is in other languages supporting the use of private x, however I believe that argument invalid as in those languages, the aforementioned restriction exists as well.

As I stated before, I will not argue. I'm only looking for the justification for this supposed implication.

@rdking Disagree with you, because if you say why private x should cause a.x to refer to private access, then why a.x can always cause to return undefined, but setter still will work:

let obj = {
  get x() {
    return undefined;
  },

  set x(value) {
    ...
  }
};

And from user point of view it can seem like x has special behavior and it is !!

Also adding to name # break SOLID, Single responsibility Name in such case serve two separate goal - access modifier and name of property ... It is not extensible enough, because what if in future you will add another modifier, how it will look ... ??? Like this -#x or like this %#x ...

There is reason why other languages use private, protected ... because it shows intent directly that this field is private ...

Anyway we can argue about it, but actually I use such convention in Python like _ and __, but it has drawbacks that it is not obvious that this name has special meaning ...

redradist commented 4 years ago

@ljharb

think your questions are about “why would seeing this.x make someone think it’s private”

Um, no. My question is exactly as I stated it. It's about why "private x implies this.x", especially given that both a private member and a public member can be defined for the same instance object. Since it's already set in stone that this.x is public, and that will never change without breaking the language, why would any new notation suddenly suggest that this.x can be non-public?

@rdking Acctually I do not understand why a.x access is backed into language that it always should be accessed ?

rdking commented 4 years ago

@ljharb

i don't think people think about public/private in that way.

So to be clear, it's just an assumption?

rdking commented 4 years ago

@redradist

Disagree with you, because if you say why private x should cause a.x to refer to private access, then why a.x can always cause to return undefined, but setter still will work:

2 things:

  1. I never said that private x should cause a.x to refer to private access. I did, however, state that this is an implicit assumption in the presumed implication. I think it's rather off-putting of TC39 to have such an assumption buried in their justifications.
  2. I don't understand what a pair of accessor functions have to do with whether or not a.x is private.

Acctually I do not understand why a.x access is backed into language that it always should be accessed ?

If you're asking why a.x must always be public, the short answer is that it doesn't have to be. If TC39 had not adopted the stance of wanting full encapsulation for private fields, then by simply disallowing the existence of a public field with the same name as a declared private field for any given object (as is the case in most languages supporting class), it would be possible to allow a.x to access the only x available on a.

That's not what they chose, and their reasons are sound enough. I don't necessarily agree with the notion of weakening a language to protect against sloppy or bad programmers, but I also cannot find fault in their reasoning. Since TC39 chose full encapsulation, regardless of what the private field definition token looks like, there's no way that the access expression can ever be a.x for a private field.

ljharb commented 4 years ago

@rdking anything that's based on people's possible mental models is an assumption. Assumptions are fine, anything that involves human thought can't truly be objective.

rdking commented 4 years ago

@ljharb

IMO, assumptions have their use. Justification is not among them. This is even more true when designing a very popular, widely used language. But maybe that's just me.

trusktr commented 4 years ago

What if #foo was kept as "hard private" like it already is, and then a new private foo was a softer alternative introduced later (where this.foo can only access one particular instance variable with a certain access level)? Would that be worth having in addition?

ljharb commented 4 years ago

Why? The word "private" is wildly inaccurate if it's not "hard private".

rdking commented 4 years ago

@trusktr Unfortunately, that would introduce confusion in the extreme. To make something like that work, a class wouldn't be allowed to have both private x and #y. You'd be forced to use either one or the other. It's not that it's not possible to do both in the same class, but that invites confusion. There's also the problem that if private is used, it would need to be impossible to add new members to an instance that have the same name as a private member. In short, while it could be done, it just doesn't seem like a good idea to have both notations.

@ljharb

Why? The word "private" is wildly inaccurate if it's not "hard private".

Um... your absolutism is showing. Think about it like this. If I have a letter in my hand, and I successfully keep you from snatching it away, the contents of that letter are private to me. Who cares if you can see it in my hand. This is what private means to most developers. The only ones doing something screwy that violates your idea of private are those who are doing non-language related things like trying to get around sloppy developers who don't maintain proper versions on their shared code. That's not an issue for the language, but for the shared code developers. While I understand that it's still a pretty big issue, I still can't say that it's within the domain of the language itself to fix (unless the language dictates a library format).

redradist commented 4 years ago

Why? The word "private" is wildly inaccurate if it's not "hard private".

@ljharb It does not matter that you do not like "private" keyword or it has inaccurate meaning ... TypeScript already use it, this keyword has been widely used in ecosystem and adopted by other languages ... For me those "+"-es more reasonable than it may have inaccurate meaning, because someone think such ...

If you will introduce new private field variable it will split ecosystem again ... You are trying to design language but it is already designed by TypeScript team mostly in good direction ... Just standardize TypeScript (without "namespaces") ;)

Also even if you will implement private fields like in this proposal how to implement than protected modifier ? Like this _#x, -#x ... ?

Instead of thinking about some minor features like private fields, it would be better if TC39 improved current language by removing at least from language implicit type coercion ...

trusktr commented 4 years ago

To make something like that work, a class wouldn't be allowed to have both private x and #y

@rdking Did you mean #x there? I don't see why both aren't allowed (they are both allowed in TypeScript, and it would carry over well, for example). Here's a simple example:

class Foo {
  #x = 123
  private x = 456
  // x = 789 // this would be a runtime error, private x already declared.
  test() {
    console.log(this.#x) // 123
    console.log(this.x) // 456
  }
}

const f = new Foo
// f.#x // this would be syntax error like it already is.
// f.x // would be a soft-privacy runtime error, "x is private in Foo"
// f.x = 987 // would be a soft-privacy runtime error, "x is private in Foo"

class Bar extends Foo {
  // private x = 123 // would be a runtime error, private x is already defined in Foo
}

Other OO languages have soft privacy; there is precedent for it.

Why? The word "private" is wildly inaccurate if it's not "hard private".

@ljharb Saying that we must have only hard privacy is merely an opinion.

Why don't we have both options and let developers choose, and make their own opinions?

trusktr commented 4 years ago

Or even better: why not take the opinions of non TC39 developers into higher consideration based on the high number of ignored votes they've placed?

trusktr commented 4 years ago

In short, while it could be done, it just doesn't seem like a good idea to have both notations.

@rdking Yeah, ideally we would have only one thing and it would be better than the current, but I think it might be nice to have another option (TypeScript already has both) if we can't reverse the current state.

If the private x version existed too (and even if it behaved like a normal prototype property except with restrictions on where it is accessed), I would find that to be very awesome and very usable and would prefer it over #private fields any day.

trusktr commented 4 years ago

Furthermore, if I really really wanted people to see a property's usage site and immediately know the property to be private, I could easily adopt a double-underscore-prefix convention for that. F.e.

class Foo {
  private __foo = 123

  method() { console.log(this.__foo) }
}

(I already use this convention in all of my code!)

ljharb commented 4 years ago

@rdking something being "absolutism" doesn't invalidate it, unless you're absolutely against absolutism :-p "I successfully keep you from snatching it away" would require also denying me any means of reflection, which is indeed what "hard private" means.

@redradist please read through all the closed issues in this repo, in which "protected" is discussed in great detail.

@trusktr we don't operate on the basis of votes; every opinion voiced here has been considered, as far as I can determine.

Why don't we have both options and let developers choose, and make their own opinions?

Great question! That's already possible. Developers can, and already do, use a convention (leading underscore), or Symbols, or documentation, to mark things as "please don't touch". What's missing is the capability to have "hard private" for instances without jumping through WeakMap/WeakSet hoops, which is what this proposal provides. For "soft private", I'm not sure a compelling case can be made that special syntax (something that has a high cost to add) should be added that can do a better job, or be easier to write, than _foo() {} or [Symbol.for('foo')]() {} or similar. However, "hard private" is a new low-level capability, which is often sufficient to warrant adding new syntax.

rdking commented 4 years ago

@ljharb

"I successfully keep you from snatching it away" would require also denying me any means of reflection, which is indeed what "hard private" means.

On this, we agree, almost. I think it would be better to say that "I successfully keep you from snatching it away" only requires an impervious defense against any attempt to access. On this point we, need to be clear. Reflection is the technique by which the structure of a thing can be revealed. However, there's nothing specific to the concept of reflection that allows you to access what has been revealed. Conflating revelation and access is problematic to say the least.

What's missing is the capability to have "hard private" for instances without jumping through WeakMap/WeakSet hoops, which is what this proposal provides.

You missed a few things. Also missing are:

  1. the ability to extend privacy via the prototype structure (i.e. protected)
  2. the ability to encapsulate soft privacy.

One of the many things that have bugged me throughout this process has been the mischaracterization of the concept of "soft private". If "hard private" means to be both hidden and encapsulated, then shouldn't "soft private" at minimum mean encapsulated"? If so, then Symbol and approaches like the _ convention are not "soft private" at all. These things are simply public as they always point to a publicly accessible property. So to say something like:

That's already possible. Developers can, and already do, use a convention (leading underscore), or Symbols, or documentation, to mark things as "please don't touch".

while trying to characterize "soft privacy" seems to miss the mark by quite some distance. If it's not encapsulated, then it's not "private" at all.

rdking commented 4 years ago

@trusktr

@rdking Did you mean #x there? I don't see why both aren't allowed (they are both allowed in TypeScript, and it would carry over well, for example). Here's a simple example:

It's just personal opinion, but for me, having them both accessible in the same class just invites potential confusion and errors. Having both "hard" and "soft" privacy in the same language would be good as it invites flexibility. Allowing both to be expressed in the same class feels like a recipe for disaster. My thinking is that for the sake of sanity, usage of "hard" or "soft" privacy on a per-class basis should be consistent. But that's just my 2 cents.

ljharb commented 4 years ago

Given that, unlike many languages, in JS, a subclass can be created dynamically and at any time, anything "protected" is indistinguishable from "public".

redradist commented 4 years ago

Guys, why private in JavaScript could not mean hard private ? I do not think it is needed to have "hard" and "soft" private ... there are lots of confusion ...

rdking commented 4 years ago

@ljharb

Given that, unlike many languages, in JS, a subclass can be created dynamically and at any time, anything "protected" is indistinguishable from "public".

You paint with such broad strokes! What you're failing to understand is that it is not an issue of security as is the case for "private". Sure, someone could dynamically create a subclass and gain access to the "protected" members. But so what? That doesn't make them public. It only makes them accessible from within a given scope that just happens to be broader than the scope for private, but more restricted than the scope for public... which is exactly what it's supposed to be. No surprise there.

When all is said and done, I don't care about "protected" that much. I've got bigger issues with how "private" is going to be implemented. I won't bother re-hashing them since that's just beating a dead horse. I'll just say this...

Where this proposal is concerned, all we really need are 2 things:

  1. encapsulation that doesn't break any other feature of the language when used together, and
  2. the ability to choose whether the former is "hard" or "soft".
rdking commented 4 years ago

@redradist

Guys, why private in JavaScript could not mean hard private ?

There's no reason it can't. It all goes back to the FAQ and that poorly based assumption I addressed here.

1316346949 commented 4 years ago

“#” so bad

trusktr commented 4 years ago

Having both "hard" and "soft" privacy in the same language would be good as it invites flexibility. Allowing both to be expressed in the same class feels like a recipe for disaster. My thinking is that for the sake of sanity, usage of "hard" or "soft" privacy on a per-class basis should be consistent.

That would be totally fair allowing a class to have only one form; it would give users of the language a better set of options.

Dear Santa, please give me protected and private for one of these holidays.

glen-84 commented 4 years ago

@redradist #100 @trusktr https://github.com/tc39/proposal-private-fields/issues/33#issuecomment-232457083 @ljharb https://github.com/tc39/proposal-private-fields/issues/33#issuecomment-309202710

rdking commented 4 years ago

@glen-84 Reading through those posts just reminded me of the likely reason we're in this debacle in the first place. Some library authors of popular packages want to be able to lock developers out of the internals so that there's less risk of breaking downstream packages when updating the internal logic of the libraries. It just hit me again how silly this argument is, and how simple the solution can be without getting us into the mess private fields is bringing.

  1. Library authors should recognize 2 things when developers hack into internals: a. Yes it's a pain, but it likely happened due to the potential need for new API in the library. b. They already likely know they did something ill advised. In this case, go ahead and break them. Sure they're going to be irritated (as well as their users), but as long as you've been properly versioning your library, and you provide an API for properly getting at what they needed, your library will only become more powerful as a result.
  2. API changes break downstream code all the time. If you're not properly versioning your library, that's not the fault of the language or downstream developers if you get complaints or find that some popular downstream package has hooked in somehow just to detect a version revision.
  3. Programming languages are suppose to be usage agnostic. As long as it's legal for the language, it's not supposed to matter whether you're writing a program or a library. Sure, it's in a programmer's nature to use whatever is accessible to accomplish their goals. However, being too aggressive in trying to block some of those possibilities on a language level only serves to cripple some portion of the language.

It's a shame that these 3 points do not seem to coincide with the understanding being applied to this proposal.

rdking commented 4 years ago

@ljharb Knowing that you disagree with my post, I'd like to know why, either in a different thread or on discourse. Understanding why anyone in TC39 can't agree with this kind of understanding is critical to understanding the mindset of those who would champion or even advocate new proposals.

trusktr commented 4 years ago

Why not give most of us the less-hard privacy with the private keyword (still inaccessible at runtime even if the existence is clearly known, as described above), then let those who really wish to have the harder privacy to simply go out of their way to use WeakMap?

Supporters of this proposal (a minority) tell the majority of people to go out of their way to use features like Symbol (which is inconvenient and more verbose) if they want soft privacy. But that's not what the majority wants.

A flip of priorities seems like the logical solution: opposite of the current proposal, and based on community feedback and not a result dictated by a mere handful of people.

Or better yet, give us both, as described above, so that the majority of people can have a decent option, while those few can still use the current proposal's features if they really need to.

Jamesernator commented 3 years ago

I've been thinking about the whole "protected" thing and realized that actually there is kind've a way for protected to work that isn't accidentally public. I realized this because this pattern is somewhat already present with the Promise constructor:

class SubPromise extends Promise {
  #resolve;
  #reject;

  constructor() {
    // Resolve and reject are essentially protected state, instances are given
    // copies and can do what they want with them (including making them public)
    // however even with this I can't capture resolve/reject from an arbitrary instance
    super((resolve, reject) => {
      this.#resolve = resolve;
      this.#reject = reject;
    });
  }
}

The main problem with this style of initializer pattern is it can be hard to add to existing APIs after the fact. Something like protected would help alleviate these cases where you want to expose a bunch of stuff to the subclass, but don't have anywhere to put it (e.g. due to using variadic args to constructor).

For example:

class SomeClass {
    protected #newHook;
    #data;

    constructor(...data) {
        this.#data = data;
    }
}

class SomeSubClass {
  constructor() {
    super();
    // Because it's on super not this, there is no way inside SomeSubClass we can access
    // protected properties of an instance of another subclass of SomeClass
    super.#newHook();
  }
}
ljharb commented 3 years ago

@Jamesernator the pattern of passing hooks up through super is viable, but we don’t need any new features for that - just a convention. Your second example doesn’t really work because a subclass can be created at any time, both of SomeClass and also of SomeSubClass.

Jamesernator commented 3 years ago

Your second example doesn’t really work because a subclass can be created at any time, both of SomeClass and also of SomeSubClass.

Other subclasses of SomeClass can't access protected properties given to SomeSubClass, however you're right that subclasses of subclasses would need to be considered. Explicit forwarding would be the only safe way.

Also I actually realized passing hooks like in my Promise example doesn't work, it throws a type error because the private fields haven't been initialized when the initializer is called.

class SubPromise {
  #resolve;
  #reject;

  constructor() {
    super((resolve ,reject) => {
      // Oops #resolve isn't created yet as super() isn't finished, so this throws a TypeError
      this.#resolve = resolve;
    });
  }
}

The real pattern would have to be:

class SubPromise {
  #resolve;
  #reject;

  constructor() {
    let resolve_;
    let reject_;
    super((resolve, reject) => {
      resolve_ = resolve;
      reject_ = reject;
    });
    this.#resolve = resolve_;
    this.#reject = reject_;
  }
}

This is a lot more awkward and error prone to deal with than the strawman protected #state I proposed above.

rdking commented 3 years ago

@Jamesernator If you're looking for a way to do protected state that isn't accidentally public, not only is it possible, I showed and early prototype of it to @ljharb, and through his feedback actually worked out how to solve the direct issues he pointed out. I've been using the results of that in my own projects since then. It's a small solution but not exactly simple. A full example of the logic for this is here. This approach doesn't involve passing anything through the constructor. Further, if it were implemented in-engine some of the complexity could be reduced due to having access to internals.

Note: At the time of that discussion, despite the functional logic, @ljharb still maintained a strong stance again inclusion of protected.

crapthings commented 3 years ago

it looks # is used for this record and tuple

https://github.com/tc39/proposal-record-tuple

class Something {
  #test1 = #[1, 2, 3]
  #test2 = #{ hello: 'kitty' }
}

so this look not so good

ljharb commented 3 years ago

That’s an argument against Records and Tuples, not against already-shipping stage 3 class fields syntax.

trusktr commented 3 years ago

A full example of the [protected] logic for this is here.

The "No peeking at your cousin's privates!" log statement in the catch block. LOL