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

[Open discussion] What would be for me the perfect class in js #15

Closed lifaon74 closed 6 years ago

lifaon74 commented 7 years ago

Hello everybody, after following the private class fied proposal and now this proposal, I wanted to discuss : why we don't go further.

First I need to specify i'm not a java, C++ guy or whatever, but a really involved an ecmascript lover. My motivation is to reach a consistent structure across languages heavily used and which have already implemented all the best for classes (since decades).

So for me what would be the perfect class in ecmascript :

1) attribute/method modifiers

In most of the language we find : public, private, protected and static. Currently only static is supported. For me we should use all of this words (already implemented in many languages) to keep consistency and fast code adaptation from one language to another.

The # for private sound wrong for me and the discussion (https://github.com/tc39/proposal-private-fields/issues/14) didn't convince me (people proposed concrete solutions to every problems...). Moreover the protected is still missing but extremely used in inheritance (sound strongly necessary).

Because we love to have full control of class properties in ecmascript, we could add a new attribute to descriptor when using Object.getOwnPropertyDescriptor or Object.defineProperty(https://developer.mozilla.org/fr/docs/Web/JavaScript/Reference/Objets_globaux/Object/getOwnPropertyDescriptor) :

interface Descriptor {
  value:? any;
  writable:? boolean;
  ...
  modifiers: ('static' | 'public' | 'protected' | 'private')[]
}

The new modifiers attribute could be an array of string representing the access of the method, and could potentially be modified after initialisation (to allow the same power than Reflect provides, allow a "backdoor" on external libraries, and allow descriptor to modify modifiers). This would bring far more power than the actual #field propose.

The public, private, protected should be allowed before static too.


To allow external classes or function to access to a protected or private member, we could use the friend keyword. Something similar to

class A {
  friend B
  private attr = 3;
}

This means than the class B can access the attribute attr of A even if it's private. We could then extends the Object.defineProperty :

interface Descriptor {
  ...
  friends: any[]
}

friends could be an array of friend functions or classes.


Finally for modifiers, a const keyword could be used to specify constant attributes, only allowed to be initialized into the constructor.

class A {
  private const attr = 3;
}

This will be a shortcut for writable=false

2) Multiple inheritance

Multiple inheritance is something that a lot of developers wants (there is a lot of subject or tutorial on the web) but can only be archived through mixins or factories. I would enjoy that you won't reply : "this is too complex because of diamond structures" or whatever because this is obviously FALSE (other languages like C++ archive it easily).

The following is just an idea how to bring multiple inheritance, it's not a concrete proposal.

First of all, because of the javascript current inheritance system with prototype, we can only inherit from one class. No more. A.prototype = Object.create(B.prototype); or in es6 class A extends B.

So we should introduce some king of new syntax.

1) We could use for example a new attribute prototypes which would be an array of mother classes, and prototype would point on the first element of this list ensuring retro-compatibility.

A.prototypes = [Object.create(B.prototype), Object.create(C.prototype)]; A extends B, C

2) instanceof should then search for all subclasses, so new A() instanceof C would return true.

3) The super keyword will need some adjustments: I propose this king of syntax super<B>.method : the super class here is B. To init a class:

constructor() {
  super<B>(...params);
  super<C>(...params);
}

Or we could use some C like : super::B. Using super.method will call the first super class (here B) for retro-compatibility.

Some other fancy stuff could be added too :

3) Abstract classes

Not the most important but really enjoyable, the abstract keyword could be use before the class keyword to define abstract classes. An abstract class may have abstract members and can't be initialized.

This still need more specifications.


So, the discussion is open. My purpose it to bring more in a big step to ecmascript instead of doing small steps and be stuck with retro-compatibility because of too various incremental specifications.

bakkot commented 7 years ago

@claytongulick, for context, we've been thinking about how to go about adding private fields to the language for well over a decade (I have a hard copy of what I believe to be the very first proposal for adding classes to the language, from around 1999, which mentions private fields as something to think about adding once we figure out a good way to go about it), with the sigil-based solution going back... I don't know how long, but at least several years.

As such, I don't think "aggressive" is really an accurate characterization: this is a core feature, difficult to achieve in userland without language support, and discussed for years. I don't see how we could be much less aggressive, and I don't think we have much to gain from holding off any longer.

blargism commented 7 years ago

I can see us (as a community) potentially kicking ourselves for burning this character to solve a problem for which there are already alternate solutions.

I share this concern.

I just remember some of the cheats that PHP put into place to solve technical problems. They later became hated features of the language that have either taken years to remove or developers are just stuck with. While the technical problems may be better considered here, I still think it a cautionary tale for this current proposal.

That said, I really appreciate all the thought put into #. It is obvious the technical concerns were very carefully considered. I hope that same attention detail and careful thought can be applied to a solution that is more extensible and palatable to the community.

bakkot commented 7 years ago

@blargism

I hope that same attention detail and careful thought can be applied to a solution that is more extensible and palatable to the community.

Is there any reason to believe such a solution with technically acceptable tradeoffs is out there waiting to be discovered?

To the contrary, I'm reasonably confident it is not, for the reasons given in the FAQ (and, as I say above, because we've been thinking about this for decades).

claytongulick commented 7 years ago

@bakkot I don't think anyone doubts the level of effort you've clearly put into this - we in userland have been tackling this problem for quite a while too, and have come up with several different effective solutions.

I see that proposal commit you linked from two years ago in 2015, and I understand why @ was abandoned in favor of using it for decorators. Of course we're all familiar with some of the history of the debates that drove the current form of classes in js and a lot of the discussions about the form they should take. Indeed, if we're talking about decades I have no doubt that Brendan Eich was thinking about it in 1995 when he invented Javascript, two decades ago.

Guys like me that cut their teeth coding on TRS-80's and asm in DOS (because C was too slow) certainly appreciate the evolution of languages and the careful consideration that goes into them. I think you're one of these as well. We also have seen mistakes made in languages that have far and long reaching consequences, not that this is necessarily one of them. I'm sure you can appreciate, however, that given the history of js, the last four or five years have seen a pretty massive acceleration in the rate of change. This is a good thing. That doesn't mean that every feature needs to go through on such an accelerated schedule.

You've received a ton of feedback from the community on this particular one. We have lots of current options for encapsulation of privates that we in userland have been using for many years, and they work fine.

I don't think we have much to gain from holding off any longer.

Is there any reason to believe such a solution with technically acceptable tradeoffs is out there waiting to be discovered?

To the contrary, I'm reasonably confident it is not, for the reasons given in the FAQ (and, as I say above, because we've been thinking about this for decades).

I'm curious - the whole point of having the proposal stages with TC39 was to have public review and gather feedback. I see this happening, which is a great thing. It's curious and strange to me that your response is to point to the FAQ, and to operate under the assumption that this is the only correct way to move forward? I thought that this exact type of situation is the reason why we had the progressive stages of approval, to make sure we're getting it right, and to collect feedback, and to slow down.

blargism commented 7 years ago

I've been developing in JavaScript since 2004. Not your auspicious "decades", so I'm pretty new at this.

Also @bakkot, if this the best you can come up with in two decades, maybe someone else should take over.

mbrowne commented 7 years ago

The current state of encapsulation in JS codebases that use classes or traditional constructor functions is abysmal. This proposal obviously recognizes that the lack of a simple way to create private properties using conventional class syntax is a problem, and offers a solution. IMO a concise syntax that makes it easier for developers to achieve proper encapsulation is a very good thing. I realize this wasn't the primary reason (or maybe not one of the reasons at all) that the # syntax was proposed. But encapsulation is a basic OO principle, and I think it's totally reasonable to dedicate a character to improve a core feature like classes.

In many languages, the default access level is private or internal. Given that the default access level for JS will alway needs to be public (for backward compatibility), using a single character to indicate private is a great idea to encourage good practice. We've gotten along this far without needing # for other features, so it's hard to imagine that whatever future features deserve special syntax couldn't be achieved with some alternative to #.

Are there any other reasons for objecting to the # character besides wanting to possibly use it for other things in the future? If someone is programming in a non-OO paradigm where encapsulation doesn't matter so much (perhaps because they only use immutable objects), they shouldn't feel constrained by the new syntax; they can simply not use it. Or is it a matter of valuing encapsulation but still disliking the syntax?

mbrowne commented 7 years ago

P.S. Just to avoid any misunderstanding of my above comment, I think it's certainly possible to do object-oriented programming without mutable properties. It just wouldn't be the traditional way of doing it, of course...and for anyone doing traditional OO, encapsulation is very important.

bakkot commented 7 years ago

@claytongulick,

I'm sure you can appreciate, however, that given the history of js, the last four or five years have seen a pretty massive acceleration in the rate of change. This is a good thing. That doesn't mean that every feature needs to go through on such an accelerated schedule.

Sure, I agree. But, well - my point was, this isn't an accelerated schedule. Private state for classes was originally slated for ES2015 (and indeed ES4 before it), and was deferred to a future edition because the committee could not at the time agree on a single solution. This proposal is very much an outgrowth of that work, and comes after exploring quite a number of alternatives in the design space and ultimately rejecting each.

You've received a ton of feedback from the community on this particular one. We have lots of current options for encapsulation of privates that we in userland have been using for many years, and they work fine.

If the userland solutions worked fine, we wouldn't need this proposal. (Though I guess you could quibble about what "worked fine" means.) This is something a lot of people, particularly library authors, have been asking for for a long time.

It's curious and strange to me that your response is to point to the FAQ, and to operate under the assumption that this is the only correct way to move forward?

I would be delighted to discover a better way to move forward.

But: I wrote the FAQ precisely to explain the constraints we consider important, which explain why we didn't think any other way thus far proposed would work - after, indeed, a great deal of feedback. At this point I don't think it's fair to call it an assumption. I think there are fairly solid justifications for my belief. I would not be helping to advance this proposal otherwise.

"We think this is the best way to accomplish this" is not incompatible with "we are taking feedback".

I thought that this exact type of situation is the reason why we had the progressive stages of approval, to make sure we're getting it right, and to collect feedback, and to slow down.

Stage 3, the stage this proposal has just reached, is the stage designated for collecting feedback. We've already been doing so for years, and I expect we'll continue to do for some time.

To be clear, then, short of killing it, what would you like to see the committee do differently with regards to this proposal?

(That said, we've already collected a lot of feedback. A lot of it has been very valuable. The remaining feedback tends to be either "I don't like the syntax" - which is a very real cost of this proposal, but also very much one we've already weighed - or "I prefer reflection to encapsulation" or similar design tradeoffs which again we have already weighed, or an alternative we have already considered and rejected for reasons outlined in the FAQ. There isn't all that much value in getting the same feedback repeatedly.)

As an aside, I think "slowing down" per se was not a goal of the stage process. Actually, checking the notes, it is an explicit non-goal.

bakkot commented 7 years ago

@blargism, to be clear, I'm referring to the committee and community as a whole, not myself personally.

blargism commented 7 years ago

@bakkot

to be clear, I'm referring to the committee and community as a whole, not myself personally.

Thanks for the clarification. To be fair, even the collective experience isn't objective proof of value. Just look at how much the experienced folks in the U.S. congress get done 🤣. I sit next to a guy who's two years into his development career an I am working towards finishing my second decade. He has still managed to teach me some things. Let's not rest on our laurels.

@mbrowne

Or is it a matter of valuing encapsulation but still disliking the syntax?

I do value encapsulation, I do dislike the syntax. There are some alternatives that can work almost the same and be much less irritating.

class SomethingPalatable {
    private.a = 0;
    private.b = 0;
    protected.c = "A potential for the future";
}

class NotMyPreference {
    #a = 0;
    #b = 0;
    // no clear path to protected
}

In this case # and private do almost exactly the same thing, and doesn't seem to have the same problems as the other alternate solutions addressed by the FAQ. In this case anything assigned to private isn't available anywhere except in the base class, just like the proposed #. The difference being it is more descriptive, clear, and provides a simpler path to more nuanced forms of access.

blargism commented 7 years ago

But encapsulation is a basic OO principle, and I think it's totally reasonable to dedicate a character to improve a core feature like classes.

@mbrowne I also forgot to mention that while encapsulation is a worth burning a special character, we can't do so without considering the future. Using # provides no clear path forward towards protected or a more innovative idea yet to be conceived. I agree we can't make a choice based on maybes, but we also can't ignore the fact that nearly every other language that embraces OO principles has the concept of protected in some shape or form. Picking a solution for private with no idea how to deal with protected seems pretty close to an oversight.

claytongulick commented 7 years ago

@bakkot I think this is my favorite comment from the issue you linked, and I think it really sums up the issue well. Hard private v/s soft private, and the reasons for each.

I'm going to take a bit of a detour here to explain where I'm coming from and why.

I'm a javascript evangelist.

I don't mean in a public give talks at SXSW way, I mean in a day to day, developer by developer hard grind kind of way. For more years than I'd like to consider, I've been slowly educating Java and .Net developers that have irrational and almost rabid hatred towards js. Explaining about the benefits of dynamic typing for some classes of problems. Explaining how prototypal inheritance is different, but just as effective (if not more so) than traditional OOP inheritance. Describing the fundamentals of functional programming and when it's a superior methodology to traditional OOP. Etc... etc... etc...

See, guys like me, we think js's openness and functional approach are strengths, not something to apologize for. We agree with Python's "we're all adults here" approach. We desperately don't want to see the OOP mess that Java and .Net devs have to deal with every day infect the language we love. This is the "second camp" from @glen-84 's remarkable comment.

There's been so much push back on this feature, I feel like it would be a signal to the js community at large and js advocates like me that the standards process isn't really interested in what they/we want.

We're at a really interesting time right now, where Javascript the language is one of the most popular in the world, due mostly to the fact that there's a captive audience. But that's changing. With web asm, eventually we're going to get to the place where js is competing on its merits. We're going to need to be able to point to our language and be proud of every part of it to evangelize, and to convince the world that they should use it rather than just do a Java -> webasm or C# -> webasm, etc... and in this, syntax really matters.

The points from the 'second camp' are what are going to win and keep mindshare, if we abandon or lose sight of that, we risk becoming a footnote.

Ok, overly dramatic soliloquy complete - what do you think about @blargism 's proposed syntax? That's really intriguing - I'm not sure I've seen it in any of the discussions so far - private.foo = 'bar'; seems (on the surface) to be a neat compromise, and feels very javascripty to me - i.e., I know that private is some sort of magic object that has props with restricted access just by glancing at the syntax. Also would solve the dynamic lookup issue? With something like private["foo"]?

bakkot commented 7 years ago

@blargism

To be fair, even the collective experience isn't objective proof of value.

Of course! But at some point, when no one has been able to come up with a better alternative which achieves the feature's goals and is feasible given the constraints of the language, you start to suspect that there might not be one. That's all I meant to say.

class SomethingPalatable { private.a = 0; }

I intended that proposal (which has indeed come up several times before in some form: x, x, x, at the least, as well as in committee) to be covered by this question in the FAQ, though I guess that's not very clear. (To save a click: it would work for accessing fields of this but also needs to explain how to access private fields of other instances, and there does not seem to be a clear workable way to do that.)

It also runs into the further, unrelated problem (mentioned those threads but not the FAQ) that private is a legal variable name in non-strict code, and always a legal property name. So the following is currently legal code we can't break:

let private = { prop: 0 };
let o = {
  private: { prop: 1},
  m() {
    return private.prop;
  }
};
o.m(); // 0

Of course, you can't write private as an identifier in strict code, including in a class body, so it is not technically impossible for private.prop to be a reference to a private property when written inside of a class. But, uh, it would be weird.

ljharb commented 7 years ago

With the private.x syntax, how would you access a private field on a different object of the same class?

bakkot commented 7 years ago

@claytongulick

See, guys like me, we think js's openness and functional approach are strengths, not something to apologize for.

I absolutely agree with this. Though I don't buy that this is an argument for reflection over encapsulation. Closures, for example, are as core a part of the language as anything you could name, and have never offered any sort of introspection: indeed, the type of privacy offered by closures has been a guiding example in the design of this proposal, including the lexical scoping of private fields.

We desperately don't want to see the OOP mess that Java and .Net devs have to deal with every day infect the language we love. This is the "second camp" from @glen-84 's remarkable comment.

Unless we're looking at different comments, that's exactly backwards: the "second camp" is the one which wants multiple types of accessibility modifiers all of which are only suggestions, which accomplish nothing not already doable relatively easily and clearly in the language as it is today and which lead to the sort of mess Java is currently trying to repair with modules.

By contrast, this proposal has been pared down to the absolute minimum necessary for its goals, on top of which other features like friend access can be built - though, again, we aren't ruling out adding language-level support for such features at some future point.

There's been so much push back on this feature, I feel like it would be a signal to the js community at large and js advocates like me that the standards process isn't really interested in what they/we want.

You're missing that there is a large if quieter camp of people who really, really want true private state in the language. I've talked to a number of library authors who are impatient to start using this - the Node foundation, for example, is on the committee and, if I recall correctly, would like to start reimplementing their internals in terms of this feature as soon as it's available.

We care a great deal about both sides of this debate. But there is a fundamental conflict here between reflection and encapsulation. They cannot both be accommodated. Our mandate is to balance the needs of all users of the language, including future users (who among other things are generally less put off by new syntax than people who have gotten used to the language without it), and do the best we can within the technical limits of the language. That has to mean that, sometimes, we advance features despite vocal objections.

claytongulick commented 7 years ago

@bakkot - if you, some library authors, and TC39 have already decided that this is going to happen, why are we bothering with a review process?

That has to mean that, sometimes, we advance features despite vocal objections.

If the feedback you're gathering doesn't have an impact, why are you gathering it? Why bother with Stage 3?

You're missing that there is a large if quieter camp of people who really, really want true private state in the language.

Our experiences have clearly been quite different, and the feedback I've read on these github issues paints a different picture to me as well.

Unless we're looking at different comments, that's exactly backwards: the "second camp" is the one which wants multiple types of accessibility modifiers all of which are only suggestions, which accomplish nothing not already doable relatively easily and clearly in the language as it is today and which lead to the sort of mess Java is currently trying to repair with modules.

I think we're interpreting that differently. The whole point of this proposal and its defense has been that even though it's ugly, even though no one likes it, the # syntax is the only viable technical solution that meets the hard private goals. However, that is not everyone's goal. In fact, it seems like it is the goal of a few people. It is remarkable and strange to me that the needs of a few highly technical library authors would drive a language's design, rather than the needs of the average js developer. I think that's how languages lose mindshare. I think TypeScript and Flow are doing a great job of illustrating my point - and they're transpiling. What do you think is going to happen when they can compile directly to web asm? Do you think the syntax in this proposal is going to help attract developers back to js?

If you're signaling that this is going to move ahead regardless of any and all objections raised, I'm not going to spend any more time on it. Participating in this discussion has been pretty eye opening for me - historically I've prevented my teams from using transpiled solutions other than babel, since it's my job in technical leadership to try to pick a winner for long term investment. This is honestly the first time I've started to wonder if js is going to be a long term winner. Maybe I should be more flexible evaluating transpiled solutions.

Even though we obviously disagree, I still appreciate the time you've spent discussing this with me and others! Best of luck.

mbrowne commented 7 years ago

@ljharb @bakkot

With the private.x syntax, how would you access a private field on a different object of the same class?

Why is this seen as a requirement? IMO private should mean private to the instance, not the class. Classes in JS are just a blueprint for creating new objects of a given type; after that, the only link between them is the prototype. This is a good thing because it means JS is actually object-oriented rather than class-oriented (although the current class implementation, in the absence of some sort of traits feature or embedding feature, lessens that a bit...but decorators will be a step in the right direction).

IMO, if we're talking about instance properties, the primary encapsulation boundary should be around the instance, and a different instance of the same class should be just as much outside that boundary as a different object of a different class. Of course static private properties can be useful too, and I'm not arguing against those -- for static private properties, the encapsulation boundary is indeed the class and not the instance.

@blargism

Using # provides no clear path forward towards protected or a more innovative idea yet to be conceived.

While I don't agree with the need for protected specifically (see my above comments), I definitely agree that there should be something between private and public. I was assuming that syntax to indicate such an access level was still on the table for future proposals. But if it's not, then even though I like the # syntax, I strongly agree that we should have a plan for how to add proper support for internal/protected/whatever before this proposal is finalized, even if the syntax for that doesn't make it into this particular proposal.

lifaon74 commented 7 years ago

I agree on some points: the current # are driven by a few and doesn't strongly care of ALL developers expectations or feedback and no metrics are available. The proposal was in stage 0 not so long ago and only a few feedbacks has been expressed until the stage 3 has been reached. We all agree on the fact that we want private but we should think about all aspects and consequences instead of hurry up.

For me things are pretty clear :

(That said, we've already collected a lot of feedback. A lot of it has been very valuable. The remaining feedback tends to be either "I don't like the syntax" - which is a very real cost of this proposal, but also very much one we've already weighed - or "I prefer reflection to encapsulation" or similar design tradeoffs which again we have already weighed, or an alternative we have already considered and rejected for reasons outlined in the FAQ. There isn't all that much value in getting the same feedback repeatedly.)

Not so much feedback and a biased sample ! I didn't see any google form or whatever with stats gathering thousands of answers... Each proposal impact millions of developers, we should not hurry up too much... The "I don't like the syntax" should be taken in account. Personally the # is fine to me. The "I prefer reflection to encapsulation" is even more important because it's critical real problems It's like saying : I prefer donuts over cake, so everybody will eats donuts until the end of their life... inacceptable. Both should be available.

jeffmo commented 7 years ago

@mbrowne

With the private.x syntax, how would you access a private field on a different object of the same class?

Why is this seen as a requirement?

I spent a lot of time questioning this too, but the case that convinced me was that this enables things like binary methods:

class User {
  #socialSecurityNum;

  constructor(ssn) {
    this.#socialSecurityNum = ssn;
  }

  isSamePerson(user) {
    return user.#socialSecurityNum === this.#socialSecurityNum;
  }
}

It also makes sense from a visibility perspective: If you have a private field, the only code that has visibility into it is the class's members. Thus, the logic that operates on the private field of one class instance is the same as the logic that operates on another class instance.

Moreover I couldn't find a compelling example of why this kind of visibility would be dangerous.

bakkot commented 7 years ago

@claytongulick

If the feedback you're gathering doesn't have an impact, why are you gathering it? Why bother with Stage 3?

I definitely don't mean to say that the feedback doesn't have an impact. Among other things, it's always possible we've missed something. It's just that getting the same feedback we've already received - for example, that many JS developers encountering the syntax for the first time find it distasteful - is unlikely to change at least my own mind, because we already know that and have weighed its cost.

The whole point of this proposal and its defense has been that even though it's ugly, even though no one likes it, the # syntax is the only viable technical solution that meets the hard private goals.

For what it's worth, "no one likes it" isn't true. Few people like it when encountering it for the first time, but it tends to even out after a while.

And - the # syntax is also the only technically viable solution that meets the soft private goals, except for the "no new syntax" goal. (Indeed, we were considering using it for soft private for a long while.)

Either way, having a subclass's public fields clash with a superclass's private fields is, I think, unacceptable for private state - otherwise you can get by just fine by prefixing things with underscores. Either way, a method expecting to operate on a private field x of an instance of the class shouldn't be operate on the public field of the same name when given a non-instance with a public field x. Either way, any solution must not significantly slow property access, because the speed of property access affects every user of the language including every visitor to every webapp, not just developers of classes. Either way, adding a new a private field x shouldn't break all other property accesses of a property named x anywhere in the class. Either way, the this keyword should not get even more special behavior.

It is remarkable and strange to me that the needs of a few highly technical library authors would drive a language's design, rather than the needs of the average js developer.

To a large extent, serving the needs of library authors serves the needs of the average JS developer, because the average JS developer relies on libraries and so relies on library authors being able to do their thing.

That said, I dispute that the average JS developer would prefer to write classes which have private state as a suggestion rather than private state as a thing enforced by the language. If the average developer is using reflection to get at private state, something has gone very wrong, and whatever "private" state is present should probably never have been added.

I think that's how languages lose mindshare. I think TypeScript and Flow are doing a great job of illustrating my point - and they're transpiling. What do you think is going to happen when they can compile directly to web asm? Do you think the syntax in this proposal is going to help attract developers back to js?

I don't think "maximizing mindshare" is actually a goal of the committee, else we wouldn't've been enthusiastic about wasm. We are just trying to evolve the language to the best thing it can be. That said, I don't expect the addition of this feature to the language will significantly turn people off of it.

(As a total aside: it is highly unlikely that TypeScript and Flow will ever compile to wasm as anything more than a party trick. Wasm is more for languages like Rust, not languages which need the support of a complex optimizing runtime which is already built in to the browser.)

If you're signaling that this is going to move ahead regardless of any and all objections raised, I'm not going to spend any more time on it.

I'm sorry to have given this impression; it is not the case. I just meant to say that we've thought a lot about this topic, including all of the objections raised in this thread several times over. Which does not mean "please don't give feedback". It doesn't even mean "please read all of what we've written about this proposal before giving feedback", or else I would not be in these threads responding to the same objections.

It just means that our response to objections already considered is mostly going to be "here's why we're still behind this proposal despite those particular objections".

Akkuma commented 7 years ago

@jeffmo Your example could be accomplished by exposing a public getter, like many other languages do.

class User {
  #socialSecurityNum;

  constructor(ssn) {
    this.#socialSecurityNum = ssn;
  }

  ssn() {
    return this.#socialSecurityNum
  }

  isSamePerson(user) {
    return user.ssn() === this.#socialSecurityNum;
  }
}
bakkot commented 7 years ago

@Akkuma Yes, but then it wouldn't be private.

I'm not aware of any language that has both class and a notion of private fields in a class where the accessibility of those fields is per-instance rather than per-class. I listed a few examples here, though that list could be substantially longer.

jeffmo commented 7 years ago

Right, the constraint I probably should've called out is that you should be able to create an instance of User with the ssn, then forget the SSN and pass those User instances around without any concern that other code might be able to read the SSN.

pitaj commented 7 years ago

Has there been any discussion about pulling private class fields into a separate proposal? I realize that it's asking quite a bit, but I do feel like it would allow for certain undisputed additions to the language to continue to move forward while discussion continues over private class fields.

It seems to be a widespread opinion that static fields and instance fields are much more cut and dry than the current private fields proposal.

RyanCavanaugh commented 7 years ago

I definitely don't buy the argument that there aren't a lot of developers who want this feature. Some perspective from TypeScript - "Private fields aren't actually private" was one of the first 10 bug reports we got. People have been demanding "true" private fields in classes for 5 years now. There is a sizable contingent of people who want this feature:

http://typescript.codeplex.com/workitem/7 http://typescript.codeplex.com/workitem/281 http://typescript.codeplex.com/workitem/912 https://typescript.codeplex.com/discussions/397651 https://typescript.codeplex.com/discussions/398585 https://typescript.codeplex.com/discussions/448054 https://typescript.codeplex.com/discussions/451129 https://github.com/Microsoft/TypeScript/issues/564 https://github.com/Microsoft/TypeScript/issues/1537 https://github.com/Microsoft/TypeScript/issues/3151 https://github.com/Microsoft/TypeScript/issues/9733 https://github.com/Microsoft/TypeScript/issues/9950 https://github.com/Microsoft/TypeScript/issues/11033

This comes up so frequently we have a FAQ entry to tell people to stop suggesting we emit nonworking (!) code to fake real-private fields: https://github.com/Microsoft/TypeScript/wiki/FAQ#you-should-emit-classes-like-this-so-they-have-real-private-members

pitaj commented 7 years ago

@RyanCavanaugh absolutely the feature of private fields is in demand. Most people are just having a hard time grasping why there needs to be such a syntactical "ugliness" when using private fields.

comex commented 7 years ago

And - the # syntax is also the only technically viable solution that meets the soft private goals, except for the "no new syntax" goal. (Indeed, we were considering using it for soft private for a long while.)

For anyone else here: "using it for soft private" refers to making # syntax sugar for Symbols - i.e. this.#foo would access a regular property whose name is a Symbol, and the Symbol used would be unique per (containing class, name) pair. Correct me if I'm mistaken.

So if you were considering that, why did you stop? Is there a link to a discussion where the matter was ultimately decided? (tc39/proposal-private-fields#33, closed a month ago, has some discussion but not a final decision.)

@RyanCavanaugh I suspect most of the people asking for "true" private fields would be satisfied with soft-private (which is a heck of a lot more private than what TypeScript currently generates). After all, they're probably used to languages like C# or Java, but both of those languages have reflection APIs that can access private fields - which can come in handy sometimes, like for unit testing. By the same token, it's not the end of the world if JS code can introspect private fields using e.g. Object.getOwnPropertySymbols().

jeffmo commented 7 years ago

why there needs to be such a syntactical "ugliness" when using private fields

FWIW: In my experience with language design, new/unfamiliar syntax can be difficult to judge when first presented. There are some fairly objective things you can observe like "exhausting to type it out" or "looks like something else in the lang", but I think this syntax passes those 2 tests at least.

Relatedly I've noticed that it's very easy to mistake unfamiliarity with unsightliness when it comes to new syntax (I definitely do it -- I think it's pretty human). This kind of feedback is so common that I've come to expect it before proposing any new syntax.

In this particular case, having worked with this private variable syntax for some time now, it's become pretty second nature to me and I don't really find it unfamiliar anymore. As a result I do feel pretty good about this choice of syntax on that front.

jeffmo commented 7 years ago

@comex:

There was a long discussion about this a little ways back here: https://github.com/tc39/proposal-private-fields/issues/33

EDIT: Doh! I just now noticed you totally linked that issue in your comment :p @littledan mentions that there's no conclusion, but the following points are my understanding about how we chose to proceed (rather than stall indefinitely)

Some tldr points are:

pitaj commented 7 years ago

@jeffmo I just want to clarify that I have no issues with the current proposal, I was just reflecting what I've observed other people saying about it

comex commented 7 years ago

@jeffmo Yes, I linked to that discussion in my own post. Underscore-prefixing is unreliable and symbol property keys are ugly. Hard-private is only really necessary for capability-based security sandboxes like Caja, but (a) those are not commonly used and (b) hardening a class to be suitable for those requires a lot more attention than just using private fields; if nothing else, freeze/seal operations are needed, and it's not much more work to add a Proxy on top.

lifaon74 commented 7 years ago

So, finally, why not reserve the # for hard private and use the "private" and "protected" keywords for soft private ? This way everybody is happy and we cover all the discussed aspects here.

ljharb commented 7 years ago

@comex hard-private is beneficial for every API everywhere. Many bugs have been caused by people changing things that they pretend are private, yet someone else was depending on - hard-private helps ensure that the documented API actually matches the observable API. It's a safer default, it's a smarter API choice, and it encourages developers to make explicit choices to allow reflection and use, rather than implicit ones.

littledan commented 7 years ago

@lifaon74 About reserving keywords in class bodies: We're actually in a really good place, since if you have two space-separated identifers in a class body (which aren't static, get, async, or other things like that which were explicitly added as features), it's already a syntax error. So there's nothing special that we have to do to reserve syntactic space for keywords indicating privacy in the future.

I'm not sure private will work for soft private, though, for the same reason it doesn't work for this proposal.

bakkot commented 7 years ago

@lifaon74 Beyond what @littledan says above, here's my response when this was brought up previously: I don't think the language has room for both of these. Programmers would have to learn both and constantly decide which is appropriate, even though they're solving similar problems, and most people wouldn't much care about the differences most of the time.

comex commented 7 years ago

@ljharb I disagree with every one of the statements in your post, but it seems like more of an ideological point (on both sides) than something that can be beneficially debated, so *shrug*. But my general response would be: what kind of a dysfunctional environment leaves you unable to trust your codebase to respect wishes expressed as firmly as soft private (or private in C#/Java)? If the offending code is in a library, maybe you shouldn't be using that library; if it's using your library, well, if they want to hack up your code badly enough, you can't stop them from just editing the source.

blargism commented 7 years ago

@bakkot I have registered my opinion. I don't like # for privates. I hope it goes my way, if it doesn't I'll still be an advocate for and user of the language. I've been a bit harsh, but I do respect the work you and the rest of the team are doing.

If you are ever in North Dallas look me up, I'll buy you a 🍺

allenwb commented 7 years ago

what kind of a dysfunctional environment leaves you unable to trust your codebase to respect wishes expressed as firmly as soft private (or private in C#/Java)?

Ugh -- the adversarial web!! Perhaps you haven't noticed that on the web some code that comes in contact with your code may have been written by people whose interests are not in alignment with your own.

kevlened commented 7 years ago

@RyanCavanaugh Is there a reason TypeScript doesn't use a WeakMap for private variables?

edit: added note for performance

class Foo {
  private x = 0;
  increment(): number {
    this.x++;
    return x;
  }
}

becomes

function Private() {
  var map = new WeakMap(); // assuming you can polyfill for a WeakMap
  return function(obj) {
    var props = map.get(obj);
    if (!props) {
      props = {};
      map.set(obj, props);
    }
    return props;
  };
}

var Foo = (function () {
  var p = new Private();

  function Foo() {
    p(this).x = 0
  }

  Foo.prototype.increment = function () {
    p(this).x++;
    return p(this).x;

    /*
    // or, for better performance
    var priv = p(this);
    priv.x++;
    return priv.x;
    */
  };

  return Foo;
})();

The same method should even support protected variables:

class Person {
  protected name: string;
  constructor(name: string) { this.name = name; }
}

class Employee extends Person {
  private department: string;

  constructor(name: string, department: string) {
    super(name);
    this.department = department;
  }

  public getElevatorPitch() {
    return `Hello, my name is ${this.name} and I work in ${this.department}.`;
  }
}

becomes

// scoped to Person and Employee
const ptd = new Private();
class Person {
  constructor(name) {
    ptd(this).name = name;
  }
}

// scoped to Employee
const p = new Private();
class Employee extends Person {
  constructor(name, department) {
    super(name);
    p(this).department = department;
  }

  getElevatorPitch() {
    return `Hello, my name is ${ptd(this).name} and I work in ${p(this).department}.`;
  }
}
mbrowne commented 7 years ago

@bakkot

I don't think the language has room for both of these. Programmers would have to learn both and constantly decide which is appropriate, even though they're solving similar problems, and most people wouldn't much care about the differences most of the time.

This is a valid point. But if, for whatever reason, we decided in the future that we should give the option of soft private in addition to hard private, would the language be able to accommodate that? Or do we need to make a final decision now that it should be one or the other? Maybe we should think about what alternative syntax soft private might require, if private x isn't feasible.

Personally I don't see this as a remote possibility...I think that as soon as hard private is added to the language (if this proposal moves forward) there will be a lot of desire for something a little less strict - and not just due to resistance to change, but from people who have valid use cases for reflection and would prefer not to resort to public properties with an underscore prefix. (Yes there are symbols already, but declaring properties using symbols is verbose.)

mbrowne commented 7 years ago

Of course soft private is not the only solution for providing reflection capabilities. One could certainly argue (and I think I agree with this) that if you want to do reflection, then in those cases you could just use protected/friend/internal (or whatever we want to offer between public and private). But this too brings us back to needing a plan for how to allow access levels other than public and hard private (even if that doesn't get implemented until a later proposal).

pitaj commented 7 years ago

@mbrowne I believe it certainly is possible. You'd add a keyword in front of the #prop declaration which would allow for an opt-in soft private behavior. For instance:

class X {
  #hardPrivate = "hard private";
  private #softPrivate = "soft private";
}
mbrowne commented 7 years ago

@pitaj Thanks.

So how do we feel about:

internal #internalToPackage (my preference) *

Or protected #myProtected

* meaning that I think it would be better to have internal/package access than protected and friend, not that I like the syntax. But if it's the only viable option...

bakkot commented 7 years ago

@mbrowne

But if, for whatever reason, we decided in the future that we should give the option of soft private in addition to hard private, would the language be able to accommodate that? Or do we need to make a final decision now that it should be one or the other?

As I say, I don't think the language has room for both. I think we need to decide now. (Not because of syntactic restrictions, just because I don't think it's a good idea to have such similar features.)

If we have hard private, it's easy enough to allow soft private to be dealt with in userland. (The reverse is very much not the case.)

For example, you can add static reflection methods to a class (static readPrivateFoo(o) { return o.#foo; }); given decorators this would be even easier: you could decorate a private field with @inspectable to install said static methods, or whatever. Or, as you say, you can just use Symbols and rely on getOwnPropertySymbols for reflection, which seem to me to be very precisely the semantics of soft private.

declaring properties using symbols is verbose

How so? Just in that you have to declare the symbol ahead of time? I know not everyone agrees, but that doesn't seem all that bad to me personally. Not enough to warrant nontrivial new syntax on its own, anyway.

But this too brings us back to needing a plan for how to allow access levels other than public and hard private (even if that doesn't get implemented until a later proposal).

I am not convinced that we need language-level support for any further accessibility modifiers. Other levels seem like they could be built without too much trouble in user code on top of the existing ones, especially given decorators. Hard private state is the only one which really requires language-level support.

esprehn commented 7 years ago

@kevlened a weak map would be extremely slow compared to a regular property lookup. Visibility modifiers should not impact the performance of your code.

lifaon74 commented 7 years ago

@bakkot So in fact we will all end up with one solution which cover only a small portion of the problem... just because the authors of the # don't want to listen and provide solid solutions ?

We already use getter/setters and symbol to achieve "private"/"protected" and "friend" and if we discuss here its because we're not satisfied with it ! It's far more verbose and slower, exposes private to public because of getters/setters and doesn't follow the DRY rule. All your examples are monkey patch to solve the problem. We already use monkey patch and we don't want them ! We aim to something native.

I could argue just like you and say : hey, we don't need # properties, we already have WeakMap... not a valid point

Moreover, reading all the comments above, most of the participants express the needs of soft private, and friend like access + inheritance. It's a majority (~2-3 for the hard vs all the remaining (10+) for soft).

@ljharb

@comex hard-private is beneficial for every API everywhere. Many bugs have been caused by people changing things that they pretend are private, yet someone else was depending on - hard-private helps ensure that the documented API actually matches the observable API. It's a safer default, it's a smarter API choice, and it encourages developers to make explicit choices to allow reflection and use, rather than implicit ones.

I strongly disagree on this one ! The bugs can only appears on the side where the developer touch the "private" fields. It's a ultra rare situation, it's its responsibility and it's fast fixed (simply refactor a variable name in most of the cases. I guess this situation is far much rarer than the need or private access (reflection) for optimization, outsourcing, etc... ! And the most critical point : even authors can't access to their own # into their package because of the class scope only. Bye bye optimizations, welcome slower code for everybody. Not all developers are gods which will use properly # properties, they will make mistakes and these mistakes will penalize potentially a lot of peoples...

bakkot commented 7 years ago

@lifaon74, Symbols are a native solution for soft-private properties. They are not particularly verbose (an extra declaration whose meaning is fairly clear), they look like property access (because they are), and there's no inherent reason they should be slow (and they don't seem to be in modern engines). None of that is true when trying to use WeakMaps as a "native" solution for hard-private.

Again, I don't want to say we'll never introduce friend, or other convenience modifiers. But right now it looks like none of them are essential (that is to say, having no reasonable solution in the language as it is) in the way that the hard-private state offered by this proposal is, and some if not all of their use cases might be met by this proposal + decorators. So they're not a part of this proposal.

lifaon74 commented 7 years ago

@bakkot Symbols cover only an aspect of the soft private. They don't include "protected" and friendship requires to share and exports the symbols. Their are really useful, but not powerful enough. "friend" just like "protected" are essentials ! They are heavily used and allow better code by outsourcing repetitive algorithms and allow better optimization. In a world of mobile, PWA, etc... optimization is really important. So friendship should be thought now. Else developers would do monkey patch and could be stuck when native friendship will exists.

littledan commented 7 years ago

Thanks for explaining all of this, @bakkot. I appreciate your efforts to explain the issues to all the people who come to this repository. These issues have been discussed at length before, but it's possible that those conversations are hard to read, so @bakkot is being very helpful in explaining them to more people.

The questions here are complicated, and involve thinking through multiple sides. I really want to get more community engagement generally on design decisions, but reducing it to "10 people on this thread think X, and 3 think Y" is a bit simplistic. Some others have supported the design decisions in other forums, and there's only so much appetite for people to repeat their arguments over and over again. It also seems hard, in this thread, to communicate subtle issues, as @bakkot has been trying to do.

@lifaon74 I don't understand how optimization could be based on friends, protected, or even this private state proposal. The part of this proposal which is most friendly to optimization is that you declare fields--this is more optimizable than fields which are dynamically created later.

My hope is that friends, protected, soft-private, etc can be covered by the follow-on decorators proposal. It's true that this is a follow-on proposal for what is really a core capability, but I hope that a combination of symbol-named fields and methods, and whatever else people are doing now, can tide users over. The decorators proposal is not done yet, but I've been working on making it more concrete, and I have some confidence that it will not conflict with the design decisions being made in this proposal.

At this point, the proposal is at Stage 3, which means we hope the design and specification are basically done, and that the proposal is stable enough to be implemented with confidence. I'd hope that, in the future, this sort of feedback can be gathered before Stage 3. I'd appreciate any ideas to improve openness to community feedback and engagement.

lifaon74 commented 7 years ago

@littledan A really simple example of optimization : imagine a Blob implementation with an internal private buffer. Without access to this buffer, we need to use the FileReader which is far slower. If somebody would then implements a Response or a File (File inherits from Blob) with # it's impossible to access to the bufferto optimize internal private methods of Response and Filecan't inherits the buffer... Simple example which strongly demonstrate why # are really incomplete and why we require friendship/protected.

Moreover, friendship is used in a lot of cases that currently are public functions/methods. In many projects, developers outsource repetitive or common core algorithm. Imagine having different classes for ArrayLike (Queue, SortedArray, etc...) which extends an abstract classes ArrayLike (all of this is abstract in JS but possible un typescript for example). All of them will share common methods and properties (including private) and could have an underlying private array to keep data. An external sort function could be applied to an ArrayLike and require to access private field (array). Most of the times Helpers requires private access (so friendship or reflection). You could take of look at the streams for example where the usage of friendship is important, and in a general manner, all the official js lib specs.

Decorators are a solution as demonstrate here but would require to modify Object.defineProperty (or kind of) and allow soft private... exactly what I point out at the first comment. Moreover "private"/"protected" keywords exists and are more elegant...