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.

ljharb commented 6 years ago

Agreed! However, the massive value of hard private is why we should be directing users there.

rdking commented 6 years ago

@PinkaminaDianePie If I were stranded on a desert island with a radio and had the tools to make the mod, I'd tweak the radio to send morse code pulses on a heavily used radio band. Notice that I'd be modifying the device, then running it, not trying to change it's operation while it's running. No need to mess around with the frequency tuner beyond it's spec. Most times, when you push a device beyond what it's engineered to handle, it breaks.

You're right in that it's impossible to predict all use cases. That's why when you design something, you design generically and leave room for others to extend what you've created to make more use cases beyond what you considered. This is what I do, and the reason why code I wrote some 20 years ago is still in use, mostly untouched, and being extended into new uses. Complaints from users about things that have been made hard private will drive developers to do 1 of 3 things:

  1. abandon project control
  2. make the project more generic
  3. abandon private for certain cases

This is a GOOD THING. Almost anything that decreases the proliferation of poorly designed code is good in my book. Consider this: one of the biggest pain-points for Microsoft Windows is that they are almost forced to take an otherwise clean OS and put in bad hacks to make old software that used non-api endpoints work again. This is why the OS has suffered so many stability and vulnerability issues. This is what you're promoting, and what TC39 rightfully wants to eliminated by introducing "hard private" in an easier to use (albeit bad) syntax.

rdking commented 6 years ago

@bdistin

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.

This is one of the reasons why I believe this proposal should be halted until a protected implementation can also be provided. Having private fields without the ability to extend properly is almost wasteful of a feature. Besides, soft private doesn't make that any better for extending.

mbrowne commented 6 years ago

I suppose my opinion on this issue is somewhere in the middle...I think it's good design to default to private, and even if hard private were the only option, I'd rather have that than no true privacy at all. Also, over the years I have come to value making members private rather than protected in one's own codebase because many of these members may never need to be extended or customized, and allowing consumers to access them would just be a potential source of bugs or at least inflexibility when it comes to future implementation changes. And it's really easy to change them to protected if you realize some reason to do so later. With a 3rd party library, foresight is more important...I have definitely encountered situations where a library does 99% of what I want it to do, but because it wasn't officially designed to be extended, the best way to customize it is to override some internal class or property. If you pin the version number and document it well, I see no problem with this and usually it still works with future versions of the library as well unless the library authors did significant refactoring. It's something I try to avoid whenever possible but sometimes it's very useful.

But honestly I don't think this alone is a very compelling argument and I am not at all against the introduction of hard private into the language - I hope this proposal moves forward. I am just questioning whether in the long term, hard private is the best default choice for private fields. In addition to libraries, another thing that comes to mind is domain objects...reflection can be very useful for serializing them, ORM libraries, etc. Reflection can also be helpful for monitoring, debugging, metaprogramming, etc. I suspect that it's not just by coincidence that other languages make reflection available by default out of the box... If we do decide that hard private is the best default - especially if it requires more than one line of code to change that default for a given class - I think we should be very conscious of the tradeoffs and the reason for doing that.

littledan commented 6 years ago

I think it's reasonable to discuss what's the right default. I disagree with @ljharb's absolutist phrasing on this and many other discussions in this repository. This is a design trade-off, and a subjective one.

In my opinion, TC39 made the right choice for "hard private" by default, for the following reasons:

I want to note that TC39 discussed this subjective trade-off in several committee meetings, and there are delegates on both sides of the debate. We have also heard from both sides in the JS developer community.

mbrowne commented 6 years ago

Thanks @littledan. I think we have a viable path forward. I just hope we can ultimately find a more concise syntax for soft private than:

@reflect
#x = 1
@reflect
#y = 2
@reflect
#z = 3

etc... Maybe we already have the answer with class-level decorators. I don't know if that's the best solution, but it's one option. If anyone wants to discuss that further, maybe we should start a new thread.

doodadjs commented 6 years ago

I think it's good design to default to private, and even if hard private were the only option

That's better design to default to "protected", which is not covered seriously there.

EDIT: A "protected" field/method can be accessed by an inheriting class, while "private" can't. I don't believe finally we can make "protected" by using "private".

mbrowne commented 6 years ago

@doodadjs In addition to the considerations I mentioned above, inheritance very often makes code harder to read and understand - one of the reasons composition is often preferable. It should be used with care, and opening up everything to extension in such an officially supported way is usually not a great idea. To be sure, there are valid use cases for protected (including open source libraries - but only fields that actually need to be accessible by subclasses, not all) and ES should offer a way to enforce it, but it would not make a good default.

doodadjs commented 6 years ago

inheritance very often makes code harder to read and understand - one of the reasons composition is often preferable

I get it... You don't want inheritance, but you want composition. I have to remind you that composition is more dynamic than declaring classes or whatever with a "class" keyword !

Composition should be something like :

const myObj = Object.compose(anotherObject, {foo() {...}}, {bar() {}}, ...)

And telling that composition is better than inheritance is debatable....

EDIT: Changed ".extend" to ".compose" to be more explicit.

littledan commented 6 years ago

I just want to point out, protected is equivalent to allowing metaprogramming on private fields, in terms of expressive power; they differ more in ergonomics.

pleerock commented 6 years ago

I see people are writing const xyz = { a: b, #y: z } code examples in this issue, are private properties covered by this proposal? (such examples doesn't seem to appear in readme)

littledan commented 6 years ago

@pleerock No, they are not. It is a potential future proposal.

pleerock commented 6 years ago

@littledan can't we append to this future proposal? Because I guess waiting a year for the same feature to be completely applied to the language is a really bad idea

export class Car {
   #engine
   drive() {
      this.#engine;
   }
}

export const Car = {
   #engine
   drive() {
      this.#engine;
   }
}
littledan commented 6 years ago

@pleerock The big open question is, will this create a new private name on each evaluation, or will it be the same one?

The reason I have not pushed further on private fields in object literals is that most of the people I have talked with who advocate programming with heavy use of function literals rather than classes also told me they don't care about privacy boundaries, and most people who care about privacy boundaries have been happy with classes. I'd rather not blindly "complete the grid" until we understand the problem space better.

glen-84 commented 6 years ago

(off topic again)

pleerock commented 6 years ago

@pleerock The big open question is, will this create a new private name on each evaluation, or will it be the same one?

can you please explain what it means? I see that they should work just like regular properties but hided inside just like they would be local variables inside the object scope.

I think we should not create "separate JavaScript" for people who advocate functional side and another "separate JavaScript" for people who advocate classic oop side.

For me class is just another organization structure and an alternative to factory function that creates an object. The reason why I think private properties are needed in object literals because it feels consistent with class syntax plus it provides a structural benefits, we won't be needed to use local variables when we want to simply create and return an object literal.

In my mind classes in javascript should have all features regular factory functions might have, especially object destructing and object spread (which means traits/mixins for classes), and shouldn't just get all possible features from the classic OOP languages. JavaScript gives lot of opportunities for the innovations so Im sure we can make more things better then in other languages.

@glen-84 sorry dude!

littledan commented 6 years ago

can you please explain what it means? I see that they should work just like regular properties but hided inside just like they would be local variables inside the object scope.

When you declare #x in a class, it doesn't just mean that the field gets added to instances, but also there's a new internal "private name" created each time the class executes. The question is, when you have an object literal with a private field, is that different each time the object evaluates, or the same? You might want it to be the same when you have a family of functions that all manipulate the same objects with private fields.

I think we should not create "separate JavaScript" for people who advocate functional side and another "separate JavaScript" for people who advocate classic oop side.

I agree. I'm pretty sure we can make an extension of this proposal which works well with object literals. At the same time, I'd like to take this step by step. Private fields are equivalent semantically to WeakMaps, which can hold objects as keys. JavaScript classes do have some other additional behavior not available to objects, like super constructor calls; I don't think it's fatal that private fields now join that category.

rdking commented 6 years ago

@littledan @pleerock Ignoring the privilege level semantics, shouldn't

export const Car = {
   #engine
   drive() {
      this.#engine;
   }
}

behave the same way as

export const Car = {
   engine=Symbol("engine"),
   drive() {
      this[engine];
   }
}

? Barring any means of declaring the private name outside the object-instance/class (please DON'T allow this), every declaration should end up producing a new private name that just happened to be accessible as this.#engine from any member function present in the class/object-instance declaration. Doing anything else would cause even more consistency issues than this proposal is already proposing.

loganfsmyth commented 6 years ago

@rdking The key is @littledan's post

The big open question is, will this create a new private name on each evaluation, or will it be the same one?

Take an code with the semantics of classes.

function makeClass(){
  return class {
    #value = 4;
    getValue(){ return this.#value; }
  };
}

const Constructor1 = makeClass();
const Constructor2 = makeClass();

const inst1 = new Constructor1();

Constructor2.prototype.getValue.call(inst1);

that final getValue call will throw, because Constructor1 is an entirely different class that doesn't know anything about Constructor2. They happen to have been created by the same syntactic class declaration, but they were created by two separate evaluations of the declaration.

If we take that same expectation to objects, what should happen if someone does

const instances = someArray.map(item => ({
   #value: 45,
   sum(obj) {
      return this.#value + obj.#value;
   },
}));

Each call to the callback re-evaluates the object literal declaration, so following the semantics of class declarations, it is now impossible for sum() to work, because each object was created by a separate evaluation of the object literal, and so each #value field, while sharing the same name, are unique, the same way your Symbol example would.

That is acceptable for class declarations because class declarations can be used to create multiple instances, but for object literals that is not the case.

mbrowne commented 6 years ago

Allowing access to private fields across instances breaks encapsulation. For classes, brand checks and comparison methods were deemed important enough to break encapsulation as long as instances belong to the same class. I'm not convinced that there's a good reason to break encapsulation for object literals...

rdking commented 6 years ago

@loganfsmyth

That is acceptable for class declarations because class declarations can be used to create multiple instances, but for object literals that is not the case.

Nor should it be. Let me expand your example to an equivalent form. Then maybe you'll get why doing that is such a bad deal.

//Your version
//const instances = someArray.map(item => ({
//   #value: 45,
//   sum(obj) {
//      return this.#value + obj.#value;
//   },
//}));

const instances = someArray.map(item => {
   class Item {
      #value: 45,
      sum(obj) {
         return this.#value + obj.#value;
      }
   };
   return new Item();
}));

This is semantically equivalent code. Not exactly the same but has the same effect. The difference is now we can check for our silliness.

//false for any integer m & n: m!==n, m<someArray.length, n<someArray.length
(someArray[m] instanceof someArray[n].constructor)

The only difference between your code and mine is that I defined my returned object indirectly using a class. Other than that, those 2 code snippets have the same effect on the members of someArray. If you wouldn't expect my code to have the same private name for #value, then you shouldn't expect that yours would either. I want that each object declared this way should have its own private name for its private fields. That's why I used Symbol in my previous post (since each run of it produces a new value).

loganfsmyth commented 6 years ago

Nor should it be. Let me expand your example to an equivalent form. Then maybe you'll get why doing that is such a bad deal.

Sorry, I could have been clearer. I absolutely understand, but it wasn't clear that difference was clear enough in the earlier posts. This was my attempt to clarify, not argue any given point. As you've shown, the object behaves like multiple evaluations of the class Item { declaration, meaning there are separate #value fields, meaning the sum will always throw an error about nonexistant fields unless obj === this.

every declaration should end up producing a new private name that just happened to be accessible as this.#engine from any member function present in the class/object-instance declaration

It wasn't clear if you were expecting obj.#engine to work across instances created from two separate constructors created by separate evaluations of the class declaration. Now that you've clarified though, I believe we're on the same page.

MichaelTheriot commented 6 years ago

#1 it was considered but does not meet use cases the committee has long since committed to solving, and failed to achieve consensus.

#2 Yes, we discussed it in committee. The committee was not enthused.

Is anyone else in this thread learning of this just now? It is really confusing to me that a proposal that generated much enthusiasm (at least as it appears on GitHub) can be taken out to the pasture and silently put down, leaving everyone else wondering what happened. I am not as involved as I wish I were but it would be nice for more transparency?

mbrowne commented 6 years ago

@MichaelTheriot where are the above quotes from?

MichaelTheriot commented 6 years ago

@mbrowne Sorry, this is a long thread so I should have linked them. I've updated my comment.

https://github.com/tc39/proposal-class-fields/issues/100#issuecomment-395980521 https://github.com/tc39/proposal-class-fields/issues/100#issuecomment-396058906

curiousdannii commented 6 years ago

Were the requirements ever published? I haven't been able to find them.

littledan commented 6 years ago

Thanks for the comments above. In the end, we're moving forward with the #-based syntax and strong encapsulation boundaries of the current proposal. We still welcome clarifying questions, small tweaks, and documentation in this repository. We've thought about various larger changes, with the help of the community including the discussion in this repo, and decided to stick with the current proposal. For more details, see the README's Status section.

PinkaminaDianePie commented 6 years ago

I look forward to seeing eslint-forbid-private rule :D

ljharb commented 6 years ago

That’s already achievable with the no-restricted-syntax rule, and wouldn’t warrant its own rule. You’re always welcome to deny yourself the benefits of new language features.

PinkaminaDianePie commented 6 years ago

benefits of new language features.

I would be happy to avoid such benefits like with, var, switch/case, labels, and of course hard private. It's better to have no privates at all, than having hard private.

tasogare3710 commented 6 years ago

Congrats. js is dead.

hax commented 6 years ago

@littledan I don't think close this issue is helpful in any way. We'll see.

rdking commented 6 years ago

@ljharb

You’re always welcome to deny yourself the benefits of new language features.

When a "feature" is as unnecessarily disruptive as this one, there's little benefit to be had when compared with the work-arounds that will have to go along with its use.

hax commented 6 years ago

@ljharb

You’re always welcome to deny yourself the benefits of new language features.

copy from my early comment in other place:

It will break the community. Each js team will have a unfriendly meeting to argue whether we should use #priv. Consider there is already TS private, the things will be even much complex. And it's harmful to JS/TS interoperability.

The code name of ES6 is harmony, which is very meaningful. I believe we could learn something from the failure/success from past.

littledan commented 6 years ago

@hax Closing this issue is intended to reflect the state of the world better. I didn't lock the thread--feel free to keep discussing. But implementations are ongoing, and TC39 and implementers have taken the community feedback into account in deciding to continue along this path.

ljharb commented 6 years ago

@hax each team might have a meeting for any new JS feature, syntactic or API - whether it’s friendly or not is a function of the team dynamic, not of the feature itself. Separately, i hope you’re not suggesting that JS should be constrained by what TS chooses? TS is a separate language; choosing to use it is taking on the risk that it will deviate from JS (albeit that the TS team strives to mitigate the risk on your behalf with codemods and upgrade paths).

bterlson commented 6 years ago

I for one certainly do worry a lot about the effects our work has on the JS ecosystem, including on TypeScript and other tools that JS developers depend on every day to get work done. I think many of us on the committee feel similarly.

hax commented 6 years ago

@ljharb

each team might have a meeting for any new JS feature, syntactic or API

No.

According to my observation, most time JS programmers just choose a Babel preset-env or just TypeScript, and allow all features in the source code. Which means, when a new feature is available in Babel/TS, you just can use it without meeting. Of coz, there may be some cases that we need to disable some features for some reason, but it's postmortem and rare.

Copy from https://babeljs.io/docs/en/babel-preset-env

@babel/preset-env is a smart preset that allows you to use the latest JavaScript without needing to micromanage ...

whether it’s friendly or not is a function of the team dynamic, not of the feature itself.

As many already said again and again, the #priv is very different, there is no other stage-3 proposal get so much disagreements broadly, continuously, drastically.

I really hope you would not ignore this fact again.

hax commented 6 years ago

@ljharb

Separately, i hope you’re not suggesting that JS should be constrained by what TS chooses? TS is a separate language; choosing to use it is taking on the risk that it will deviate from JS

In theory, I do not necessarily disagree with that. But, ironically, in some other cases, I see the arguments that we should keep align with the precedents of TS/Babel... blah blah blah.

[For example, when discuss public field (the questions like: should this feature be postpone? or should we add keyword for declaration), many said TS/Babel already support it in such syntax several years! We always use such feature in React/NG... So we should land it in this syntax now.]

I hope you do not use double standards.

Note, one of the hidden forms of double standards is: When you disagree a idea, you use your standard to dismiss it. When you agree it, you keep silence even it do not match your standard.


What I believe is, discussion of language feature is very complex, you need to consider all aspect and try to balance them. Simply say "TS is a separate language; choosing to use it is taking on the risk that it will deviate from JS" whenever you don't want TS/JS related problem to bother you, is not a healthy way for communication and discussion.

nicolo-ribaudo commented 6 years ago

Babel and TypeScript are two different things: Babel implements ECMAScript proposal trying to match exactly the proposed semantics, while Typescript isn't JS and thus doesn't need to implement a feature as it is proposed, it can do it in any way.

In TS, you use public to declare fields:

class Foo {
  public x = 1;
  private y = 2;
}

With Babel, that code would look like this:

class Foo {
  x = 1;
  #y = 2;
}

TypeScript doesn't give real feedback about a proposal, while Babel does.

bakkot commented 6 years ago

For example, when discuss public field (the questions like: should this feature be postpone? or should we add keyword for declaration), many said TS/Babel already support it in such syntax several years! We always use such feature in React/NG... So we should land it in this syntax now.

FWIW, I've never said this, and I try to push back against it when I notice the argument being made. Usage of features in Babel and TypeScript very often is a good indicator that people find a feature useful and not confusing (or that they do find it confusing, for that matter), but is not sufficient reason to add something or to pick a particular behavior over another.

We also recognize that TypeScript has different constraints than we do. In particular, TypeScript a.) has a fairly strong static type system and b.) does not want to implement any features which would be hard to transpile to ES5.

pleerock commented 6 years ago

you guys get a wrong understanding on what typescript is.

littledan commented 6 years ago

My understanding is that TypeScript has a pretty strict policy these days of not implementing new, additional non-type features which are not Stage 3+ in TC39. Maybe @DanielRosenwasser can clarify.

slikts commented 6 years ago

TypeScript does not have runtime privacy; TypeScript's privacy modifier refers only to the type level, and that is by design, as it's actually a not-uncommonly requested feature to, for example, make the private modifier configure the property to be non-enumerable, but it's not been done because it'd mean introducing additional runtime behavior. If you like the privacy modifier in TS, you should also like this proposal, since it would also allow TS to have real privacy instead of pretend privacy that can be trivially opted out of even within TS.

The private fields can't come soon enough, as hiding implementation details is a good practice for what one would think are obvious reasons. It has nothing to do with universities, the reasons are not academic, it's just how you write better software. What flexibility it takes away from the consumer, it gives back to the implementer, without requiring the implementer to make the code longer and more bug-prone or slower by using workarounds. It also ultimately benefits the consumer that the API is cleaner, more reliable and faster. Following better practices can also benefit the consumer in the long term if they're not writing throw-away code.

As for the linked, upvoted comments griping about the syntax – the actual content of the comments doesn't bear out as being well-informed. It's people having initial reactions with a bandwagon following.

mbrowne commented 6 years ago

@slikts

As for the linked, upvoted comments griping about the syntax – the actual content of the comments doesn't bear out as being well-informed. It's people having initial reactions with a bandwagon following.

There are a few people who have made more full and rational arguments against the syntax, particularly @rdking. But I agree with you about the comments from almost everyone else.

The private fields can't come soon enough

Yes!

To those claiming that the committee hasn't sufficiently listened to the community: are you aware that the committee has been not only listening to but also responding to feedback on this proposal publicly since at least 2015 (see the previous repo)? (And probably before then in earlier proposals; I don't know the full history.) Maintaining disagreement (based on logical reasoning) with the feedback received does not imply a lack of interest or a lack of listening. I have had a few complaints about how the committee has handled communication with the community as well, but it's totally unfair to claim that they're out of touch and not listening.

littledan commented 6 years ago

The private fields can't come soon enough

This is a key point: In addition to not having complete community consensus on all aspects of this proposal, we also don't have community consensus to delay private fields further. That's why it comes down to a hard decision. If everyone outside of TC39 were happy with this taking a few years longer, then I'd like to think that we'd be delaying it further.

hax commented 6 years ago

@slikts

If you like the privacy modifier in TS, you should also like this proposal, since it would also allow TS to have real privacy instead of pretend privacy

It seems you assume TS will make private x has the same semantic of #x in the future. But this is very unlikely to happen because it will have a very big backward-compatibility problem.

As my knowledge, TS team will keep their compile-time private and implement #priv if this proposal eventually land. So there would be three different "privates" in TS: private x, #x, private #x.

At first glance, it give you the full control of the combinations of "compile-time privacy" and "real privacy", but after second thought you will find this is a big burden for programmers and have a very little benefit. Many programmers may be a bit perfectionist, so they are forced to decide which "privacy" of three is appropriate for each property/method! Actually the difference of three is almost meaningless in real world cases, or even they have some usage, it can't compare to the add of complexity of mental model.

And all TS programmers have to remember, whenever they change private x to #x/ private #x or vice versa , strictly speaking, it is a breaking change. Unfortunately, because add/remove # is too trivial, I can imagine many will forget it and has a chance to introduce bugs in productions.


Actually I don't understand why TypeScript guys in TC39 could say yes to this proposal.

hax commented 6 years ago

@littledan

If everyone outside of TC39 were happy with this taking a few years longer, then I'd like to think that we'd be delaying it further.

My impression is some of you in the room already decide to not investigate other possibilities any more, and only give the community two way:

It sound more like hijack than choice.

littledan commented 6 years ago

TypeScript has been continuously represented in TC39 over the development of this proposal, by @rbuckton, @bterlson, @DanielRosenwasser and @jonathandturner.

littledan commented 6 years ago

@hax I don't mean no private at all; I literally mean, spend at least a couple more years thinking about potentially better private proposals.