-
Notifications
You must be signed in to change notification settings - Fork 113
Does the FAQ really justify the syntax and its limitations? #133
Comments
@bakkot Thank you for at least agreeing to try. So let's get started.
I'm not arguing that access is supposed to be The first real problem begins here:
1 is true. Given the notation you've chosen, access semantics are broken there. The problem is that you've made the
2 is false. The notion of 'private', as mapped into ES, is only the notion that such a member is not accessible as a property of the owning object. So if |
If there were a sufficiently good type system, they would not get a TypeError later - you would not be able to add or refer to the public field from within the class, and from outside if it you would only be able to refer the public one. That is in fact what languages like Java do, as in class Base {
private int x = 0;
public int m() {
return this.x;
}
}
class Derived extends Base {
public int x = 1;
}
Derived foo = new Derived();
System.out.println(foo.m()); // 0, no errors
System.out.println(foo.x); // 1, no errors I stand by that paragraph in the FAQ.
I find the symmetry between declaration and access (as in
That is one mental model; it is not mine. Mine is as given in the FAQ. I suppose it could be worth rewording to "contrary to one possible notion of private" |
@bakkot In the example you gave above, Base.m cannot access Derived.x even if Base.x doesn't exist. In fact, if Base.x doesn't exist, this code won't compile. That has nothing to do with the type of x. Taking this into consideration, if by "sufficiently good type system" you were referring to how properties are bound to objects, and the access limitations placed on Base methods by other languages, then I get what you're saying, but that has nothing to do with static typing, but is rather due to the inheritance and referencing structure of the language. ES could reproduce the same results by keeping functions and non-functions separated within an object, but then functions would no longer be 1st class values. The reason I'm asking you to rework that paragraph is explained here. While you mean to refer to the object type system in use, you use "statically typed language" and "dynamically typed language", both of which refer to how the language handles the type of a variable. That's misleading at best.
So basically, you're saying you prefer aesthetics over functionality? Aren't there problems with this?
BTW: |
In a somewhat long-winded post here, I made the following arguments about preferring 'private' to '#' for member declarations:
So let me ask a different way. What is so valuable about the symmetry of your proposed notation that it warrants breaking programmer intuition about access notation, how to declare members, and the use of symbols? |
It has to do with the type of Base, and of
I am saying that aesthetics and ergonomics are important, and in this particular instance I am unwilling to sacrifice them to gain a particular functionality, yes. It is not a blanket statement about a preference for one or the other.
Symmetry is inherently valuable. It isn't valuable above all else, but it's not nothing. It makes it easier for people to hold the language in their head, to understand its meaning when reading code, to write it fluidly without having to stop and think about the syntax. These things are important. Also, in teaching this feature, I have not found that it significantly breaks most programmer's intuition about access notation, how to declare members, and the use of symbols. It's never going to be possible for something to be intuitive for everyone, unfortunately, but I strongly suspect your proposal would be much worse in this regard. |
@bakkot This one is a TL;DR for sure. Let me summarize it like this: While I get that you truly believe what you said, I think it's only due to familiarity and sheer effort of evangelizing when compared to significantly more complicated and less viable proposals that yours has come out on top. If you were to do some blind testing, your suggested syntax vs mine with developers who haven't seen either, both suggestions being presented in parallel, you won't get the results you expect.
Here we agree... to a point. I think the point of disagreement between is in exactly how "ergonomic" or "aesthetically pleasing" your use of the class Example {
#field = 1;
member() {
return this.#field;
}
} vs
On the issue of ergonomics: You win with the shorter syntax.
Isn't it though? There are many different ES programming paradigms that make rampant use of the simple fact that
And yet I, and several others, have had exactly the opposite experience. In general, any decent programmer is going to be able to shrug off the oddities of your syntax after a moment of getting used to it. There's no doubt about this. However, that's not a justification for using an unnecessarily odd syntax. You suspecting that the proposal I've offered "would be much worse in this regard" has little merit. I've tested your syntax and mine by placing them both in front of developers, gave an unbiased explanation for any questions asked and found that:
You see, it's one thing to indoctrinate people to a particular syntax first, then introduce competition, but it's entirely different when both are introduced in parallel. I'm willing to bet that if you introduced my suggestions along side yours to someone who hasn't yet seen your proposed syntax or mine, you'd be shocked by the response you get. I'm of the impression that your proposal has such a strong following merely because it truly was the best suggestion anyone had come up with until recently. It has the advantage of the time people have already invested in it. Pride makes it hard to even consider other suggestions in such situations. While I understand that, I find it hard to be sympathetic to that when the result is unnecessarily limiting, functionally disparaging, damages possibilities for future expansion, and so very easy to remedy. What I meant by damaging towards future expansion is that, suppose your proposal goes through to stage 4. That means that @ljharb has told me that one of the justifications for even attempting private fields is due to the complications involved with properly implementing WeakMaps for this use, and yet every developer wanting Why will this happen? Simply because you opted to use |
I'm sorry, I didn't realize you were still proposing that declaration would be class A {
private a;
constructor() {
this.a = 1;
}
m() {
return this.a;
}
} and have that constructor and method be referring to a public field named Yes, I know you are proposing that access would be I think we've covered this particular point several times by now; I don't know what else I can say on the particular question of using
We disagree about the weight of the costs of the these things and of your proposed remedy. I'm not sure I see a way we can resolve this disagreement.
OK, this is a sidebar, but - as I've said before, there are a great many possible kinds of access modifiers beyond "public", "private", and "protected". I don't buy the argument that we'll definitely want exactly those three but never any others and so we must provide access modifiers using exactly the three keywords we happen to have reserved (or two, with the default being |
Separately, as I’ve said many times, a hill i will die on is that a “protected” keyword that does not actually protect anything will not be a part of the language; the concept of “protected” in all other languages that have it is badly misnamed, and we should not adopt it. |
@ljharb We've already had that discussion, and I think we both agree that it is badly named since it doesn't protect anything in any language it is used in. That doesn't mean that you should throw your life away to stop the concept from getting into ES where it is already both highly desired and extremely useful pattern for those who write API's for use by other developers. I hate to predict a future that will involve you being marched over, but the concept of
So we agree on that issue. Your example is wrong for what I've proposed. Here it is corrected: class A {
private a;
constructor() {
this#.a = 1;
}
m() {
if (this.a === this.#a) {
throw new Error("This should never happen.");
}
return this#.a;
}
} It can't be denied that
To this I reply "True, but so what? That's just a straw-man argument." Every borrowed concept in ES resembles something present in another language but with very different semantics. Will running
few if any who would choose to use this feature would be confused by the resulting functionality. In fact, based on the user testing I've done, exactly the opposite is the case.
Sadly, you have a good point. Someone making that mistake would still have functional code, but would have leaked their private data. I hate to tell you this, but I still have to say "so what?" to that. Your approach allows the same thing! I get that you don't think so, but don't assert it if you haven't tested it. I'm not saying anything to you that I haven't thoroughly tested with uninitiated developers. class A {
#a;
constructor() {
this.a = 1;
}
m() {
return this.a;
}
} To a novice programmer, the above code would look like it's supposed to work, especially if they've been told that they "need to use
This is already a higher mental load than my approach, which only requires that the developer remember to use class A {
#a;
c;
constructor() {
this.#a = 1; //works
this.c = 2; //works
this.b = 3; //works
this.#d = 4; //fails
}
m() {
return this.a;
}
} If
While your approach has the advantage of apparent naming symmetry, it comes at the cost of breaking the actual naming and access symmetry currently a long-standing and highly-valued part of ES. Trading in something real for something fake is generally a bad thing.
I do, and it's not hard at all. You just need to prove your assertions to yourself. Find someone you haven't already indoctrinated and write a small piece of sample code using both your approach and mine. Don't label them. Let them ask you whatever questions pop into their heads. Answer without bias. Describe any language features that would be expected to work but won't for each approach without mentioning what the other approach does to mitigate that issue. Ask your test subjects to choose the better of the 2. Ask for explanations. From this you'll at least gain backing for your assertions. I've already done this many times and have only found 2 developers that preferred your approach to mine. I would have far less issue with this proposal if the majority of developers would say they prefer your approach. |
As I said: Yes, I know you are proposing that access would be I don't know how else I can say this.
This is mostly an aside, but that code is not valid Java at all. There's an important difference "works, with different semantics" and "does not work".
"Far too high" is not really a claim which can be baseless. We agree there's a risk for confusion. You think the level of risk is acceptable. I do not. But there's no objective standard for "acceptable".
Yes, which is why instead we say "to make a field private, begin its name with But I agree this mistake is possible for both possible syntaxes. It's just that it will be much, much more common with yours. This code: class A {
#a;
constructor() {
this.a = 1;
}
m() {
return this.a;
}
} does not look nearly as much like other languages as this code: class A {
private a;
constructor() {
this.a = 1;
}
m() {
return this.a;
}
} and people are consequently much less likely to assume they know what it means. These differences in risk are important.
We disagree about which concerns count as "real", I think.
To be clear, I've talked to dozens of people about dozens of syntax variations over the last several years without presenting my own opinions, including several which included But in any case this approach could not resolve the disagreement, because it is a disagreement about which things we value. |
@bakkot That was beautiful! You managed to cherry pick against a straw-man argument and miss the point entirely.
The point you missed is that the only reason it works at all in ES is because the semantics of
Per domain, a standard for "acceptable" can be defined. Problem is, none has been defined for the domain of "acceptable level of confusion due to syntactic similarity." Even that is beside the point I was trying to make. My point was that the possibility that someone will forget to include the And by the way, when you make a claim of this form: "X is far too Y to justify Z.", you automatically imply that you have some basis from which to measure X, Y, and Z. That's just the way the language works. By stating what you did in your previous post, you confirmed my claim of "baseless".
Again a baseless assumption. Your syntax can be taught with the following 2 rules.
My syntax also has 2 rules.
To this end, I would agree that it would be "more common", but not nearly as much as you seem to want to claim. This is a testable assertion. I encourage you to do so as I have. This time, try using developers who primarily work in ES. I've tested with both groups. Some spotted the issue without it being pointed out, but in the end, they still preferred the syntax I suggest. |
@bakkot you know, I've decided to agree with you in that we need to agree to disagree on the value of this issue. Don't worry about replying to my previous post. My next post will be a solution to the problem that works regardless of who's syntax gets chosen. I am hoping that, assuming this solution is amicable to you, that then you will be willing to reconsider your position. |
@bakkot I'm thinking that if we can do something to significantly reduce the likelihood that someone will code access to a public field when they meant to access a private field by the same name, then your rather persistent desire to (imho)break the language as a countermeasure should subside. Am I wrong on this? Assuming I'm not, I'm thinking the problem comes down to the issue of dynamically adding properties to However, the principle behind that simple suggestion still remains. Here's the real idea:
For example: class Ex1 {
constructor() {
this.x = 2; //works
}
}
class Ex2 {
#zed = 1;
constructor() {
this.x = 2; //SyntaxError
}
}
class Ex3 {
#zed = 1;
x;
constructor() {
this.x = 2; //works
}
}
class Ex4 {
#zed = 1;
constructor() {
this['x'] = 2; //works
}
} The reason Ex4 works is because using Functions added to either the instance or the class A {
private a;
constructor() {
this.a = 1; //SyntaxError
}
m() {
return this.a; //SyntaxError
}
} Does this reasonably mitigate the issue for you? |
Just in case it must be fully stated:
|
@rdking, I don't think your proposed mitigation is sufficient. Consider operating on a parameter. class A {
private a;
getA(param) {
return param.a; // is this dereferencing a private field?
}
} Your proposed dereferencing syntax is identical for public and private fields, so the intent of the class author is lost. Maybe the author was intending to dereference a public field There's no way to lock down the properties accessed on method parameters because JavaScript does not know the type of the parameter. It might be homogeneous. It might not. The FAQ addresses this here. |
@robpalme Thanks for the input, and good question. To answer your question, I considered that scenario in my wording. If param is an instance of A, then return param.a becomes an Error. Thinking about it more, I should have said ReferenceError instead of SyntaxError since this is something that can only be evaluated at the time the
Not true. The syntax I'm proposing for accessing private fields still includes the |
Let me restate the rules for this idea:
|
So by using a private field, I’ve denied myself direct access to a public field of the same name? I’m not sure why that tradeoff is an improvement. |
@ljharb Incorrect. You still get access to a public field of the same name, but only if it is explicitly declared as field of the class Example {
#counter = 0;
constructor() {
//this.otherField = true; //Would cause a ReferenceError
this['otherField'] = true; //Works
}
getCustomField(field) {
++this.#counter;
/*The line below is still a ReferenceError even though this.otherField exists. */
//if (this.otherField)
if (this['otherField'])
return this[field];
}
}
var ex = new Example();
if (ex.otherField) {
ex.bar = "foo";
}
ex.bar === ex.getCustomField('bar'); //returns true The net effect is that regardless of notational symmetry, most cases of accidental public access would be caught and flagged as a ReferenceError. @borela That was originally dismissed as being less clear than |
Forcing bracket access to a normal property from inside the class is also unacceptable, and would violate almost every common styleguide. |
You missed something again. Look at the Ex3 example I gave before. If you want to use |
Thanks, i think i understand what you mean now. What about inherited properties? If i declare one, it becomes an own property, and shadows the inherited one - so i can’t have a private field “length” and also use an inherited “length” property via dot access? |
Are you talking about this case? class Base {
constructor() {
this.pi = Math.PI;
}
}
class Derived extends Base {
#circumference;
constructor() {
super();
this.#circumference = 2 * this.pi; //Does this work or not?
}
}
class Derived2 extends Base {
#circumference;
pi;
constructor() {
super();
this.#circumference = 2 * this.pi; //returns NaN
}
} If so, then even though it means that code may have to be rewritten (which will likely be the case anyway) I'd say it shouldn't work. ... If not, then were you referring to this? class Base {
pi;
constructor() {
this.pi = Math.PI;
}
}
class Derived extends Base {
#circumference;
constructor() {
super();
this.#circumference = 2 * this.pi; //Does this work or not?
}
} This works as expected. -- Edit note: forgot about super().... |
There's 1 other case that I find bothersome. function Base() {
this.pi = Math.PI;
}
class Derived extends Base {
#circumference;
constructor() {
super();
this.#circumference = 2 * this.pi; //Does this work or not?
}
} In this case, since |
It’s tenable to force the author of a subclass to make changes when adding a private field; but not to force the superclass to be rewritten. Whether the property is on the prototype or not is irrelevant, because member access in JavaScript walks up the prototype chain. |
For some reason I was forgetting to take into account that members added to an instance by base ancestor methods are own properties of the instance, so the bothersome case isn't a concern at all. So all that needs to be done is ensure that there exists a public declaration for each public field accessed via
We agree on this. So is there a use case I'm not considering that makes this a major concern? |
A public property “foo” only on a parent class’ prototype (installed later, perhaps, since syntax can’t give you this kind of benefit about other code) - inside the child class instance, I’d be unable to access it with |
I'm assuming that the child
|
class Record {
private #id;
constructor(id, data) {
this.#id = id;
Object.assign(this, data);
}
getPrivateID() {
return this.#id;
}
getID() {
return this.id;
}
get(key) {
return this[key];
}
} Modifying that code to match your proposal, what does |
It throws a ReferenceError because class Record {
private #id;
id;
constructor(id, data) {
this.#id = id;
Object.assign(this, data);
}
getPrivateID() {
return this.#id;
}
getID() {
return this.id;
}
get(key) {
return this[key];
}
} This would do as you want. I'm thinking that is is not unreasonable to require the public declaration of |
I think that it is unreasonable for the language to require explicit declaration of public properties in a dynamic language like JavaScript, under any circumstances. |
Then there is also either:
Private properties are being billed by this proposal as if they are non-own properties of an object. ES currently doesn't allow duplication of property names on a single object. If you're going to claim that private properties are not properties of the object from which they can be accessed, then they are logically properties of some other object. Therefore, by your own reasoning, it is unreasonable for the language to require explicit declaration. If, however, they can indeed be thought of as non-own properties of the object from which they can be accessed, then their names must not conflict with any other property accessible from that same object. You can't logically have your cake and eat it too. |
I'm not sure what logic you're using to say that something is not an own property, therefore it can only be a property of another object - variables aren't own properties either. I don't find it unreasonable to require explicit declaration for private properties - to require it for normal properties, however, does not match the idioms of the language since its inception. |
Bag the straw-man argument please. If you have to access it by preceding it with a variable name and a
Please understand that this is the mental model this proposal must align with lest it require every ES developer to alter their understanding of the language in a very peculiar way.
Doesn't disagree with it either. What I'm trying to get you to see is that this is a whole new scenario that has never been present in this language. Explicit declaration of properties of any kind has never been frowned upon in the language, but also has never been required, so you're right there. However, it has always been invalid for an object to contain 2 different fields by the same name. But that's in this proposal. Before we can go any further, these 2 questions need to be answered definitively.
Depending on how you answer these, what is logically reasonable will change. |
Neither.
These terms are not sufficiently well defined for there to be a meaningful answer to this question. |
Then what is it?
If an "own property" of accessing object |
A new kind of thing.
No, I don't know what you mean by "property". I don't think the spec is ambiguous about the semantics here; I'm not sure why you want me to answer this for you, rather than deriving it yourself from the existing semantics based on your own definitions. |
Alright. I hope you're not offended by being compared to Bill Clinton ("That depends on what the definition of is is."). If you don't know what I mean by property, then you're probably not an ES developer and probably shouldn't be making a proposal. But, humoring your (hopefully) feigned ignorance, in the expression
Please stop evading the question. Every language is composed of a set of component categories in which each token is exclusively categorized. What is the sigil? Is it one of these?
If not, then what exactly is it? Currently everything in the syntax for ES falls into these categories exclusively with varying degrees of overloading. There is nothing in ES that spans multiple of these categories. In fact, you'd be hard pressed to find anything in any programming language that spans multiple categories. So once again what is the |
@rdking I’ll remind you that our repos operate under a Code of Conduct - please remain respectful even if you disagree with something that’s been said. |
Such definitions tend to be provided by reference to existing things, just as you've done. In those cases, when there's a new kind of thing introduced which shares some characteristics with existing things which we would consider to be examples of X and which lack other such characteristics, there is no answer to the question "is this new thing an X?".
Like I said, it's a new kind of thing.
I don't think this is so. Anyway, I don't think this line of discussion is likely to be productive. I'm going to bow out now. Apologies. |
@ljharb Fair enough. I thought that one might skirt the line. I'm just having an infuriatingly frustrating time trying to understand why he doesn't wish to provide a clear and definitive answer to the questions I've posed. Even if the @bakkot Please tell me you're not being serious.
You left one out:
This is the only one in the language that can be placed under 2 different categories. However, the category that it can be placed in is based on the context, and those contexts are distinct. Your So please, instead of just saying "it's a new kind of thing", please either categorize it, or admit that you cannot. If you cannot then this proposal has a serious issue that I hope the TC39 board isn't willing to just gloss over. I care not if it's one of the 6 I listed before or something created specifically for this language, but I do care that you can give it a clear and categorical description. As for the 2nd question, I'm going to give this one more try. This one is mostly from the ECMAScript specification. Properties - containers that hold other objects, primitive values, or functions and collectively comprise an object. If this definition isn't satisfactory for you to provide an answer, then please provide your own definition and answer with respect to that. |
@bakkot Can I help you with this a little? If we have to keep your proposal as is, then I suggest you declare Is this even close to the "it's a new kind of thing" you kept saying? |
@nicolo-ribaudo I'm putting part of my response to your post here instead...
Not really. The definition you gave satisfies its use as a literal in a declaration, but the |
Thanks for the comments above. We'd welcome more documentation in the FAQ to clarify the points above. In the end, we're moving forward with the |
@littledan you just dropped all alternatives without proper reasoning. It's the worst decision you could made... |
@lgmat There's lots of reasoning in the responses within these threads. I don't agree with all of the arguments in favor the current approach, but overall I don't really see how we could move to one of the alternatives. |
@littledan By saying
Are you agreeing that there are flaws in the arguments that back this proposal? By saying
Are you acquiescing to those flawed arguments because you personally don't see a better way? If so, then please answer this: If there were nothing (not even the existing language itself) preventing you from having what you would consider to be the best private field proposal, what would that look like? |
I can't think of what I would consider a better proposal. By saying I don't agree with all arguments, I mean many times, arguments in favor of this proposal were expressed in this repository in an absolutist way, when I believe they are more of a trade-off (but then I agree with the direction of that trade-off). I also don't see branding as a huge factor in this proposal. |
I don't see branding as a factor at all. However, I do see where the choices and logical contradictions have led this proposal. While I am grateful that so much though has been put into the proposal, I think it suffers from a severe lack of comprehension of both the expectations of developers and future expansion paths for the language itself. I (and I'm sure many others) would love to understand why this particular proposal is being advanced despite its rather obvious drawbacks and difficulties. |
I only read the first post, but I agree the fact is bluffing, to make it seem that some things are more complicated than they really are. For example,
Of course it would. That's why we can think of alternatives like @rdking' example (from #100 (comment)): var aSymbol = Symbol();
var bet = "bet";
class Test {
#field = 3;
#['alpha' + bet] = 'abcdefg...'
#[aSymbol] = "It works!"
test() {
var alphabet = 'alpha' + bet;
console.log(`There are ${this.#field} private fields in this class.`);
console.log(`this.#${alphabet} = ${this.#[alphabet]}`);
console.log(`this.#[aSymbol] = ${this.#[aSymbol]}`);
}
} Why does the spec not aim to allow this? |
What would be the use cases? The point of symbols is to avoid collisions with properties used by unknown code - but inside one’s own class there can’t be any such unknowns. The point of dynamic access tends to be to be able to use reflection when one lacks the knowledge to hardcode the properties - but inside one’s own class, there’s no need for dynamism (and storing an object or array inside a private field can trivially provide it). |
There's need. I might want to define property types (a runtime type system) as I can keep imagining!... |
How would a type system get access to static private type data, since “private” means that no code outside the class can observe or interact with them? |
The type system runs internally inside each class, and the definitions stored in the It's not an external type system, otherwise obviously it would only work on public properties. |
It wouldn't work well with inheritance perhaps... but that's where protected comes in. |
First, don't join in on this conversation unless you've read
The FAQ
This thread is to be a discussion on how the considerations of the FAQ affect not only the usability of the language should this proposal become standard, but also how it affects the future extensibility of the language. If you have issues with this proposal that come about because of the FAQ, and you can support that with reasonable examples and use cases, or if you have counter-arguments in support of the FAQ, also with reasonable examples and use cases, please join in.
The text was updated successfully, but these errors were encountered: