proposal-class-fields icon indicating copy to clipboard operation
proposal-class-fields copied to clipboard

Developer experience of the syntax and semantics of this proposal

Open mbrowne opened this issue 7 years ago • 87 comments

This new issue is intended as a continuation of the recent discussion in #100. (The only reason for a new thread is that the original has become very, very long, which among other things makes it less approachable to anyone not already participating in the discussion.)

VERY IMPORTANT NOTE: Anyone who would like to comment here should first read the PRIVATE SYNTAX FAQ. This will save everyone time. Thank you.

mbrowne avatar Nov 23 '18 17:11 mbrowne

Hi, I've been programming with JS for about six years now. I never intended to influence the standard, because most new things were indeed quite useful and made sense. But I have a feeling that the language is now being bloated with too many and partially weird things, just like the # char to declare private fields.

The strengths of JS are - among others - that it's simple and easy to learn for devs coming from C/C++, Java or PHP.

So, we have syntax and classes like in some of these languages. Why not just do something like:

class MyClass {
    myVar = 3; // public field
    public otherVar = 4; // also public field
    private yetAnotherVar = 5; // private field

    protected myMethod () {
        // do things here...
    }
}

As you can see, adding access modifiers is optional in this example, the default one being public. Public fields will simply be properties on the instance object that can be accessed normally from outside the class, whereas attempting to access private/protected from outside will throw an error e. g.:

const myObject = new MyClass();

myObject.myVar; // returns 3
myObject.yetAnotherVar // throws Error

And while I'm at it, athough this is OT:

function myFunc (String name, Number age, cityOrCode) {
    // name is definitely a String
    // age is definitely a Number
    // cityOrCode can be any type
}

let myVar = 3; // normal variable, any type
let String otherVar = 'some string'; // can only be assigned a string type

My point is: JS should be simple and not have unexpected, weird syntax. Development of the language should focus on making a developers life easier and allowing noobs to quickly understand code. In my opinion, it's better to not have a feature, than to add a weird or complicated one (Object.assign, # for private, ...)

nabil1337 avatar Nov 24 '18 01:11 nabil1337

It would be unexpected and weird if "private" things were publicly visible properties that threw on access, exposing their existence.

ljharb avatar Nov 24 '18 02:11 ljharb

It would be unexpected and weird if "private" things were publicly visible properties that threw on access, exposing their existence.

Good point. Maybe make private and protected fields not enumerable. Though I think this would only be an issue in those cases, where you attempt to add random properties to instance objects - which is bad style anyway, e. g.:

let myInstance = new MyClass();
myInstance.someRandomProp = 3; // bad style, unless someRandomProp is a known public field.

nabil1337 avatar Nov 24 '18 02:11 nabil1337

Whether it's a bad style or not isn't the point - it's a core feature of javascript that objects are mutable unless otherwise made immutable, and privacy isn't related to mutability.

ljharb avatar Nov 24 '18 02:11 ljharb

Whether it's a bad style or not isn't the point - it's a core feature of javascript that objects are mutable unless otherwise made immutable, and privacy isn't related to mutability.

I would expect privacy to be related to mutability, just as it is the case in other languages. If the standard doesn't yet allow this to be the case, shouldn't it be adjusted first? In other words: If the standard limits you from implementing fields the easy way, why would you implement them in a hard to grasp way instead of adjusting the standard first, before attempting to implement fields?

nabil1337 avatar Nov 24 '18 02:11 nabil1337

JS is constrained by backwards compatibility, so no, that's not something that can ever be changed.

ljharb avatar Nov 24 '18 02:11 ljharb

@nabil1337 It sounds like you might not have seen or read the FAQ about the private fields syntax. Actually this prompted me to add a disclaimer to my first post here encouraging everyone to read that FAQ as a prerequisite for commenting on this thread. This is not intended to be overly strict or unwelcoming but simply to make the discussions here more productive. This is especially important given that the private field syntax in particular has been discussed very extensively especially in the past couple years, and the syntax you suggested has already been suggested by many others and comes with some very significant tradeoffs that have been discussed many times.

Of course, as you can see by reading through comments in #100, the syntax is still quite controversial even among those who are aware of the tradeoffs. But it's important to realize that syntax like this:

class MyClass {
    private myPrivateField = 1; // private field

    someMethod() {
       console.log(this.myPrivateField);
    }
}

...would actually make it impossible to have fields that are 100% private. This relates to another past discussion that you might want to familiarize yourself with, regarding the desire for "hard private" and the tradeoffs between hard private and soft private: #136. And just to counterbalance what @ljharb said, in many other languages you can always access private properties via reflection, so private doesn't have to mean 100% hard private, but the committee has landed on hard private being an important requirement especially for library authors (for more on that as well as counterarguments, see #136 as well as earlier discussions in #100 and other threads).

After years of careful deliberation, this class fields proposal is already in stage 3 which means it's highly unlikely that it will be changing much. The main reason for continued discussion is that there are still some alternative proposals that some community members are interested in, primarily these three:

  • https://github.com/zenparsing/js-classes-1.1
  • https://github.com/rdking/proposal-class-members (a modified/expanded version of classes 1.1)
  • https://github.com/zenparsing/proposal-private-symbols

As @littledan (a committee member) has communicated, the TC39 committee has reaffirmed consensus on the class fields proposal multiple times, so keep in mind that these alternative proposals are unlikely to succeed... but since many people still have concerns with the current proposal, there is still interest in discussing them, improving them, and advocating for them in the hope that the committee might hold off on a final decision and reconsider other options one more time.

(Side-note: Personally I am more interested in advocating some smaller adjustments to the current proposal rather than rejecting it completely, and I believe the use of a prefix like # is the best possible option for private fields given the constraints. But the above summary is not a reflection of my opinion but rather my attempt at a realistic description of the situation right now, so as not to give anyone unrealistic expectations.)

mbrowne avatar Nov 24 '18 04:11 mbrowne

@mbrowne Thank you for explaining! Sorry I didn't notice the FAQ.

I have some remarks:

The FAQ states:

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.

It explains this as follows:

If private fields conflicted with public fields, it would break encapsulation; see below.

Futher:

Library authors have found that their users will start to depend on any exposed part of their interface, even undocumented parts.

The simplest solution would be to just throw an error when trying to access these special fields, as I proposed. It doesn't really matter if someone knows that there is a private field foo, as long as they are unable to get it's contents. This is probably also what #136 meant.

My approach:

  • more similar to other languages, faster to learn
  • not propagating the anti pattern of having duplicate field names (imagine forgetting the '#' or adding it when not needed)
  • not having to deal with this['#x']
  • allowing for protected
  • no weird characters involved

I know, this has apparently been discussed for years. But as a normal dev, you don't really get to know all these things too quickly. And please consider the largely negative feedback from the recent discussion. Shouldn't the people who actually use the language be able to decide if they want this proposal? Thanks for your time.

nabil1337 avatar Nov 24 '18 04:11 nabil1337

Yes, it absolutely matters, because it means adding, renaming, or removing private fields becomes observable and thus part of the public api. Its contents are irrelevant, its mere existence is information that must not be obtainable.

ljharb avatar Nov 24 '18 05:11 ljharb

In addition to @ljharb's comment, it would also be really annoying from an ergonomics point of view if a private field on a base class prevented a similarly named public field or method on a subclass. Among other things, it would mean that adding a private field would always be a potentially breaking change.

Shouldn't the people who actually use the language be able to decide if they want this proposal?

A great many people involved in this discussion actually use the language quite heavily both personally and professionally, myself included.

bakkot avatar Nov 24 '18 06:11 bakkot

operationally, how many of you would actually enjoy maintaining web-projects with polymorphic-classes with subclassed-properties having the same names as [unobservable] private-fields?

this feels like a low-level hammer for low-level general-purpose programming things encountered in java/c++, not a high-level glue-language like javascript used by most of us primarily for baton-passing serialized JSON-objects around (with async static-functions).

kaizhu256 avatar Nov 24 '18 07:11 kaizhu256

operationally, how many of you would actually enjoy maintaining web-projects with polymorphic-classes with subclassed-properties having the same names as [unobservable] private-fields?

@kaizhu256 The real problem is, one would never know what private fields are used in base class as they're not part of your project, and they're also not documented as not being part of the public API.

So that whenever trying to extend an external class, your derived class would break weirdly. Even it doesn't break now (when you writes them), when the base class add a new private field suddenly your class is broken.

trotyl avatar Nov 24 '18 07:11 trotyl

@trotyl, the problem is deeper than that. i feel the whole class-based approach to solving async, UX-workflow problems commonly encountered in javascript is flawed.

class-methods are ok for blocking-code design-patterns in java/c++, where adding new features simply means inserting new blocking code between-the-lines of existing blocking code.

class-methods are not ok in async UX-programming where commonly added features like autocomplete, autovalidation, file-upload-progress, etc... often involve rewriting the entire ux-workflow (and any io-based class-methods with it). most of these tasks are more elegantly solved with callback-based static-functions.

kaizhu256 avatar Nov 24 '18 08:11 kaizhu256

@ljharb

Yes, it absolutely matters, because it means adding, renaming, or removing private fields becomes observable and thus part of the public api

It's a bit of a stretch to say it's part of the "public api" if that weren't the author's intention at all and they indicated it with private, but this is indeed an important consideration for anyone writing classes that will primarily be consumed by others. I found the code example in this comment by @bakkot quite helpful for understanding this issue.

Another significant issue is that without a prefix, the interpreter wouldn't have an immediate way to distinguish between private and public at the location in the code where a field is being accessed. For a compiled language this wouldn't be a big issue, but since JS is an interpreted language, it would make all property/field access more expensive (at least at class evaluation time). @nabil1337 that's why your suggestion of just throwing an error if someone attempts to incorrectly access a private field is not as great of a solution as it seems, because the interpreter would have to check for all possible base classes and prototype modifications on every private field declaration to make sure there isn't already a public field with that name, and some of the accesses as well (at least the dynamic ones).

For these reasons, even the alternative proposals that community members here are still seriously considering include dedicated syntax for accessing private fields. There is also one other option that I think is very much worth considering, which is keeping # but adding the keyword private to the declarations (e.g. class MyClass { private #x = 1; ... }). That would make the syntax more consistent and easier to explain in the future if other access levels besides public and private were ever added.

BTW @nabil1337, I'm not sure if you're already aware of this, but this['#x'] is not a valid way of accessing a private field in this proposal. That would access a public property named '#x'. This is an unfortunate but necessary consequence of the rest of this proposal given the need for backward compatibility.

mbrowne avatar Nov 24 '18 14:11 mbrowne

@nabil1337

Shouldn't the people who actually use the language be able to decide if they want this proposal?

The committee members are fortunately very familiar with real-world usage and concerns, thanks to first-hand experience as @bakkot mentioned. (They have also done community outreach outside of this repo, which the committee members could speak to better than me.) As for me, I use the language every day as a frontend (and sometimes full-stack) web developer and I am not a member of the committee, just someone who noticed this github repo a while back and started participating in discussions. Despite a few lingering concerns, I think this proposal is still better overall than any alternative; unfortunately we're fairly constrained by the existing language in some ways and no solution is going to be perfect. There are plenty of other members of the community who agree. However, they're not well-represented here because those who are satisfied with the proposal have less motivation to comment. As to the great number of people who continue to oppose the # syntax, it's unclear how many of them have fully looked into the consequences of alternative options such as the syntax you suggested. Certainly some people have fully considered the issue and still object to #, but it's important to learn about the tradeoffs that come with each of the alternatives.

mbrowne avatar Nov 24 '18 14:11 mbrowne

Thanks for all the replies.

@ljharb

Yes, it absolutely matters, because it means adding, renaming, or removing private fields becomes observable and thus part of the public api. Its contents are irrelevant, its mere existence is information that must not be obtainable.

It's not part of the api if it's private. Knowing about the existence of a private property that you cannot access doesn't really matter in day to day programming.

@bakkot

In addition to @ljharb's comment, it would also be really annoying from an ergonomics point of view if a private field on a base class prevented a similarly named public field or method on a subclass. Among other things, it would mean that adding a private field would always be a potentially breaking change.

That's an important point. If in a child class, it must be possible to define a field or method by the same name. If we look at PHP (by far not my favorite langauge, but widespread), we are allowed to have this code:

class ParentClass {
    private $field = 1;

    public function print () {
        echo $this->field;
    }
}

class ChildClass extends ParentClass {
    public $field = 3;
}

$child = new ChildClass();
echo $child->print(); // prints 1
echo $child->field; // prints 3

This is intuitive, because the parent method can fulfill the author's intent without being misled by whatever the child classes' field value is.

@mbrowne

that's why your suggestion of just throwing an error if someone attempts to incorrectly access a private field is not as great of a solution as it seems, because the interpreter would have to check for all possible base classes and prototype modifications on every private field declaration to make sure there isn't already a public field with that name, and some of the accesses as well (at least the dynamic ones).

Not quite sure if I understand you here. Isn't this a one time scan that could easily be stored in memory?

I'm not sure if you're already aware of this, but this['#x'] is not a valid way of accessing a private field in this proposal.

That's why I pointed it out as a disadvantage. Because it's inconsistent: You can do this.#x but not this['#x']. But then you can do both this.x and this['x'].

As to the great number of people who continue to oppose the # syntax, it's unclear how many of them have fully looked into the consequences of alternative options such as the syntax you suggested. Certainly some people have fully considered the issue and still object to #, but it's important to learn about the tradeoffs that come with each of the alternatives.

I agree. I might not be aware of all consequences yet, this is why I'm hoping to learn. But it's not easy to find the right place to start. Information seems to be scattered to serveral issues and text files.

One more question I couldn't find an answer to: What about protected fields and methods?

nabil1337 avatar Nov 24 '18 21:11 nabil1337

Not quite sure if I understand you here. Isn't this a one time scan that could easily be stored in memory?

I don't have personal experience with the inner workings of the JS engine (aside from outwardly observable behavior) so there are others who would be better qualified to describe this in detail. But I do know that unprefixed private fields (sharing the same namespace as public fields) would complicate the current semantics and add some amount of additional overhead, whereas the current proposal would keep the current semantics for public property lookups unchanged. Yes, the field declarations at the top of the class could be scanned once and stored in memory, but there are more significant differences when it comes to the places (call sites) where fields are accessed. # immediately signals a private field meaning public and private could be distinguished more efficiently. Another thing: when I mentioned "dynamic" lookups I was referring to something like this:

obj[myPropertyName]

If public and private shared the same namespace, then I don't see how such dynamic access could work in all cases without requiring extra checks for private fields at run-time.

Anyway, all of this is moot given that the ability to detect the presence of a private field from outside a class has been deemed unacceptable by the committee. Truly hard private fields are apparently one of the most frequently and emphatically requested features from library and framework authors (for reasons discussed in many previous threads, although I'm sure someone could summarize them if you can't find them easily).

mbrowne avatar Nov 24 '18 23:11 mbrowne

One more question I couldn't find an answer to: What about protected fields and methods?

Here's a link to a fairly recent issue specifically about protected: https://github.com/tc39/proposal-class-fields/issues/122

More recent discussion: https://github.com/rdking/proposal-class-members/issues/3#issuecomment-436042432 (note: external link) https://github.com/tc39/proposal-class-fields/issues/150#issuecomment-439663963

My opinion: First of all, I agree with a point that some committee members have made in the past: even if protected were viable for JS (which is questionable given the lack of strict typing), access modifiers should be based on real-world usage and feedback since JS is unlike other OOP languages in important ways. It would be really good to be able to work with private fields for a while to figure it out and ensure a good decision—including whether or not intermediate access modifiers like protected are actually needed in the first place. The use of decorators (currently in stage 2) in combination with private fields gives a lot of interesting possibilities for sharing and enabling reflection on private fields. Perhaps private fields + decorators will be sufficient, but if not they can still help provide very useful feedback that can inform future proposals. I also believe that some sort of module-scoped access level for fields (could be called internal) might be more generally useful and appropriate for JS than protected (friend could also be useful), but regardless, there are important use cases for multiple classes sharing private members internally.

But most of all we need to make sure we're not painting ourselves into a corner with this proposal: https://github.com/tc39/proposal-class-fields/issues/144#issuecomment-430585551

On the other hand, maybe we're not painting ourselves into a corner but just limiting ourselves a bit (but I'm not fully convinced): https://github.com/tc39/proposal-class-fields/issues/122#issuecomment-427630234

mbrowne avatar Nov 25 '18 00:11 mbrowne

For completeness I'm also including a link to a very relevant comment by @bakkot, although I remain skeptical that private fields + decorators will be a fully acceptable solution for intermediate access levels in the long term: https://github.com/tc39/proposal-class-fields/issues/144#issuecomment-430701900

mbrowne avatar Nov 25 '18 00:11 mbrowne

The only reason why hard private is desired over soft private I can find is this from FAQ:

Library authors have found that their users will start to depend on any exposed part of their interface, even undocumented parts. They do not generally consider themselves free to break their user's pages and applications just because those users were depending upon some part of the library's interface which the library author did not intend them to depend upon. As a consequence, they would like to have hard private state to be able to hide implementation details in a more complete way

Honestly, it only explains why encapsulation is required. I don't see why you need to hide all those private members. You can always find out what are those via reading the source code anyways, even if there is hard private. In the reality, you only want to prevent users from "depending upon" those undocumented APIs. To me, just disallowing developers from accessing them is more than enough. (I actually prefer TypeScript style compile-time encapsulation without runtime enforcement. Private should just be an advice instead of limitation. Personal opinion though.)

SCLeoX avatar Nov 25 '18 06:11 SCLeoX

@SCLeoX it's easy - and more common than you'd think - for users to write code that branches on the mere presence of public properties as a brittle means of feature/version detection. For those of us that want to truly avoid breaking changes in a non-major version, preventing access is not nearly enough.

ljharb avatar Nov 25 '18 06:11 ljharb

@ljharb I don't get it. ._. So what's the problem with feature/version detection... If there is a change, there is a way to detect it. Otherwise, it will be the exact same thing. ._.

Even if the "hidden change" requires a password to unlock, you can always .toString to get the source code of the function.

SCLeoX avatar Nov 25 '18 06:11 SCLeoX

@SCLeoX the point of private implementation details is that the maintainer is/should be free to change/refactor them in any way they like that doesn't change the observable behavior - the API.

.toString is a separate issue, that can be handled by using .bind or Proxy, but in practice, isn't really a concern.

ljharb avatar Nov 25 '18 06:11 ljharb

@ljharb If no observable behavior is changed, why there is a change in the first place... If there is a change in the code, something should change in its behavior - let it be performance improvement or bug fixes. If the behavior is exactly identical, for example, renaming a local variable, there should not be a new release of that library anyways.

SCLeoX avatar Nov 25 '18 06:11 SCLeoX

@Scleox What if the library author renamed or removed a private field in the process of reimplementing something to fix a bug, but otherwise the public API remained unchanged?

mbrowne avatar Nov 25 '18 12:11 mbrowne

A solution that could be technically viable but probably not practically viable would be to use private x for soft private (which would translate to symbols or something similar) and #x for hard private. The problem is that having two kinds of native syntax for private could be confusing, and not everyone agrees on which is the better initial choice when writing a class that will be consumed by others. With hard private fields, you'll be able to use a decorator to expose them, e.g.:

@reflect  // an example decorator that would make `#x` available to a reflection API
#x

I think that makes more sense semantically than the other way around (i.e. it makes more sense than a decorator that would somehow turn a symbol into a hard private field).

The unfortunate part is that class authors don't always anticipate the need for reflection. So even in cases where reflection would be very useful and done with the full expectation that the author could change the internal implementation at any moment, it simply wouldn't be an option unless the class author decorated those private fields. This is worsened by the fact that the decorators proposal was recently changed so that you can no longer access private fields via a decorator on the whole class (due to concerns about leakage of encapsulation), but would have to decorate each field even if your intent is just to make all fields available for reflection.

In some ways I think it would be better if soft private were the default, but OTOH, exposing internal implementation details that some developers might not even realize they're exposing is arguably worse, so hard private is a better default from that perspective. Decorating each field to enable reflection could be a bit tedious especially for classes within your own code base that will never be consumed by a 3rd party, but I haven't seen many use cases for reflection of private state within the same code base in the first place.

mbrowne avatar Nov 25 '18 13:11 mbrowne

but OTOH, exposing internal implementation details that some developers might not even realize they're exposing is arguably worse, so hard private is a better default from that perspective.

how many js-devs are naive enough to believe it's practical to audit webapps with hard-privates/encapsulation to guarantee against data-leakage when mixed with 3rd-party code? nobody. security-arguments for private-fields in javascript are laughably impractical.

kaizhu256 avatar Nov 25 '18 16:11 kaizhu256

Plenty are, and plenty do it, myself and my company included. I’m sorry your experience is with devs that don’t, but that doesn’t mean your experience is universal nor does it give you the right to be condescending.

ljharb avatar Nov 25 '18 16:11 ljharb

@ljharb nobody mixes crypto, credit-card, password, etc. with untrusted javascript code regardless if it's encapsulated or not. one does not see untrusted-code/ads running on merchant's payment webpage, and private-fields is not going to change that.

sensitive data has and will always be handled in javascript by running it in isolated webpage (client-side), or isolated at network-interface level (server-side). but never at code-level.

kaizhu256 avatar Nov 25 '18 17:11 kaizhu256

Yes, they do, and yes one does, and yes, it is. Again, your perspective may not include these things, but that’s a limit of your perspective, not of the language.

ljharb avatar Nov 25 '18 17:11 ljharb