-
Notifications
You must be signed in to change notification settings - Fork 19
Hard-private vs soft-private #33
Comments
Hard-private is fully polyfillable via Private state shouldn't be tested or accessible. If you need to test it, it shouldn't be private, and vice versa. The "new language construct" here is simply exposing a way for users to create internal slots - something that previously only the spec was empowered to do. imo this simplifies understanding and reduces complexity. |
I apologize if this is out of place, but would there be value in supporting both? Use hard-private (
Use soft-private (
Edit: It would also be compatible with TypeScript, if I'm not mistaken. |
That seems a bit confusing, and "soft private" doesn't require additional syntax - you'd just use module-level |
@littledan I think you covered most of my thoughts on this topic pretty well. A few additional thoughts: Fundamentally, "hard private" state means private state on an object that cannot be seen by any objects not expressly granted access to that state. This means that requests for that state cannot be intercepted by installing a proxy, nor by creating a subclass of the original class. In contrast, "soft private" state means private state on an object that, while difficult (or annoying) to access, can be accessed without an explicit grant. Any attempts to "secure" this kind of state can't possibly work (tautologically). Most of the proposals for private state, including the one in this repository, work by using lexical scope to grant access to the private names, an elegant approach that eliminates the possibility of snooping through proxies and provides a nice, static semantics that is useful for efficient implementations. A minor concern: from this perspective, any plausible semantics for protected state must be considered soft-private, since it is necessarily possible to capture a "soft-private" symbol by constructing a subclass: // this example uses theoretical syntax, but the basic point applies to protected syntax
// modelled in terms of the current hard-private state proposal.
class Parent {
protected #mine;
}
// in a module attempting to gain access to #mine
class Child extends Parent {
get mine() { return o => o.#mine; }
}
let mine = new Child().mine; // I now have a generic way to access Parent's #mine
mine(new Parent()); // using it This means that if we are ever to add support for protected state, we will end up with soft-private state regardless. Given that protected state is highly desired by some members of the committee, starting with hard-private may result in getting both anyway, and I would prefer not to do a piecemeal approach that could end up less coherent than we would like. (As an example, if we end up modelling protected state on hard-private state, protected state will end up with the same proxy blindness as hard-private state. But given that protected state has no special security reason for this limitation, this would be an unnecessary limitation that could feel incoherent.) @ljharb said:
@littledan said:
I find these arguments compelling. A possible approach that could allow us to make progress without making an immediate decision on scarce syntax might be to add support for private state via a meta-property. class Person {
private.first;
private.last;
fullName() {
return `${private.first.get(this)} ${private.last.get(this)}`
}
} As people have pointed out, this would give transpilers enough core semantics to transpile both hard-private and soft-private immediately, without needing to make an immediate call about which of the two styles we want to grant the most sugary syntax ( If transpilers can efficiently experiment with both styles, this will teach us whether one of the two styles is dominant and whether protected is highly desired (and what its semantics might be). We might also learn that hard-private and soft-private are both very popular, which might mean that we should try to find sugary syntaxes for each. The nice thing about a meta-property is that it gives us the new language feature that we need for efficient transpilation, putting it on equal footing with symbols both from @littledan's perspective (ability to experiment efficiently using transpilers) and @ljharb's (exposing a feature of spec objects to user-space), decoupling it from the most sugary syntax. |
To state my own position: I think we should stick with hard-private. I think it's really valuable to allow programmers to build solid abstractions, just like the platform. This gives library authors more ability to evolve over time, without users depending on implementation details. It's just good object oriented design to have a reliable separation between interface and implementation. As language designers, I think it's OK for us to take an opinionated stance in favor of this sort of good design. When library authors do want to expose things as inconvenient/soft-private/protected parts of the interface, there are currently other mechanisms to do so. Current mechanisms for soft private stateES currently has two mechanisms to provide soft-private state:
I'm not sure I'm convinced that these two existing mechanisms are insufficient as they currently stand. Any soft-private mechanism would, at its core, have to give you a nice way to access and distribute the symbols, but I have a hard time picturing anything nicer than our current support for symbols in lexically scope variables. Square brackets being ugly might be the main disadvantage, since you have to put them both before and after the symbol name, and because it looks like array/object-as-a-map reference, which might not meet some programmers' mental models. Maybe we could have some syntactic sugar just for this purpose ( Avoiding using a sigil like
|
That isn't the main issue. The main issue is that the underscore namespace just becomes another contended namespace. Using an underscore in a subclass means verifying that the superclass doesn't use the same name (or trying to use a name that is unlikely to collide). Adding an underscore property in a class that is meant to be subclassed is therefore a semver-breaking change. In practice, this is sufficiently problematic to cause serious problems in my experience.
We've been using this style quite a bit in Ember, and I've also been using the TypeScript support for private fields. As a point of information, the lack of good "friend" support in TypeScript causes me to lean on the "soft" nature of the TypeScript approach. I strongly prefer the TypeScript style to the symbol style, which requires a noisy outside-of-class declaration. It's possible to think of the outside-of-class declarations as "declarations", but it just feels like too much busy-work in practice. (I do it when I'm not in TypeScript because of the problems with underscores, but it's more annoying).
I can only speak for my own experience: the underscore mechanism is unusable in many cases, and the symbol mechanism exceeds my ceremony tolerance in many cases. What this means is that it's much easier to be sloppy (and use public fields) than use symbols. That said, I agree that many of these claims are hard to verify, and I also agree that it's possible to transpile various versions of the soft-private-using-symbols approach. I'd like to attempt that kind of experimentation before settling, as a language, on using Since the primary rationale for landing hard-private now (as opposed to experimentation via transpilation) is exposing a language feature that can be efficiently implemented, I argue that we should prefer a less-sugary version of the hard-private feature. That way, we can experiment with sugar-for-hard-private and sugar-for-soft-private via transpilation, and determine which of our various instincts reflects how people will use the feature. I also think, in general, it makes sense for us to be more cautious about rushing language features that can be transpiled, and instead encourage experimentation via transpilation (as you have astutely argued for re: decorators). |
I'm fine with that. You could easily construct the object yourself if you wanted to share it (and perhaps with decorators, you could make it easy to do so): class {
private.first;
get firstAccessor() {
return o => o.private.first;
}
} |
Which part of the symbol mechanism is excessive ceremony for you? Is it having to declare the variable, use square brackets, or both? Do you have another idea for how you'd like to share the symbol around? |
Why? They would each have a different name and purpose (private fields/data/whatever vs private properties).
So now we must define all of our property names outside of the class? That's crazy, and I think that is what @wycats was referring to as being "a noisy outside-of-class declaration" and exceeding his ceremony tolerance. I agree 100%. If symbols are to be used for soft-private state, they need some sugar, and the TypeScript-like syntax is already well-known and commonly used. Regarding hard-private, I think that the |
@littledan @tc39/delegates We had a very brief discussion (10 minutes) at the last meeting about private state, and at the time I wasn't comfortable advancing this feature without a longer discussion about soft vs. hard private. After the meeting, I had a conversation with Mark Miller and @allenwb about hard vs. soft private (especially as they relate to protected and friendly fields), and I think we came up with a path for all of these features that can be based on the private field approach in this spec. In short, the idea is that names could be "imported" into a class, either from a superclass or a friendly class. Here is some straw man syntax as an example: const Friends = Symbol();
class LifeForm {
protected #name;
#homePlanet;
export #homePlanet for Friends;
}
class Human extends LifeForm {
use protected #name;
}
class Spaceship {
use #homePlanet using Friends;
warp(lifeForm) {
setACourse(lifeForm.#homePlanet);
}
} This syntax and semantics is still awkward and would want quite a bit of refinement, but it shows that providing a mechanism for exposing (and using) a symbol from a superclass, as well as collaboration between friends, could be accomplished within the rubric of this proposal. I think that protected (and support for some kind of friend access without hacks) is important, and something that we should try to work out (or reject, perhaps) as we advance this proposal. In light of this discussion (which we couldn't have during the meeting itself because we were restricted to 10 minutes), I am comfortable advancing the feature to Stage 2. (If I recall correctly, there were several other committee members who also seemed uncomfortable advancing given the short timeframe for discussion, but we should be able to move this fast in September.) |
I'm all for figuring out how to make friends with better forms of access. I'll try to iterate on this. |
I'd like to make an argument for soft private semantics. In my experience I had to use private state several times to debug or patch critical issues. In the case of open source libraries, the user has the option of forking the library and removing the hard private declarations. It’s just a hassle. Hard private makes it difficult to patch bugs for a specific environment that the library maintainer didn’t quite envision. Those issues could be hard to address properly and might require major refactoring that the end user isn't capable of doing. Reaching out into private methods should definitely be discouraged. Library maintainers are NOT expected to keep private variables the same over patch releases or even within a patch release. The user should be given static analysis tools bring the code into compliance as much as possible and in rare cases opt out where strictly necessary. I think it would be fine if accessing soft private properties required some ugly syntax like |
I hate these magical properties ... couldn't there just be some from of reflection function/class? |
@syrnick Given that a library author can already do |
I absolutely prefer private.field or this.private.field to #field or this#field. Is the sigi-less option suggested by @wycats still an option? |
@dalexander01 Which sigil-less option--using |
@littledan yep, private.field is what I had in mind. |
Oops, @bakkot pointed out to me that this idea makes no sense actually-- |
@littledan the owner of the private properties would access them normally (e.g. via @glen-84 that's a perfect reaction. Any "trespassing" should raise an alarm to the reader. I think it's ok to reserve "private" in classes that want to use private properties. Only if the class (or a base class) declares a private property, it would be able to access private properties. E.g.
|
@syrnick would |
@syrnick Consider also the interaction with public class properties. With your proposal: class C {
private foo = 1;
bar = 2;
method() {
this.bar; // 2
this.foo; // undefined - wth?
}
} The sigil-based proposal works elegantly with all of these constraints: class C {
@foo = 1;
bar = 2;
method() {
this.bar; // 2
this.@foo; // 1 - as expected
}
} |
@littledan, wouldn't the Consider: var private = "SECRET STRING – DON'T TELL!";
class Foo {
private secret;
method(){
// What is `private` here?!
}
} Either a) In the other issue thread there seemed to be excitement about the |
AFAICT, this is not an issue since
|
Well would you look at that, you're right. Ignore that I said anything then! |
That is, at most, a one off mistake. |
WRT testing: there is an advantage to allowing access to private fields. Yes, you can theoretically test every code path but it's a lot of extra work. I'm pretty ignorant to how these features are actually implemented, but would it be possible to allow access based on a runtime switch or a macro so we could enable access during testing? |
If you need to test it, it shouldn't be private. |
I'm not sure that's universally agreed upon. For example it's very common
in C++ to friend the Test class so it can access methods and state you
would otherwise not want used. Having a hard private without an escape
hatch like "friend" or a reflection API is unusual across most languages.
I'd certainly prefer we have some escape hatch, even if it's ugly, since
there's many existence proofs of various use cases which need it across
many languages and systems.
|
@bakkot if someone is using Reflection to get to a soft private field then they can suffer the deprecation and change their code, there has to be a limit to the hand holding we do with bad practices. |
A lot of people would say exactly what you're saying about people who are willing to violate the convention that Anyway, per FAQ:
|
If it's accessible, it's public. "soft-private" is no different than underscores, because both are a convention (albeit, the former would be a convention baked into the language). If a user can do it, they will, and if you break them, you broke them, even if they did something unwise. |
Why do you guys keep saying this? Let's compare: Underscore: I can access the property with no errors or warnings, I might not even know about this convention, there will seldom be any documentation about the convention (within the context of a specific project), and it "just works". Soft-private: I cannot access the property, I might see errors or warnings (or just nothing), I can easily find out that a hash prefix (puke) means that it's private and should not be accessed in general code. If I learn how to access it via some obscure reflection mechanism, then I've taken extra steps to do so, written additional lines of code to access the value, and have probably seen warnings about appropriate use cases. Please stop suggesting that they are the same. They're not.
If I was a library author, I wouldn't give a flying f*ck if someone accessed the private state of my code using reflection, and I then decided to refactor that code, thus "breaking" theirs. How often do you see similar complaints from other languages like C#, PHP, Java, etc.? Regardless, it's almost pointless discussing this, because the decisions have already been made. The majority of developers think that the sigil looks terrible, but yet you're still moving forward with it under the same classification. |
I disagree with @ljharb -- it is qualitatively different, in that you're much less likely to get into the situation by accident. You have to go and do the less ergonomic thing to get there. This makes soft private somewhat useful--in practice, people would be copying a recipe, or knowingly going around to get there, rather than just using some kind of tab completion. The real downside is that it's missing another ingredient that would be useful for many library authors.
I'm not sure all library authors have that luxury. Node has had to revert changes which were about things that definitely look like unstable implementation details, and documented as such, when popular-enough libraries end up depending on them. For that reason, @bmeck expressed a preference for "hard private". |
I will state that they were doing very strange things at times to get to internals. I do not thing "less ergonomic" would be a mitigating factor to any real degree. |
You see similar complaints from other languages all the time. In fact one of the major new features in Java 9, modules, has as one of its main goals strong encapsulation in (as I understand it) exactly the sense discussed here: allowing libraries to hide internals even from reflection. They went through much the same debate we've been having here (and have been having since at least 2010). This was requested by library authors for exactly the same reasons library authors requested strong encapsulation in JavaScript: for example, JUnit 5 came about in large part because
Likewise, maintenance of the Java platform itself had become difficult because of people depending on internals through reflection. |
If other languages are having problems with soft-private state and JS already has soft-private via Symbols; are there reasons to not want hard-private? Some statements above seem to be afraid of accidental leakage, but that is something that the programmer is leaking by writing a leak. To my knowledge, no attempt at inheriting the private field namespace for an instance is in the works so things like |
@bakkot I'd like to add the beginning of that quote about JUnit5:
That is an essence of open software - assuming that users will have imagination and come up with use cases that authors have not imagined and the interfaces exposed might be insufficient. That's why closed source software isn't fun even if you have an API to it. It's fully-encapsulated. |
Well, given what was described above, maybe it would've been better if JUnit thought things through and wrote an explicit interface for tool authors. This would have the best of both worlds--tooling and maintainability. And with open source, if the previous library maintainers don't want to add this, someone can fork it and add such an interface. I'll mark this bug as closed. Unfortunately, we haven't come to complete agreement here, but there are some really compelling arguments in favor of maintaining a strong privacy boundary. |
Perhaps if we could have dynamic access like I suggest in #104, it would provide a happy medium for those wishing to see soft private. |
It seems to me that in many posts here, "soft-private" through Symbols and TypeScript-private are used as synonyms. I'd like to emphasize that they do not have the same semantics!
In comparison, "soft-private" is just missing hard encapsulation, meaning you can still access soft-private members through reflection at run-time. So we actually have three levels of class member privacy:
For my use case, I used "soft-privates", defining What I, as a library / framework author, need is exactly syntactic sugar for "soft-private" members, instance as well as static, fields as well as methods. This seconds some of the opinions uttered in proposal-class-fields issue 189. |
No description provided. |
Should private state have an "escape hatch" like TypeScript private, or be strongly private the way that closed-over values are? Soft-private isn't fully written out the way that @zenparsing has done an excellent job on the current WeakMap-based proposal, but the core would be that "private state" is simply (public) symbol-named properties, with syntactic sugar for those symbols, and possibly some kind of introspection over them. It might be bad for overall language complexity/intelligibility to have both kinds of private state in the language and better to decide up-front what we want. Below, I @-mention people who either made this argument or who I think might be especially interested in the argument.
Soft-private
Soft-private would be syntactic sugar for symbols. In ES6, you can currently write code like this:
With a soft-private language feature, there would be syntactic sugar that looks like this:
Advantages
Disadvantages
Hard-private
Hard-private (the proposal described in this repository currently) is based on state that can really only be accessed by code within the class body. This has been constant across all of the different proposals that have been advanced to TC39, though not what TypeScript provides.
Advantages
static
blocks that run as part of class definition (currently not concretely proposed or championed), it would actually be possible to build getter/setter functions to share with friends (though this code may be slower, especially as the page is starting up), e.g.,Even without static blocks, you could create a static method which does the exporting, though this is ugly.
Disadvantages
Thoughts?
The text was updated successfully, but these errors were encountered: