Open futpib opened 7 years ago
This proposal deliberately avoids making private fields a kind of property. That approach was attempted at TC39 in the past, but it leads to the question: If private fields are object properties, then how do Proxies view them? It didn't seem like there was any good answer to the question that preserved the properties that Proxies were intended to have. cc @erights
@littledan Thanks, I get that. I guess what I was really trying to say was that making PrivateFieldEnvironment, PrivateFieldIdentifers and [[PrivateFieldValues]] usable by means other than in class declaration would make private fields more useful.
This does not necessarily mean that existing reflection methods like Object.getOwnPropertyNames
or Object.getOwnPropertySymbols
should expose private properties. Nor even that there should be special methods like Object.getOwnPrivatePropertyNames
specifically for [[PrivateFieldValues]].
But I think making a new universal object slot [[PrivateFieldValues]] exclusively for class declaration is very limiting.
This does not seem to contradict the arguments made in this FAQ entry.
Sorry for misunderstanding your message. I've been thinking about what sort of reflection mechanism we should have for private fields; one draft is in this file. However, I've been thinking about this mostly as it relates to decorators--not use outside of a class.
I don't think it'd be great if people had to go around using imperative, semi-reflective machinery to use private fields outside of classes. It would be nicer if some sort of syntax just works.
For example, we could make the #names
be available to everything in the script or module, but not outside of it; then we could allow object literals to have private fields, and reference them from outside of that literal. The downside of this particular idea is that people often concatenate scripts when shipping them, and this concatenation would break the encapsulation; generally, it's nice that JavaScript has this property that you can wrap things in separate IIFEs and they'll stay basically separate, and it would be unfortunate to lose this property.
Do you have another particular idea for how we should enable this feature?
Sorry for being unclear, I see how my objections are vague. My prime motivation for creating this issue after reading the proposal was that adding new slots to objects and especially the execution context to be used only in classes (which are a derived concept themselves) left me uncomfortable since desire for hard encapsulation is not necessarily tied exclusively to classes or OOP. Another thing is the inability to define private properties dynamically/reflectively.
PrivateFieldEnvironment looks like just like the usual lexical environment, except it is used for private identifiers. Very similarly to how lexical environment is used for common identifiers. Unfortunately I can't imagine a really-really nice way to extend this proposal in it's current form to allow general use of PrivateFieldEnvironment, but let me propose the best I got:
function Point () {
private #x;
private #y;
const p = {
#x: 0,
#y: 0
};
p.#x; // 0
p.#y; // 0
return p;
}
I think this is pretty self-explanatory (function declarations get a PrivateFieldEnvironment just like class declarations), but If there is any interest in this, I can try to formalize this.
EDIT: The lack of reflection still bugs me, even though if you view private identifiers like another kind of lexical identifiers, the lack of reflection kind of makes sense.
Interesting idea! Would the following be the desugaring into WeakMaps?
function Point() {
const x = new WeakMap();
const y = new WeakMap();
const p = {};
x.set(p, 0);
y.set(p, 0);
x.get(p); // 0
y.get(p); // 0
return p;
}
Yeap, that's right.
Oops, no, actually, that's not quite right.
The trick is that PrivateFieldEnvironment for classes is created when class definition is evaluated. It's better be the same for functions.
The following desugaring gets this right.
const Point = (function () {
const x = new WeakMap();
const y = new WeakMap();
return function Point() {
const p = {};
x.set(p, 0);
y.set(p, 0);
x.get(p); // 0
y.get(p); // 0
return p;
};
})();
But if we want to be really thorough we would replace WeakMap
with something like PrivateFieldIdentifier.
@futpib Interesting idea. Do you think this proposal should have any changes to "future proof" for that as a follow-on?
@littledan Is this too major of a change for the proposal at it's current stage? I think something like this should be included with the first landing of private fields (and should have been included from the get-go). If that's not an option, I guess it's future-proof enough for this to be included later.
This fits the JavaScript's class-function duality so nicely >_<
@futpib
Is this too major of a change for the proposal at it's current stage?
Almost certainly. This would be a huge addition, and we've been trying to get class features in for a long time. I would strongly prefer not to add more new things to the proposal at this point.
I want to point out a potential conflict with the current proposal:
function Point () {
private #x;
const p = {
#x: 0
};
class Other {
#x = 1; // Is this the same '#x' as above?
}
}
We previously considered (and rejected) declaring private fields in classes as private #x
, which would avoid the issue nicely. On the other hand, it would be kind of surprising if private #x
were the only declaration legal in both function bodies and class bodies.
@bakkot I thinks the "conflict" you pointed out is not an issue when private identifiers are lexically scoped.
class A {
#x = 0;
constructor() {
class B {
#x = 0;
constructor() {
#x = 1; // lexically closest #x
}
}
}
}
@futpib It's a conflict in the sense that it's not obvious that {#x:0}
references a previously declared name whereas class {#x=0}
declares a new one.
Almost certainly.
Hang on, @bakkot; it's not at Stage 3 yet. TC39 has been working on this problem for several years; I really want to see it shipped now too, but let's make sure to keep taking any useful feedback we get.
Anyway, @futpib:
I think the way you're resolving the conflict there makes sense--classes introduce a new scope for these identifiers, as does any block that contains "private #foo", and shadowing works lexically. It all makes sense to me, and seems like a consistent extension of the current proposal. This is actually pretty similar to some ideas that I remember seeing on the ECMAScript wiki, but I don't know where to find that; @allenwb do you remember this? It's really nice to have a story for object literals here, and it would actually be consistent with the destructuring proposed in a different issue. The only concerns I have would be:
private #x
to just declare a name, outside of a class, with #x
declaring a field inside of a class?private #x
is only used for object fields, unlike the earlier syntax where it was what was used to introduce all private identifiers. Do you think there's a more evocative name possible here?A good repository to follow up on this further would be the unified class features proposal, which currently assumes that objects' curly brackets will be what sets off the new private name scope.
Doesn't future proofing for this extension require using private
for class declarations now? To illustrate further the hazard @bakkot raises:
private #id;
let instances = 0;
export class A {
// declare a new private name with same name
#id = instances++;
}
export function getId(a) {
// refers to a different name than A's, so will never return an id.
return a.#id;
}
I'd expect the above to work, though you might expect it to work even with a private
keyword before the class declaration. Errors on private name shadowing could mitigate this somewhat.
@bterlson, Would you expect the following example to work (apologies for the horrible names)? Maybe if you come with a Java-like intuition, and you expect such a reference to be disambiguated, you would. The current proposal, though, expects that users can understand what it means for a private name to be shadowed.
Do you think if we introduced the keyword private
, it would create the false intuition that that's the only place where a new name binding is created, but without ever creating that syntax, it's more clear that shadowing works the way it's currently proposed?
class Button {
#id = MakeGUID();
labelFactory() {
let button = this;
return class Label {
#id = MakeGUID();
getButtonID() {
return button.#id;
}
}
}
}
@littledan I know this has been discussed already, but nevertheless, if private
keyword was the only thing that can introduce new private identifiers to the scope, there will be much less confusion. I think, if this addition is to be accepted, then both in class and function declarations (or maybe in any block) the only valid form of private identifier declaration should be private #x
.
@futpib We already have some other differences with classes compared to outside of classes. Outside of a class, you define a function by function f() {}
, but inside of a class, you define a method by f() {}
. Doesn't leaving out private
inside the class seem analogous?
This is similar in some ways to an old proposal by @allenwb for name declaration (back from the private name days), with the difference that the names themselves are not reified. This deserves some thought.
A precursor proposal to the current private fields proposal used the private #foo;
syntax and contemplated supporting private fields in object literals.
The "private name" proposal that @zenparsing mentioned is Syntactic Support for Private Names from 2012. Note that in this context, a Name is more or less what was eventually called a Symbol in ES 2015. It built upon an earlier Instance Variable proposal that dates to early 2011. So, none of these ideas are particularly new, the challenge all along has been building sufficient consensus around any particular private state scheme.
Perhaps not surprisingly, I like some of the ideas in this thread as they address some defficienceis with both the current private fields proposal and plausible extensions to it.
One issue is that strictly linking the lexical declaration of private field identifiers with the definition of private object field limits the utility of inner class definitions. For example while this is legal:
class Outer {
#foo=42;
getFooAccessWrapper() {
const anOuter = this;
return new class {
get value() {return anOuter.#foo} //inner class can access outer class private
}
}
console.log(new Outer().getFooAccessWrapper().value); // 42
There is currently no way to allow an outer class to access an private field of an inner class:
class Outer {
#helper = new class Helper {#foo};
exec(service) {
service(this.#helper); //services needs Helper instances
}
set foo(v) {this.#helper.#foo = v} //Syntax Error: #foo not visibly declared
}
and trying to declare #foo in the outer class doesn't help:
class Outer {
#foo; //create a #foo field to eliminate syntax error
#helper = new class Helper {#foo}; //note introduces a inner #foo
exec(service) {
service(this.#helper); //services needs Helper instances
}
set foo(v) {this.#helper.#foo = v} //Reference Error: helper instances don't have this #foo
}
Adding private fields to object literals would have similar issues because, applying the principles of the current proposal, such private fields would be instance private:
function fooFactory(value) {
return {
#value: value,
sameAs(aFoo) {return this.#value === aFoo.#value}
}
}
console.log(fooFactory(42).sameAs(fooFactory(42)));
//Reference Error: Each evaluation of obj lit creates a new distinct field idetity
Adding a private #foo;
declaration that introduces a new lexically visible private field identifier without actually defining a private field would provide a solution to both of these issues:
class Outer {
private #foo, #helper; //create new private field ids #foo and helper
#helper = new class Helper {#foo};
//Outer instances have a #helper field, Helper instances have a #foo field
exec(service) {
service(this.#helper); //services needs Helper instances
}
set foo(v) {this.#helper.#foo = v} //works! #outer can reference Helper's #foo
}
private #value;
function fooFactory(value) {
return {
#value: value, //defines a private field using the lexically visible #value binding
sameAs(aFoo) {return this.#value === aFoo.#value}
}
}
console.log(fooFactory(42).sameAs(fooFactory(42))); // true!
Note that in the Outer class I used a private
declaration for #help even though it probably isn't really necessary. I did this to emphasize the distinction between private
declarations that introduce a lexically scoped private field identifier and class instance/object literal field definitions that actually create an object level data slot that is keyed with the currently scoped private field identifier.
In practice, I'm sure that for ergonomic reasons, for the most common cases people would still want to be able to make declarations like:
class P {
//don't need to also say: private #x,#y;
#x;
#y;
constructor(x,y)
this.#x=x;
this.#y=y;
}
}
So, I would define the semantics of class private field definition for #foo as using the private #foo
declaration that is currently in scope and if there is no visible declaration of private #foo
one is automatically introduced with class scope. The same could be done for object literal fields but it feels like a foot-gun to me as I don't think instance private is necessarily what will be wanted for most such obj lit use cases.
A few final thoughts
I don't see any reason to restrict private
declarations to class and function scopes. It should be usable in top level and block scopes (and do-blocks if they ever hatch).
export private #foo;
private #foo;
declarations seems like they are almost a non-breaking extension to the current private fields proposal. However, the addition of a outer scope private
to pre-existing code based on the current proposal might break inner classes that currently redefine outer scope private field definitions. I not sure whether this is a big enough of a concern to say that it is a "now or never" for introducing a lexical private
declaration form. private #x,#y;
//define functions that reference #x or #y and put them in a propertiesDescriptor object
let obj = Object.create({__proto__: desiredProto, #x, #y}, propertiesDescriptor);
@allenwb, @littledan I'd still argue that we are better off with a mandatory private
keyword for private identifier declarations. This is not similar to omitting function
when defining methods in classes because method declaration do not introduce anything to the lexical(-ish) scope.
The confusion outlined in this comment is a valid concern. Without mandatory private
, when reading a class declaration, in order to understand weather a #x = 1
line is a declaration (of a new private identifier) or a reference (to an already declared private identifier) one has to skim through all the parent scopes looking for #x
. This is trivial in a small example, but in a large file this would be a bummer:
class Outer {
#x = 0;
// imagine a lot of code here
labelFactory() {
return class Inner {
#x = 1; // is this a reference or a declaration?
}
}
}
Similarly, without mandatory private
, it is easy to accidentally break code in inner scope by introducing a new identifier to the outer scope (turning an intended declaration to a reference):
class Outer {
#x = 0; // imagine this line wasn't here before
// imagine a lot of code here
labelFactory() {
return class Inner {
// this was a declaration, but it became a reference
// when one introduced #x to the outer scope
#x = 1;
}
}
}
You can see that with mandatory private
this is not an issue, private #x = 1
is always a declaration, #x = 1
is always a reference.
Another point is keeping syntax consistent. Currently one can introduce lexical identifiers only with keywords (const
, let
, function
, class
, import
...). (Except in non-strict mode you can do x = 1
, but it's considered an anti-pattern universally)
It seems that these arguments are valid even against proposal in it's current form (with private identifiers being exclusive to classes).
@allenwb Thanks a lot for the historical links, Allen. It seems like we may be on a good path, if we're thinking along the lines of those proposals, but then avoiding the Proxy issue by not making private fields be properties. Your analysis seems to make sense to me; all together, sounds like you're suggesting this proposal first, and explicit private lexical declarations could fit in well as a follow-on proposal, and that it's reasonable to use different syntax for these (since they are doing very different things); am I understanding correctly?
One small thing about private
in top-level scopes: It seems like it should be restricted to the script/module, rather than joining part of the global lexical contour, right?
@futpib What you're saying sounds like a downside of not having a token before the declaration in the class (or possibly of having the private access shorthand), rather than something that's specific to private fields in particular. For understanding whether a declaration is a declaration or assignment, the issue appears for public fields as well, such as class Outer { x = 0; }
. And, in the constructor, for understanding whether a line is an assignment or a declaration, the analogous problem also occurs, where class Outer { constructor() { x = 0; } }
will be an assignment to whatever the outer scoped x
is. However, after years of debate, TC39 settled on the token-less form for such field declarations at the May 2017 TC39 meeting.
@littledan It seems to me that if you wanted to expand the syntax with private name declarations in the future, then the private
keyword for fields needs to be mandatory for now.
Presumably, one of the primary use cases for this feature would be "friend" classes.
private #x;
class A {
#x; // A has an "#x" field
}
class B {
readX(a) {
console.log(a.#x); // B has access to "#x";
}
}
Note that in A
a new lexical name is not introduced. The field uses the name defined in the outer scope.
The semantics required for such a pattern would be:
private
keyword, then both a lexical name and a field definition are introduced.private
keyword, then a field definition is introduced which uses the private name defined in the outer scope.private
declaration would only introduce a lexical name.Crazy idea (feel free to trounce it):
private
keyword is required for private field definitions.So this:
class A {
private _x;
constructor() {
this._x = 1;
}
}
is rewritten at compile-time to have the following semantics:
class A {
private #x;
constructor() {
this.#x = 1;
}
}
The idea is that with private declarations, the user opts-in to private lookup semantics for that identifier name only.
Advantages:
#
for later usage.Disadvantages:
a._x
syntax using the same name as a private name in scope.This has the (IMO terrible) consequence that if this same code operates on an external object that just happens to use the same name as a public property name, then the obvious obj.name
syntax for operating on that external object stops working.
When people express the preference you're reacting to, their preference does not take into account this cost.
@erights Yes, that problem was what eventually cause all previous non-sigil based approached to be abandoned.
Statically typed languages use type declarations to distinguish the external-public/internal-private cases with out the need of a sigil. But a dynamically typed language doesn't have enough static information to differentiate those cases.
@zenparsing that one is in the FAQ, even!
@bakkot It might be worth pulling in Allen's comment about how statically typed languages disambiguate here; I think this is pretty critical to the point.
BTW this comment makes me especially convinced about the hazard. Not sure what exactly we should do right now.
@littledan Do you think changing the private field declaration in classes between #x
and private #x
could reasonably be done at stage 3? If so, I think we should go ahead with the class fields proposal as it stands, and maybe bring this up as an extension, possibly as its own proposal.
I think this idea actually works well with the current proposal except for the above-noted conflict, and that conflict (it seems to me) is resolved very nicely by requiring private #x
as the declaration in classes. The committee has rejected the private
requirement there previously, but that was when there was no language-level need for it. I think it would reconsider if there were one.
@erights
I added a refinement my (half-serious) proposal above, requiring that names start with an underscore. This reduces the hazard you point out (though it certainly doesn't eliminate it).
@zenparsing which raises the question: why is (an unreliable) "_" better than (a reliable) "#" ?
@erights Consider it a Plan B for the possibility that there are no available characters to choose that are both reliable and palatable.
"#" is only significantly less palatable than "_" until one gets used to it. (Though I admit I'll probably never like it.)
Unreliable is hugely less palatable than reliable.
The generalized private discussed in this thread sure seems to harken back to the old "relationship" ideas and your ( @zenparsing 's) old generalized access notation. Rather than introduce a new kind of lexical namespace, we could instead introduce a the following new bits of syntax:
base::name
expands to name.geti(base)
base::name = expr
expands to name.seti(base, expr)
base::name(...args)
expands to name.geti(base).call(base, ...args)
For suitable choice of keyword that means hoist out one level:
class Foo ... {
keyword decl2 name = initExpr;
...
}
expands to
const Foo = (function(){"use strict";
decl2 name = initExpr;
return class Foo ... {
...
};
})();
or, more generally
decl1 Foo ... {
keyword decl2 name = initExpr;
...
}
expands to
const Foo = (function(){"use strict";
decl2 name = initExpr;
return decl1 Foo ... {
...
};
})();
where decl1 may be at least "class" or "function" and decl2 may be at least "let", "const", (or with obvious local changes) "function", or "class".
I am not saying that we should necessarily go there. But if we are considering the generalizations in this thread, then why not go all the way back to this?
@bakkot We need to work the #x
vs private #x
issue before Stage 3. I'd hope that Stage 3 would mean the language design is basically done, and that implementers, tool authors and users can proceed with the confidence that changes from there would be minor and only based on unforseen things that came up in implementation, etc.
I'm not convinced that #
is a character that's significantly uglier than _
or @
. For new programmers who have not used another language, or for people who have spent more than 5 minutes looking at JS code using the feature, what's the difference? I could buy that sigils are bad, but it doesn't seem like a particular sigil choice will be fatal. It's hard for me to see why we'd arrive at the need for this Plan B aside from the general sigils-are-bad argument.
If we're talking about reliability: You could see #
as still unreliable, e.g., if you have nested classes and want to refer to the private field of the outer one. However, it seems less likely to come up in practice; I think some of these design issues are more on a gradient than absolute.
@erights One step further and we arrive at "operator overloading with magic methods" 😄.
This proposal is still about private fields, I think that's the reason we don't go that general.
This issue is about making private fields usable outside of classes. That's it, and it's not even that huge of a generalization.
Already in this proposal:
This issue proposes:
The mandatory private #x
point stands against this proposal in it's current form, but accepting this issue also makes this problem easier to encounter. (also related: #53)
@allenwb I'm mixed on your point about not allowing export private #x
, while it's not great in that it would be trivial to get access to the internals by importing such a file, it might also be useful to share it amongst things that need it e.g. someone might implement something in one file but have operators in many separate files (like Observable) in which case modules may need to share that private field.
Now this would be doable as is without exporting them as you could always have:
private #state;
export default class SomeClass {
#state = {};
}
export function _readState(someClass) {
return someClass.#state
}
But this is just as bad as allowing export private #x
so why not just allow it?
Ultimately it's always going to be possible to break private state if people want to spread things over multiple files. e.g. I can always do this:
class SomeClass {
private #state
__breakState() {
return this.#state
}
}
What developers should really do instead is make sure that the exposed entry point to the application doesn't expose those private states e.g.
// privateImplementation.js
private #initializer
class Task {
constructor(initializer) {
this.#initializer = initializer
}
...
}
export default Task
export #initializer
// someOperator.js
import Task, { #initializer } from "./privateImplementation.js"
function delay(task, time) {
return new Task((resolve, reject) =>
setTimeout(_ => task.#initializer(resolve, reject), time)
)
}
// publicTask.js
export { default } from ".../privateImplementation.js"
// someConsumer.js
import Task from ".../publicTask.js"
import delay from ".../operators/delay.js"
@erights I still like that old proposal! The only issue is that we needed to distinguish between "creating" the field and assigning to the field, and the ::
proposal didn't provide that distinction. Any ideas?
I still like that old proposal!
Thanks!
The only issue is that we needed to distinguish between "creating" the field and assigning to the field, and the :: proposal didn't provide that distinction. Any ideas?
No positive ideas yet, but a possible direction. Since this proposal would separate the (hoisted) declaration of the WeakMap-ish from the initialization of per-instance state, we could again consider moving the per-instance-state initialization back inside the constructor, where it can see the constructor arguments.
One issue with this is that it conflicts with some requirements @wycats has been advocating for about the syntax of decorated private fields. The hope is that you can use something like this for a field which has a private field and a generated reader:
class C {
@reader #x;
}
Here, the claim is that it would be awkward to insert a keyword such as own
(or private
) in between @reader
and #x
because conceptually, for the user, this is actually a publicly readable field, which you address as instance.x
. We were previously thinking, if own
is the keyword, then it can be omitted when decorators are used, as long as the decorator would supply the placement (in effect, that you could implement an @own
decorator, and combine that with your own decorator logic).
However, such logic would not explain why private
is required--for @zenparsing 's "friend class" use case above, you'd expect that omitting private
would use the #x
from the outer lexical scope, not declare a new one just because it was preceded by a decorator.
Decorators are the new Proxy: "But have you thought about how this interacts with decorators?" 😄
I don't think adding private
between @reader #x
is that confusing:
class C {
// A reader for the private field x
@reader private #x
}
The fact that it generates a public field is just a detail of @reader
adding an additional field, the same argument would apply to the #x
syntax by itself.
I would personally expect this to work though if the private
syntax was added:
class C {
// Declare the field
private #x
// use it in a decorator
@reader #x
}
OK, it'd be good to get more context from @wycats here on why exactly @reader private #x
is a problem. Either way, personally, I hope we can stick with the syntax without explicitly saying private
.
Since class is syntactic sugar for function and "special"
"prototype"
property (at least class constructors are defined in terms of function and "prototype"), I feel like it's better to tie private fields to those constructs. In other words, one should be able to achieve exactly same results with function and prototype as with a class.Currently this proposal adds PrivateFieldEnvironment to Execution Contexts and the only defined way to create new PrivateFieldEnvironment is ClassDefinitionEvaluation (that is evaluation of a
class ...
expression).This will result in that whenever user wants to create on object with truly private fields, they will have to use a dummy
class
declaration with all it's limitation (like inability to declare arbitrary class members in runtime in reflective manner (withouteval
-like features)).I propose that at least
Object.create
EDIT: The
Object.create
part has been revised as unnecessary later in this thread.