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

Treat the # as part of a .# operator instead of as part of the variable name #foo. #113

Closed
trusktr opened this issue Jun 26, 2019 · 21 comments

Comments

@trusktr
Copy link

trusktr commented Jun 26, 2019

The following should work, and it would mean that we can provide much better functionality:

class Foo {
  #      x = 5

  test() {
    console.log(this  .#      x) // it should work, but it doesn't
  }
}

It didn't work in Chrome (I am doubting it is a bug in Chrome, and that is it working as spec'd, which throws a syntax error).

Looks like the #foo is treated as a name, which is what makes things like indexed-access not currently possible (or so it seems).

If we change the semantics so that .# is an operator (f.e. it means "access a private property, of which the name follows"), and can be separated by spaces just like with public access using ., then we can accommodate other features like indexed access in a way that makes sense:

const propName = 'bar'

class Foo {
  foo = 3
  # foo = 4
  [# propName ] = 5

  test() {
    console.log(this  .      foo) // it works, 3
    console.log(this  .#      foo) // it should work, 4
    console.log(this[#'foo']) // it should work, 4
    console.log(this   [#   'foo' ]) // it should work, 4
    console.log(this  .#      bar) // it should work, 5
    console.log(this [#    propName ]) // it should work, 5
  }
}

because now the syntax is tied to the type of access you want to attempt, not tied to the name of the field.

@trusktr
Copy link
Author

trusktr commented Jun 26, 2019

So, then, we can have more dynamism and be closer to JavaScript origins:

const privateProps = ['foo', 'bar', 'baz']

class Foo {
  constructor() {
    let i = 0
    for (const privProp of privateProps) {
      this[#privProp] = i
      // or with any spacing this[ # privProp  ] = i
      i++
    }
  }
}

With creativity, you can imagine use cases for this, like mapping a definition from an external helper (such as a decorator, mixin, or class factory) to private properties.

@rdking
Copy link

rdking commented Jun 26, 2019

Good luck with that. You're not going to get them to bite. I tried that suggestion (making .# a private operator). I argued with @bakkot I believe about giving # a clear classification in each of its use cases, rather than just saying "it's something different", but that went nowhere. So, good luck. I hope yours is better than mine.

@trusktr
Copy link
Author

trusktr commented Jun 26, 2019

@rdking Were there any particular problems that couldn't be solved?

@trusktr
Copy link
Author

trusktr commented Jun 26, 2019

Oops, I posted in the wrong place anyway. I'll open an issue in the other repo.

Private fields should not be merged to the other repo. in all honesty.

@trusktr
Copy link
Author

trusktr commented Jun 26, 2019

Moved to tc39/proposal-class-fields#250

@ljharb
Copy link
Member

ljharb commented Jun 26, 2019

They already were merged, a long time ago. They will not be split. They are shipping at stage 3. # is part of the name and will forever be.

This stuff has all been rehashed ad nauseum already.

@rdking
Copy link

rdking commented Jun 26, 2019

@ljharb Feisty!

Q: Other than the [[Set]] vs [[Define]] debate, public fields is readily acceptable to the uncontested majority of ES developers. Why is it not ok to split them and let public fields go to stage 4? Or if that's not even an acceptable question, then why were public and private fields merged in the first place? They're similar. I get that, but in the interest of an MVP proposal, which seems to be the current approach, what sense does it make to merge a virtually uncontested proposal with a highly contested one? Unless the goal was to force something onto the community, this seems to need some explaining, especially since there doesn't seem to be any technical requirement for private fields to be released concurrently.

@bakkot
Copy link
Contributor

bakkot commented Jun 26, 2019

@rdking The proposals were merged because we felt it was impossible to design one in the absence of the other. Having merged them once, the committee has very little appetite for splitting them again.

Also, splitting them really would not affect anything.

@rdking
Copy link

rdking commented Jun 28, 2019

Let me see if I understand what I'm reading. You're saying it was impossible to add public fields to class without also doing private fields? On first blush, that makes no sense at all. Public is the only state that ES till now understands. Whether on the prototype or as instance properties, the result would have been pretty much the same. There's nothing about public that requires any private considerations.

If, however, you're saying that private fields couldn't be designed without interfering with the design of public fields, then I have to call bologna again. The simple reality is that since the inception of WeakMap, private data has been being implemented completely without regard to public properties for some time now.

So I have to say that I don't understand what the difficulty was. Is there any chance you can elucidate? There has to be something I'm not seeing. I'm sure others are curious as well.

@ljharb
Copy link
Member

ljharb commented Jun 28, 2019

Yes, the syntax for private fields had to be designed at the same time as the syntax for public fields (ideally also the semantics, so they can form a cohesive mental model - which it is, even if you’re not a fan of it)

@rdking
Copy link

rdking commented Jun 28, 2019

I agree that the model between public and private fields is cohesive. What I do not understand is how that matters at all. ES has always had public properties. There's no 2 ways about that. There's also nothing particularly special about a "field" seeing as how it's just a deferred declaration of an instance property. The only things that needed to be decided to end up where we are with public fields are:

  1. Should class public data declarations end up on the prototype or directly on instance objects?
  2. Should class public data declarations use [[Set]] or [[Define]] semantics.

Those really are the only 2 considerations outside of the syntax. Everything else is already defined, including the mental model. From your perspective, question 1 answers itself since there is this uncannily strong desire to avoid the "instance object on the prototype" foot-gun. Question 2 has only 1 answer if the data is placed on the prototype: use [[Define]]. Otherwise, what's left is the very argument that's still a touchy point among non-TC39 members. So in the end, regardless of the presence of a proposal for private fields, public fields would have ended up right where they are now.

Now, are you saying that if public fields had've gone to stage 4 without private fields, that it would have been impossible to design private fields to share a cohesive mental model with public fields? If that's not what you're saying, then I'm afraid I need to ask for a more detailed explanation of the problem.

@bakkot
Copy link
Contributor

bakkot commented Jun 28, 2019

There were many other syntactic and semantic decisions which had to be made, and most of them were relevant to both proposals.

If we had designed one with no consideration of the other, it is possible we'd've ended up with a design which the other could have been grafted onto with a minimum of fuss, but it is at least as likely we would not have. It did not make sense to design them separately.

Again, this is mostly just a historical note about the process. The proposals are merged and are extremely unlikely to be unmerged, and unmerging them would accomplish nothing anyway because we have consensus on the entire design, including both public and private fields.

@rdking
Copy link

rdking commented Jun 28, 2019

@bakkot Don't worry. I've only been looking for the history. I'm long past expecting any result from arguing about both the process and results behind this proposal. Save for some unexpected event, I'm also way past hope for either halting this proposal, or at least correcting the rather grievous design flaws and technical trade-offs that force developers to give up on common design patterns or receive shocking results. (No slights intended. This is just how I feel about the technical merits of this proposal.)

If I thought there was any chance at all, I'd want to see if I could at least change your perspective regarding the smallest of the technical problems (set vs define). But to even carry out that debate, I'd need to have a clear understanding of the intended goals and requirements. From what I understand, there's already a clear conflict of interest. However, I can't imagine that the TC39 members didn't already notice that when they made their decision.

@rdking
Copy link

rdking commented Jun 28, 2019

@bakkot

There were many other syntactic and semantic decisions which had to be made, and most of them were relevant to both proposals.

I'm not worried about the syntactic decisions. While it still think it could have easily gone in a better direction, I get how you ended up where you did. It's the semantic decisions that I'm interested in as they are breaking common paradigms. Thinking only of public fields, can you elaborate on what semantic decisions needed to be made that amounted to more than maintaining the existing nature of public instance properties?

@bakkot
Copy link
Contributor

bakkot commented Jun 28, 2019

Thinking only of public fields, can you elaborate on what semantic decisions needed to be made that amounted to more than maintaining the existing nature of public instance properties?

Sure: when do initializers run (both for base classes and for subclasses); in which scope are they evaluated; how do this, arguments, new.target, and super evaluate in initializer bodies; are they installed in a two-phase process, where all properties are set up with value undefined and then initialized, or a one-phase process, where properties are not installed at all until their initializer runs; should fields be data properties on instances or should they be accessor pairs on the prototype for an internal slot on the instance which is not exposed to user code; should there be a way for the constructor to manually refer to or invoke initializers... others I'm forgetting, I'm sure.

That said, the syntactic questions alone would have been sufficient reason to merge the proposals, once they were both stage 2.

@rdking
Copy link

rdking commented Jun 29, 2019

@bakkot Thank you again for being so forthcoming with these answers.

Just as a matter of note, my answers would have been:

  • When do initializers run?
    • For prototype-based public properties: At class evaluation time, just like class methods.
    • For instance-based public properties: At constructor time, immediately after super(). Just as the developer would expect from something purporting to allow constructor-less initialization.
  • In which scope are they evaluated?
    • For prototype-based public properties: In the scope of the function in which the class is declared. I.e. no changes.
    • For instance-based public properties: In the scope of the constructor. No changes.
  • How do this, arguments, new.target, and super evaluate in initializer bodies?
    • For prototype-based public properties: Error as they are not available. No changes.
    • For instance-based public properties: Just as they do for any code run in the constructor. No changes.
  • Are they installed in a one-phase([[Define]]) or two-phase([[Set]]) process?
    • For prototype-based public properties: One-phase. No changes.
    • For instance-based public properties: Two-phase. No changes.
  • Should there be a way for the constructor to manually refer to or invoke initializers?
    • For prototype-based public properties: No, since the initialization happens during class evaluation. No changes.
    • For instance-based public properties: No, since the only point in doing so is to provide default values initialized before any declared methods of the class (not its ancestors) can be invoked. That includes post-super constructor logic.

Up to this point, I don't even see these as questions since I would continue with the existing semantics. There really is no need to change how any of that works. Doing so would lead to unwanted and surprising results for common ES programming patterns. As for the rest:

should fields be data properties on instances or should they be accessor pairs on the prototype for an internal slot on the instance which is not exposed to user code

Was this seriously a possibility? If this is how properties like Array.prototype.length are (conceptually?) implemented, then I can see the question. However, this doesn't track at all with how properties are added to objects. Wherein it keeps defined fields on the prototype, this is good. However, doesn't it fall apart when the class prototype is attached to a non-instance object? Developers doing this would get the surprising result of an error because the internal slot won't exist for the non-instance. Not a good trade. Stick with existing property semantics.

If instead the internal slot can be created by the accessor's setter, then this is even better. However, it still falls apart if someone calls getOwnPropertyDefinition since they'll get back an accessor definition instead of the property definition they were expecting. So again, stick with existing property semantics. Short of flagging the accessor as a field somehow, it would prove difficult to fake the developer's expectations consistently, especially under edge cases.


That was fun. Hopefully it also helped you understand where I'm coming from with what I've been saying. I get that there were many possible alternate paths that could have been taken and other questions you haven't listed. However, anything that deviates unnecessarily from the path ES has followed thus far regarding how a public property should behave should automatically be considered unacceptable. That's why I was able to reduce it down to just the 2 questions I listed. The answer to all others is "follow the existing pattern".

@hax
Copy link
Member

hax commented Jul 2, 2019

Yes, the syntax for private fields had to be designed at the same time as the syntax for public fields (ideally also the semantics, so they can form a cohesive mental model - which it is, even if you’re not a fan of it)

The truth is, though public fields and private fields have similar syntax (what I called "duality"), the semantic of them are totally different (property-based vs weakmap-based). We already see many complains in the issue list because of that.

To some degree, I agree public/private need to be designed together but unfortunately current design just fail on "form a cohesive mental model".

@gynet
Copy link

gynet commented Jan 7, 2020

I will not be surprised the proposal will cause more trouble to people once it releases

@trusktr
Copy link
Author

trusktr commented Jan 7, 2020

@ljharb

Yes, the syntax for private fields had to be designed at the same time as the syntax for public fields

Or after, not necessarily at the same time. I don't think anyone can beat @rdking's argument about that in his last comment.

@ljharb
Copy link
Member

ljharb commented Jan 8, 2020

@trusktr no, at the same time, since choices made for either private or public fields could impact the other, and that's not a risk worth taking imo.

@littledan
Copy link
Member

We've discussed the issue here thoroughly, and rehashed many of the reasons why we had previously concluded the syntax to be what it is.

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