Skip to content
This repository has been archived by the owner on Jan 25, 2022. It is now read-only.

Does the FAQ really justify the syntax and its limitations? #133

Closed
rdking opened this issue Sep 26, 2018 · 59 comments
Closed

Does the FAQ really justify the syntax and its limitations? #133

rdking opened this issue Sep 26, 2018 · 59 comments

Comments

@rdking
Copy link

rdking commented Sep 26, 2018

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.

@rdking
Copy link
Author

rdking commented Sep 26, 2018

@bakkot Thank you for at least agreeing to try. So let's get started.

Why isn't access this.x?
...Having a private field named x must not prevent there from being a public field named x, so accessing a private field can't just be a normal lookup.

This is only an issue in JavaScript because of its lack of static types. Statically typed languages use type declarations to distinguish the external-public/internal-private cases without the need of a sigil. But a dynamically typed language doesn't have enough static information to differentiate those cases.

I'm not arguing that access is supposed to be this.x. This is ES. It can't be this.x while still providing hard encapsulation. My issue here is that your justification is completely bogus. The type of the property isn't the problem. Suppose someone wanted to add a public String x to an object that already had a private String x. The real issue is that public and extensible is default for objects in ES. Someone adding a new public member to an object that just happens to collide with a private member would be shocked by the TypeError they'd get trying to access it later. You might want to swap out that 2nd paragraph.

The first real problem begins here:

Why doesn't this['#x'] access the private field named #x, given that this.#x does?

  1. This would complicate property access semantics.
  2. Dynamic access to private fields is contrary to the notion of 'private'.

1 is true. Given the notation you've chosen, access semantics are broken there. The problem is that you've made the # part of the property's name. There's a simple way to fix this issue. Swap .# with #.. This gives you back dynamic access and eliminates the concerns you mention below.

But doesn't giving this.#x and this['#x'] different semantics break an invariant of current syntax?
Not exactly, but it is a concern. this.#x has never previously been legal syntax, so from one point of view there can be no invariant regarding it.

On the other hand, it might be surprising that they differ, and this is a downside of the current proposal.

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 x is private on obj and obj doesn't have a public x, then "x" in obj is always false. However, if obj has a member function f, then f() will still have access to x when called with obj as its context. How that access is carried out, whether through . or [] is entirely irrelevant to the notion of 'private'.

@bakkot
Copy link
Contributor

bakkot commented Sep 26, 2018

Someone adding a new public member to an object that just happens to collide with a private member would be shocked by the TypeError they'd get trying to access it later.

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.

There's a simple way to fix this issue. Swap .# with #..

I find the symmetry between declaration and access (as in class { #x; y; m(){ return this.#x + this.y; } }) sufficiently valuable that I would not want to give it up just to avoid this concern or to allow dynamic access.

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.

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"

@rdking
Copy link
Author

rdking commented Sep 26, 2018

@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.

I find the symmetry between declaration and access (as in class { #x; y; m(){ return this.#x + this.y; } }) sufficiently valuable that I would not want to give it up just to avoid this concern or to allow dynamic access.

So basically, you're saying you prefer aesthetics over functionality? Aren't there problems with this?

  1. You're willing to break functionality to gain something you think of as more aesthetically satisfying. That's like knocking down a support wall in a house just to make room to hang a large picture. Breaking a language feature just for code aesthetics should rarely ever be done. Are you saying that the symmetry performs some function beyond simple aesthetics?
  2. The symmetry you wish to preserve poses other issues, like the mental model problem. For those of us that use an intimate knowledge of the language to craft our code, this notation is problematic. # is not a valid character in an [[IdentifierName]]. This proposal isn't changing that fact. Otherwise it would be possible to do this: var #foo;. Object member access is always either <object>.<identifer> or <object>.[<IdentifierName>]. Given that the [] form is out, this means that the # is part of the identifier. So then what is #? Everything else in the language has a simple, single meaning, but under this proposal, # is a non-identifier character that is part of an identifier. That's confusing. What you're replacing (_) was always a valid identifier character. So the mental models don't match.
  3. The use of (_) for private members can't be said to be a simple 1-to-1 replacement since many of the properties that have been declared with _ are meant to be protected instead of private. So, for those who have complex libraries needing protected but still won't get it under this proposal, there is not much merit in making the effort to convert to the use of #. Not to mention that this will need to be done carefully and with much testing so as to avoid accidentally making a protected member private. What will be even more difficult for the larger libraries is ensuring that all members intended to be private have been marked as such.

BTW:
What I offered in counter shares a near symmetry while not disabling functionality (as in class { #x; y; m(){ return this#.x + this.y; } }). This simple variation maintains both visual and functional symmetry with existing code. It also maintains partial symmetry with the declaration in that in order to access or declare a private member, '#' must be present.

@rdking
Copy link
Author

rdking commented Sep 26, 2018

In a somewhat long-winded post here, I made the following arguments about preferring 'private' to '#' for member declarations:

  1. Everywhere else in ES, non-keywords never trigger declaration. It's always
    • var
    • let
    • function
    • class
    • or just
  2. Keywords are the primary method of declaring private fields in the languages that support the concept and style being borrowed.
  3. Punctuation characters (like #) are historically used as operators, not keywords in C-like languages (of which ES is one).

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?

@bakkot
Copy link
Contributor

bakkot commented Sep 26, 2018

In fact, if Base.x doesn't exist, this code won't compile. That has nothing to do with the type of x.

It has to do with the type of Base, and of foo. In particular, it has to do with whether the compiler can know at compile time which class contains the declaration of a particular field accessed on a particular expression, and whether the shape of objects can be known statically at all. If it can, it can statically enforce access restrictions (and, for example, fail compilation if properties are missing). If it can't, it can't. JavaScript's type system is not sufficiently static to do this. I really don't think that's misleading.

So basically, you're saying you prefer aesthetics over functionality?

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.

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?

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.

@rdking
Copy link
Author

rdking commented Sep 27, 2018

@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.

I am saying that aesthetics and ergonomics are important...

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 # actually is. Case in point:

class Example {
  #field = 1;
  member() {
    return this.#field;
  }
}

vs

class Example {
  private field = 1;
  member() {
    return this#.field;
  }
}

On the issue of ergonomics: You win with the shorter syntax.
On the issue of aesthetics: You lose for reduced readability and a declaration syntax that is inconsistent with everything else about the language. It's almost as if you're trying to turn ES into lisp.

...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.

Isn't it though? There are many different ES programming paradigms that make rampant use of the simple fact that obj.x is equivalent to obj['x']. These paradigms often help to DRY the code further than can be done otherwise. This is another point that works against the current proposal. Less DRY code means longer download and parse times. Marginal to be sure for most cases, but still not a good thing to force onto the developer for at-best arguable aesthetic reasons. It may not be a "blanket preference", but it is definitely a questionable one that will negatively impact developers.

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.

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:

  1. The changes I suggest lead to less questions in trying to understand the code.
  2. It takes on average 3 different questions from developers before they understand why the symmetry between . and [] is broken with your syntax.
  3. There were cases in testing where developers forgot to use the # after a . when accessing private members using your syntax regardless of who's proposal style was requested first.
  4. Given the same assignment using my suggested changes, there were significantly fewer developers who forgot to use # after the object name regardless of who's proposal style was requested first.
  5. Developers found the notation of your syntax aesthetically displeasing when having to deal with chained private fields (i.e. obj.#x.#y.#z) and no less so with mine (i.e. obj#.x#.y#.z).
  6. While accepting that options were scarce, there was a general aesthetic reluctance to accept use of the # in any context.
  7. While most developers enjoyed the brevity of # as a declarator for writing code, all but 1 preferred to use the private keyword for its familiarity and readability.

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 # will be the token for declaring private fields. Many library developers will then try to use # notation to protect their code only to very quickly realize that they are still missing support for protected. You've shown it can be somewhat reasonably done using a convoluted decorator and WeakMaps.

@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 protected support will roll their own implementation or copy one from the net. This will lead to libraries with custom, incompatible implementations of this feature. With enough time, the TC39 board will want to make it a language feature. Even though the 'protected' keyword is perfect for this use, they won't have a choice but to leave it as a decorator. This will limit the ability to improve functionality around this paradigm.

Why will this happen? Simply because you opted to use # as a declarator, a symbol instead of a word like all the other declarators in ES. I submit to you that your aesthetic taste doesn't have the easy mental image that you claim, and is far more costly in the long run than you're anticipating.

@bakkot
Copy link
Contributor

bakkot commented Sep 27, 2018

Case in point

I'm sorry, I didn't realize you were still proposing that declaration would be private x, rather than #x. As I've said before, I think anything which allows you to write

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 a is absolutely unacceptable. Very nearly identical code in a number of other languages has very different semantics. I think this used to be the first point in the FAQ and it is still the most important one. The confusion this would cause to readers and authors of code is far too high; if we could not find a syntax which avoided this problem, we would not add private fields to the language.

Yes, I know you are proposing that access would be this#.a rather than this.a. But your proposal still allows you to write the above code, and has it mean the wrong thing. It does not matter that there is a different, more correct thing which could have been written instead as long as this code does not error and does not have the semantics of accessing the private field, which it could not.

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 private x for declarations.

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.

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.

With enough time, the TC39 board will want to make it a language feature.

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 public).

@ljharb
Copy link
Member

ljharb commented Sep 27, 2018

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.

@rdking
Copy link
Author

rdking commented Sep 27, 2018

@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 protected will become a very high priority among developers using class within a short amount of time after they've adopted whatever form of private gets added. If you're willing to die on a hill so fruitlessly, that is your choice. Whether it's protected, friend, internal, or something else entirely, the need to selectively share private methods and fields will be demanded. Good luck avoiding that.

@bakkot

I'm sorry, I didn't realize you were still proposing that declaration would be private x, rather than #x. As I've said before, I think anything which allows you to write (example omitted), and have that constructor and method be referring to a public field named a is absolutely unacceptable.

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 private is what nearly every developer not aware of your proposal will be expecting to be able to use for declaring a private field in a class. You've given a seemingly reasonable aesthetic argument against it, namely:

Very nearly identical code in a number of other languages has very different semantics.

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 i = Integer(5); in Java return you a primitive 5? No, but running i = Number(5); in ES will. Will running this.foo() in the member function of a base class work if foo() only exists in the derived class that this is an instance of? Nope, but it works just fine in ES. I could go on for hours listing borrowed concepts that are conceptually the same in ES but semantically very different. The use of private would be no exception to this, and contrary to this baseless assertion of yours:

The confusion this would cause to readers and authors of code is far too high; if we could not find a syntax which avoided this problem, we would not add private fields to the language.

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. # takes just a bit more explaining since the testers were unfamiliar with it, but private was perfectly clear as to what was meant. Have you done any parallel testing of your own to verify your assertions?

Yes, I know you are proposing that access would be this#.a rather than this.a. But your proposal still allows you to write the above code, and has it mean the wrong thing. It does not matter that there is a different, more correct thing which could have been written instead as long as this code does not error and does not have the semantics of accessing the private field, which it could not.

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 # to declare a private field." Since var #field; is an immediate SyntaxError, they're likely to assume that # is a declarator, not part of the name. This is one place where our approaches differ. With your approach, the developer must think of # as both:

  1. part of the property name, and
  2. a marker signifying that the property name is private.

This is already a higher mental load than my approach, which only requires that the developer remember to use # on the owning object to access any private field. Another disadvantage for your approach looks like this:

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 # is to be thought of as a name character, even if it is only valid within classes, it only makes sense that it would be something dynamically addable just like public properties. That means not doing this becomes another thing developers will have to be taught and remember. Sure, the same thing might need to be taught to novices for my approach, but since private is already a well known concept, the lack of extensibility in the private container should come as no surprise. Look at the difference in what must be taught:

  • Yours: any member field who's name begins with a # must be present in the class declaration (counter intuitive).
  • Mine: the private field container of a class is not extensible (well known fact).

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.

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.

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.

@bakkot
Copy link
Contributor

bakkot commented Sep 27, 2018

So we agree on that issue. Your example is wrong for what I've proposed. Here it is corrected:

As I said:

Yes, I know you are proposing that access would be this#.a rather than this.a. But your proposal still allows you to write the above code, and has it mean the wrong thing. It does not matter that there is a different, more correct thing which could have been written instead as long as this code does not error and does not have the semantics of accessing the private field, which it could not.

I don't know how else I can say this.

Will running i = Integer(5); in Java return you a primitive 5?

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".

contrary to this baseless assertion of yours

"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".

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 # to declare a private field."

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.

Trading in something real for something fake is generally a bad thing.

We disagree about which concerns count as "real", I think.

Find someone you haven't already indoctrinated and write a small piece of sample code using both your approach and mine.

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 private x for declarations and which had something other than this.x for access. I don't think I've ever brought up yours in particular, but yours has the same flaw as several others which I have, which is that it allows the code in my previous comment. People who have written code in other languages with class and private have, in my experience, generally agreed that this flaw was fatal as soon as the possibility was pointed out to them.

But in any case this approach could not resolve the disagreement, because it is a disagreement about which things we value.

@rdking
Copy link
Author

rdking commented Sep 27, 2018

@bakkot That was beautiful! You managed to cherry pick against a straw-man argument and miss the point entirely.

Very nearly identical code in a number of other languages has very different semantics.

Will running i = Integer(5); in Java return you a primitive 5?

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".

The point you missed is that the only reason it works at all in ES is because the semantics of SomeType(val) are different between Java and ES. For Java, SomeType is a well-defined type structure with a function that is automatically called when this notation is used. For ES SomeType is just a function. What matters is that the concept of types was ported from other languages into ES, but the semantics were altered to accommodate the nature of ES.... and the language suffered no loss for it. Likewise, if ES absorbs the concept of private and uses the private keyword, but has to alter the semantics a bit to make it fit the language, ES developers who want the feature will feel no loss due to the semantics change.

But there's no objective standard for "acceptable".

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 # rises with code complexity. Neither syntax has any ability to mitigate that, and both syntaxes will suffer because of it. It's just the natural cost of trying to enforce undetectability.

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".

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.

Again a baseless assumption. Your syntax can be taught with the following 2 rules.

  1. To make a field private, begin its name with # (unintuitive)
  2. You cannot use a private field in this way: obj['#field'] (unintuitive)

My syntax also has 2 rules.

  1. To make a private field, use private
  2. To access a private field, append a # to the owning object name (unintuitive)

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.

@rdking
Copy link
Author

rdking commented Sep 27, 2018

@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.

@rdking
Copy link
Author

rdking commented Sep 28, 2018

@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 class instances. If it was acceptable to simply seal object instances after construction, then the entire issue would become moot and this.x would be valid for private fields. But I've already accept that this is not even going to be remotely considered.

However, the principle behind that simple suggestion still remains. Here's the real idea:

  • Make it a SyntaxError for a member method present in the declaration to access anything on an instance of the same class using . notation that was not part of the class declaration if the declaration includes private fields.

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 [] notation doesn't suffer from the same historical use issue that . notation does, so I'm thinking it's ok to exclude it. However, I'm not against making that a SyntaxError as well. It would certainly be more consistent to do so. However, I can imagine that there are scenarios where not being able to iterate through appended public properties would be disruptive.

Functions added to either the instance or the class prototype beyond the class definition won't be subject to this restriction. This makes sense given that they also won't have access to private fields. This idea means that for the example code you gave before:

class A {
  private a;
  constructor() {
    this.a = 1; //SyntaxError
  }
  m() {
    return this.a; //SyntaxError
  }
}

Does this reasonably mitigate the issue for you?

@rdking
Copy link
Author

rdking commented Sep 28, 2018

Just in case it must be fully stated:

  • The restriction only applies to declared member functions of a class declaring private fields and is not affected by the prototype chain.

@robpalme
Copy link
Contributor

@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 a on another class of object B. But now any user can now abuse the API to access A's private a.

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.

@rdking
Copy link
Author

rdking commented Sep 28, 2018

@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 . operator is running.

Your proposed dereferencing syntax is identical for public and private fields...

Not true. The syntax I'm proposing for accessing private fields still includes the #. The difference is that # is a binary operator instead of a name character, and its effect is to retrieve the private container owned by the LValue. The RValue for this operator is always an access operator('.' or '[]'). If that seems awkward, an equivalent way of saying it is that # is a postfix operator, and it's a SyntaxError if you don't immediately follow it with an access operation. So it's just obj#.x for my suggestion instead of obj.#x as in the current proposal.

@rdking
Copy link
Author

rdking commented Sep 28, 2018

Let me restate the rules for this idea:

  • Make it a ReferenceError for a member method present in a class declaration to use . notation to access anything on an instance of the same class that was not part of the class declaration if the declaration includes private fields.
  • The restriction only applies to declared member functions of a class also declaring private fields and is not affected by private fields occurring on class definitions appearing in the prototype chain.
  • When active, the restriction does not prevent access to declared members of any ancestor class.

@ljharb
Copy link
Member

ljharb commented Sep 28, 2018

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.

@rdking
Copy link
Author

rdking commented Sep 28, 2018

@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 or one of its ancestors. This only prevents declared member functions from being able to dynamically access properties on the object using '.' notation. Here's a (hopefully) clarifying example:

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 .#, and a potential ASI hazard. It's in the FAQ. While that notation has the advantage of using # as an operator, it still fails to support symmetry with []. Put another way: if # is the private version of ., then what's the private version of []? You could try #[] but that would have the same feel as .[], which is illegal. Not an insurmountable problem, though.

@ljharb
Copy link
Member

ljharb commented Sep 28, 2018

Forcing bracket access to a normal property from inside the class is also unacceptable, and would violate almost every common styleguide.

@rdking
Copy link
Author

rdking commented Sep 28, 2018

You missed something again. Look at the Ex3 example I gave before. If you want to use . notation to access a public field, just make sure to declare that field as part of the class definition. Basically, I'm saying that if the issue of accidentally accessing public fields when you meant to access private fields is such a big deal, then don't allow developers to be sloppy in the presence of private fields.

@ljharb
Copy link
Member

ljharb commented Sep 28, 2018

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?

@rdking
Copy link
Author

rdking commented Sep 28, 2018

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. Base adding pi that way is the same as if it had been done external to the class. Derived has no way of knowing it's there.

... 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. Derived can see that Base has a pi pubic field, so that field can be accessed.

-- Edit note: forgot about super()....

@rdking
Copy link
Author

rdking commented Sep 28, 2018

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 Base is not a class, I would want to rely on the prototype. That means this would fail since pi is not in Base.prototype.

@ljharb
Copy link
Member

ljharb commented Sep 28, 2018

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.

@rdking
Copy link
Author

rdking commented Sep 28, 2018

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 . notation from within the methods of a class that declares private fields.

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.

We agree on this. So is there a use case I'm not considering that makes this a major concern?

@ljharb
Copy link
Member

ljharb commented Sep 28, 2018

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 this.foo?

@rdking
Copy link
Author

rdking commented Sep 29, 2018

I'm assuming that the child class has a private field, because if it doesn't, then there's no issue at all.

  • Before installing foo on the parent, you'd get a ReferenceError because you'd be trying to create a public field on the instance.
  • After installing foo on the parent, there's no issue since foo is defined by the prototype chain.

@ljharb
Copy link
Member

ljharb commented Oct 2, 2018

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 const r = new Record('a', { id: 'b' }); [r.getPrivateID(), r.getID(), r.get('id')] return? I'd expect ['a', 'b', 'b'].

@rdking
Copy link
Author

rdking commented Oct 2, 2018

It throws a ReferenceError because r.getID() tried to access the undeclared this.id. It should have been this:

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 id when you obviously knew you were going to use that field when you wrote this.id. There's no chance of ambiguity when using . notation to access a field. So there's no harm in requiring it either.

@ljharb
Copy link
Member

ljharb commented Oct 2, 2018

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.

@rdking
Copy link
Author

rdking commented Oct 2, 2018

Then there is also either:

  • no need for private properties to be explicitly declared, or
  • no need for private properties to co-exist with public properties of the same name.

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.

@ljharb
Copy link
Member

ljharb commented Oct 2, 2018

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.

@rdking
Copy link
Author

rdking commented Oct 2, 2018

...variables aren't own properties either.

Bag the straw-man argument please. If you have to access it by preceding it with a variable name and a ., then it is a property of something. Therefore a private field is a property of something. That something is either:

  1. the object from which you access it
  2. some other object which is being accessed indirectly

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.

...to require ( explicit declaration) for normal properties, however, does not match the idioms of the language since its inception.

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.

  1. Is the # an operator or part of the [[IdentifierName]]?
  2. Is a private field a non-own property of the accessing object?

Depending on how you answer these, what is logically reasonable will change.

@bakkot
Copy link
Contributor

bakkot commented Oct 2, 2018

Is the # an operator or part of the [[IdentifierName]]?

Neither.

Is a private field a non-own property of the accessing object?

These terms are not sufficiently well defined for there to be a meaningful answer to this question.

@rdking
Copy link
Author

rdking commented Oct 2, 2018

Is the # an operator or part of the [[IdentifierName]]?

Neither.

Then what is it?

Is a private field a non-own property of the accessing object?

These terms are not sufficiently well defined for there to be a meaningful answer to this question.

If an "own property" of accessing object obj has a name name that satisfies obj.hasOwnProperty("name")===true and is accessible via at least one of obj.name or obj['name'], then for the same object, a "non-own" property's name fails to satisfy the hasOwnProperty condition. Clear enough? Please answer question 2.

@bakkot
Copy link
Contributor

bakkot commented Oct 2, 2018

Then what is it?

A new kind of thing.

Clear enough?

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.

@rdking
Copy link
Author

rdking commented Oct 2, 2018

No, I don't know what you mean by "property".

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 obj.x, x is a property of obj. Use the definition of property nearly everyone using ES-based languages use. Please answer question 2.

Then what is (#)?

A new kind of thing.

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?

  • Keyword
  • Operator
  • Literal
  • Separator
  • Identifier
  • Terminator

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 #?

@ljharb
Copy link
Member

ljharb commented Oct 2, 2018

@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.

@bakkot
Copy link
Contributor

bakkot commented Oct 2, 2018

Use the definition of property nearly everyone using ES-based languages use.

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?".

If not, then what exactly is it?

Like I said, it's a new kind of thing.

Currently everything in the syntax for ES falls into these categories exclusively with varying degrees of overloading

I don't think this is so. ..., {, (, =>, . - none of these fall cleanly into the above categories, unless you're defining them in an unusual way.

Anyway, I don't think this line of discussion is likely to be productive. I'm going to bow out now. Apologies.

@rdking
Copy link
Author

rdking commented Oct 2, 2018

@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 # is "something new", he should be able to categorize that something. If the use of the # is something he is incapable of categorizing, then that is a serious failing for this proposal.

@bakkot Please tell me you're not being serious.

  • ...: Spread operator
  • {}: Object/block literal
  • (): Call/group operator
  • =>: Arrow function literal
  • .: Property access operator

You left one out:

  • []: Array literal/Property access operator

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 # doesn't have such a distinction because the contexts overlap. This is problematic.

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.

@rdking
Copy link
Author

rdking commented Oct 3, 2018

@bakkot Can I help you with this a little?

If we have to keep your proposal as is, then I suggest you declare # to be a literal token. The only way at all that I can justify the way you're using it, is to treat # the same way as =>. In this way you could call it a "private field literal", meaning only that it must appear wherever a private field appears. It would have no meaning beyond that. That means it only exists to disambiguate private field syntax from all other similar syntaxes.

Is this even close to the "it's a new kind of thing" you kept saying?

@rdking
Copy link
Author

rdking commented Oct 3, 2018

@nicolo-ribaudo I'm putting part of my response to your post here instead...

As for what # is, I think that "# is a sigil which introduces the name of a private property" could be a good definition.

Not really. The definition you gave satisfies its use as a literal in a declaration, but the # is also being used in access notation. If you had instead said "creates/retrieves(introduces)" instead of "introduces", that would have worked in both cases. The word "introduce" doesn't really fit when the thing being introduced doesn't yet exist. Even with the suggestion I've given, it's a still a basically a literal as described in my previous post.

@littledan
Copy link
Member

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 #-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.

@Igmat
Copy link

Igmat commented Oct 10, 2018

@littledan you just dropped all alternatives without proper reasoning. It's the worst decision you could made...

@littledan
Copy link
Member

@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.

@rdking
Copy link
Author

rdking commented Oct 11, 2018

@littledan By saying

I don't agree with all of the arguments in favor the current approach...

Are you agreeing that there are flaws in the arguments that back this proposal? By saying

...but overall I don't really see how we could move to one of the alternatives.

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?

@littledan
Copy link
Member

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.

@rdking
Copy link
Author

rdking commented Oct 11, 2018

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.

@trusktr
Copy link

trusktr commented Jan 8, 2019

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,

Why doesn't this['#x'] access the private field named #x, given that this.#x does?

  1. This would complicate property access semantics.

Of course it would. That's why we can think of alternatives like this.#['foo'+'bar'], this[#'foo'+'bar'], etc, which would work perfectly fine.

@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?

@ljharb
Copy link
Member

ljharb commented Jan 8, 2019

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).

@trusktr
Copy link

trusktr commented Jan 8, 2019

There's need. I might want to define property types (a runtime type system) as static private properties on a class, where the keys are property names, and the values are object containing meta info about the types. For example, this is what SkateJS does. And I need the class instances to have dynamically generated private names as well as matching attributeChangedCallback handlers (they are Custom Elements) that can detect use the private static type map to check if attribute handling is allowed for a given attribute name, and otherwise throw an error.

I can keep imagining!...

@ljharb
Copy link
Member

ljharb commented Jan 8, 2019

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?

@trusktr
Copy link

trusktr commented Jan 8, 2019

The type system runs internally inside each class, and the definitions stored in the private static map are the type system. The meta info includes functions like serialize/deserialize/coerce/get/set, etc. Those meta functions contain type checking logic.

It's not an external type system, otherwise obviously it would only work on public properties.

@trusktr
Copy link

trusktr commented Jan 8, 2019

It wouldn't work well with inheritance perhaps... but that's where protected comes in.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

7 participants