-
Notifications
You must be signed in to change notification settings - Fork 113
[Private] yet another alternative to #
-based syntax
#149
Comments
Yes please. And we can finally start to use symbols for what everyone I know wanted to use them for in the first place. P.S. I prefer the first syntax as it's such a minor change to existing syntax. |
Syntax 2... You use Copy from zenparsing/js-classes-1.1#45 (comment) class Counter extends HTMLElement {
const setX = (value) => { x = value; window.requestAnimationFrame(render); };
const render = () => this.textContent = x.toString();
const clicked = () => setX(x++);
let x = 0;
constructor() {
super();
this.onclick = clicked;
}
connectedCallback() {
render();
}
} |
@hax I thought it could be a confusion between this proposal and
|
@Igmat Just how useful is the distinction? Case 1: (Class scoped private symbol used to add instance scoped private property)
Case 2: (private data contained completely within a closure)
The kick in the shins here is that for case 1, if by some accident the private symbol is leaked, there's nothing to stop the leak of the private data. Whereas with case 2, if the developer wishes to share some of the private data, the developer must explicitly provide for that. Which is better? |
@rdking in both cases to share private data developer has to explicitly provide access, so probability of unintended leaking is equal for both solutions. class A {
symbol somePrivateName;
leak() {
return somePrivateName;
}
} and this: class A {
#somePrivate;
leak() {
return this.#somePrivate;
}
} in terms of protecting private data. And since first allows additional usage scenarios built on that developer is able to share not only private data itself, but also private name, I think that first case is better. |
@Igmat it is different because you have leaked the key to the data. Not just the data as in the second case. But I personally think this ok as it's known to be a symbol and key. People shouldn't be returning the key directly and are unlikely to do it by accident. Returning |
@shannon I didn't mean that those samples do same stuff, I meant that in terms of privacy it doesn't matter what had leaked data or key to data - in both cases encapsulation is broken. BTW, I realized that leaking of a key seems to be even more unlikely happened than leaking of data. |
@Igmat There is a small but important difference. Leaking the data only puts a copy of or reference to the data in public hands. Leaking the key, puts the private property itself in public hands. In the former case, at most, the fields in an object can be changed, but the object itself will remain in the private property. In the latter case, an object in the private property can be completely replaced. If a library keyed something using that object, being able to replace it can break code. On this point, I think my usual opponents would agree with me, being able to leak the actual name of the private field is a worse problem than just leaking the data. |
@rdking yes yes, but footguns and flexibility and what not. This to me is a feature and not likely to be done by accident. Developers already understand that symbols are keys and this is a way to explicitly and simply provide access to those private properties. |
@rdking IMO it's rather feature than foot-gun, because there is mostly no chance to unintentionally reveal private symbol. symbol somePrivate; this is definitely declaration, more of that - it's self-explaining declaration, and the most important it doesn't interfere with any existing syntax, so it can't be misunderstood like Because this class scoped variable has special type - class A {
symbol somePrivate;
leak() {
return somePrivate;
}
notALeak() {
return this[somePrivate];
}
} There is no chance that somebody will implement something like class A {
symbol somePrivate;
leak() {
return { somePrivate };
}
notALeak() {
return { somePrivate: this[somePrivate] };
}
} This won't be confused for the same reason as previous sample. class A {
symbol somePrivate;
leak(cb) {
cb(somePrivate);
}
notALeak() {
cb(this[somePrivate]);
}
} This won't be confused also because class A {
symbol somePrivate;
leak() {
return { [somePrivate]: `anything could be here even private symbol itself` };
}
} It's even not a leak, because to access private symbol or its value outer code must have it already. class A {
symbol somePrivate;
leak(obj) {
return obj[somePrivate]= `anything could be here even private symbol itself`;
}
} It's not a leak, for same reason as previous sample. I can't imagine how private symbol could leak unintentionally, so it's definitely feature, and not a bug, unless you can provide some example when it happens by an accident. P.S.
class A {
#somePrivate = { a: 'some private string' };
leak() {
return this.#somePrivate;
}
} After calling |
Funny. Notice I never claimed that being able to do so was a bad idea. Just capable of a potentially worse problem. In the end, what you're offering is a throwback to proposal-private-fields and part of the foundation of the current proposal. It eventually became the "PrivateName" concept. So while you may have some arguing against you, know it's a viable concept because it's mostly already implemented, but with uglier notation. Btw, |
@rdking, on one hand it makes sense
on the other hand TS and flow popularized another type anotation: let a: number; So I don't think that
If you have any ideas for better keyword, I would love to take a look on them :) |
It might not even be necessary for the symbol to be declared in the class itself. As long as Symbol.private is implemented you can just pair it with the existing public properties proposal for it to be pretty useful. Set/Define debate aside. const someName = Symbol.private();
class A {
[someName] = 'private';
...
} It's easier to use than a weakmap and not incredibly verbose. This is really how I wanted symbols to work in the first place. When I learned that you could just get references to them via getOwnPropertySymbols I was very disappointed. The symbol/whatever keyword could be added in a follow on as simple sugar. Which would make this just: drop private properties as a class construct and implement private symbols and call it a day. |
However, I know people are going to say it needs to be syntax to avoid monkey patching Symbol. So there is that to consider. |
@shannon totally agree with that |
The big issue with using private symbols in this sort of way is that it's unclear how to square their semantics with some integrity goals that @erights has been championing. Separately, I think that it'll be lighter-weight for people to adopt |
So, maybe you need a utils with some code typography skills only. for your case: class A {
// declaring of new private symbols in lexical scope of a class
symbol somePrivatePropertyName;
symbol somePrivateMethodName;
// [somePrivatePropertyName] = 'this is private value';
// somePrivatePropertyName= 'this is public value with a `somePrivatePropertyName` property name';
[somePrivateMethodName]() {
// it is a private method
}
somePrivateMethodName() {
// it is a public method with `somePrivateMethodName` name
}
methodA(obj) {
this[somePrivateMethodName](); // call private method
this.somePrivateMethodName(); // call public method
this[somePrivatePropertyName]; // access private property
...
} ^^. There is a layout solution: // A utils function
const definePrivated = (f, ...args) => f(...args);
// Define a class with privated names
A = definePrivated(( // define a privated name part
somePrivatePropertyName = Symbol(),
somePrivateMethodName = Symbol()) => class { // and a class part
[somePrivateMethodName](x) {
console.log("Yes, in private method:", this[somePrivatePropertyName], 'and', x[somePrivatePropertyName]);
x[somePrivatePropertyName] = 'PrivateProperty is property and defaut is undefined value, updated.';
x.methodB(x);
}
methodA() {
this[somePrivatePropertyName] = 200;
this[somePrivateMethodName](new A);
}
methodB(obj) {
console.log(obj[somePrivatePropertyName]); // updated
}
}); Please, try it: > obj = new A;
> obj.methodA();
yes, in private method: 200 and undefined
PrivateProperty is property and defaut is undefined value, updated. Done. hahaha~~ Now, No new concept! No No new semantics! No magic! No community breaking! No new spec! Good! 👍 |
Could you elaborate on the integrity goals you are referring to?
@aimingoo Almost, except without Symbol.private you can access the symbols with getOwnPropretySymbols. So there would need to at least be spec for that. Edit: for clarification |
And, passportcard = Symbol();
A = definePrivated(..., passportcard);
B = definePrivated(..., passportcard); Both A and B Class can access both private members. ^^. @littledan yes, need disable public these OwnPropertySymbols, get, enumerate, etc. |
@aimingoo friend classes are achievable in my proposal too, since private symbol could be created outside of class and shared between several class declarations. My proposal is only trying to introduce more ergonomic syntax for symbols. @littledan are you talking about
I'm not sure about this assumption. I have opposite opinion, especially taking into account how many issues was caused by |
All of MM's concerns can be solved with Private Symbols. His biggest, proxy transparency, can just have it so that proxies throw when the key they're trapping is a private symbol. As for passing a reified private symbol through a membrane, he could decide to throw. Or just let it pass through, since it can't be used to extract data from the other side of the membrane (the membrane will throw on private symbol properties, per above).
We can both have private symbols and
Yup. See the first part of my comment. |
See the notes of the September 2018 TC39 meeting where we discussed this issue. For example, about making private symbols work, @erights said at that meeting:
Have you convinced @erights of your ideas above? I'm pretty sure the idea there is within the spectrum of things that was considered in the ES6 cycle and rejected. |
No, but that' doesn't change that it's possible. See zenparsing/proposal-private-symbols#7 (comment). Note, too, that in the last TC39 meeting we were discussing this under the assumption of transparent proxies (where There are really 3 orthogonal choices:
Private Fields chose non-transparency, But Proxy transparency is an orthogonal decision to encapsulation. Private symbols can choose non-transparency to solve the MM's goals. I'm fine with doing whatever will satisfy membranes. I do not want an incorrect idea about proxy transparency to force our hand on branding. That leaves us with the last two choices. Class fields choose But branding is also an orthogonal decision to both proxies and syntax. This decision affects what we can reify to. If we choose branding, symbols just don't make sense. But if we choose prototype-lookup, we can reify to either a symbol or a (modified) |
Personally, I would be fine with a follow-on proposal that reifies private symbols, with private symbols having the semantics that they are not transparent through Proxies and do not do prototype lookup. I see this as actually compatible with "branding"--we could include the same kinds of exception throwing behavior with A few issues with this proposal, which led me to not pursue it in this repository:
|
Yeah I know the difference. I just want to say use |
If you never export (leak) the private symbol, there would be no way to monkey patch. |
@hax I meant monkey patching Symbol.private. So when Symbol.private is called it's not the original function and you can capture all private fields. |
Could you please clarify this?
It's very arguable. Do you have any facts (e.g. result of some poll) behind such statement?
Are you talking about returning symbol from a method or something like this?
What invariants aren't maintained? Is zenparsing/proposal-private-symbols#7 one of them? You may read my last comment about it and why it's not an issue of private symbols at all. |
To interpret how these cases would apply to private symbols, assuming that private symbols are primitive (as symbols are), then there's no such thing as a "proxy to a private symbol". Instead, when "fT" represents a private symbol of interest on one side of the membrane, "fP" should be whatever the corresponding value is on the other side of the membrane. If private symbols pass though membranes unaltered (as all primitive values do, including symbols), then "fP" and "fT" are the same and we only have four cases, not eight. |
The bottom of that wiki page restates (and improves the phrasing of) the email at |
Thank you @erights ! I will try my hard to read it and understand it. If there was a runnable testcase I think it would be easy for us to investigate how the design affect membrane. Anyway, thank you for your information! |
If you'd like to try to create tests based on the eight cases, I could advise. Thanks! |
@erights, as I said before there is a solution that makes your test reachable using function isPrimitive(obj) {
return obj === undefined
|| obj === null
|| typeof obj === 'boolean'
|| typeof obj === 'number'
|| typeof obj === 'string'
|| typeof obj === 'symbol'; // for simplicity let's treat symbols as primitives
}
function createWrapFn(originalsToProxies, proxiesToOriginals, unwrapFn) {
// `privateHandlers` are special objects dedicated to keep invariants built
// on top of exposing private symbols via public API
// we also need one-to-one relation between `privateHandler` and `original`
const privateHandlersOriginals = new WeakMap();
// we're keep track of created handlers, so we'll be able to adjust them with
// newly exposed private symbols
const allHandlers = new Set();
// just simple helper that creates getter/setter pair for specific
// private symbol and object that gets through membrane
function handlePrivate(handler, privateSymbol) {
const original = privateHandlersOriginals.get(handler);
Object.defineProperty(handler, privateSymbol, {
get() {
return wrap(original[privateSymbol]);
},
set(v) {
original[privateSymbol] = unwrapFn(v);
}
})
}
function wrap(original) {
// we don't need to wrap any primitive values
if (isPrimitive(original)) return original;
// we also don't need to wrap already wrapped values
if (originalsToProxies.has(original)) return originalsToProxies.get(original);
const privateHandler = {};
privateHandlersOriginals.set(privateHandler, original);
allHandlers.add(privateHandler);
// note that we don't use `original` here as proxy target
// ↓↓↓↓↓↓↓↓↓↓↓↓↓↓
const proxy = new Proxy(privateHandler, {
apply(target, thisArg, argArray) {
thisArg = unwrapFn(thisArg);
for (let i = 0; i < argArray; i++) {
if (!isPrimitive(argArray[i])) {
argArray[i] = unwrapFn(argArray[i]);
}
}
// but we use `original` here instead of `target`
// ↓↓↓↓↓↓↓↓
const retval = Reflect.apply(original, thisArg, argArray);
// in case when private symbols is exposed via some part of public API
// we have to add such symbol to all possible targets where it could appear
if (typeof retval === 'symbol' && retval.private) {
allHandlers.forEach(handler => handlePrivate(handler, retval));
}
return wrap(retval);
},
get(target, p, receiver) {
receiver = unwrapFn(receiver);
// but we use `original` here instead of `target`
// ↓↓↓↓↓↓↓↓
const retval = Reflect.get(original, p, receiver);
// in case when private symbols is exposed via some part of public API
// we have to add such symbol to all possible targets where it could appear
if (typeof retval === 'symbol' && retval.private) {
allHandlers.forEach(handler => handlePrivate(handler, retval));
}
return wrap(retval);
},
// following methods also should be implemented,
// but it they are skipped for simplicity
getPrototypeOf(target) { },
setPrototypeOf(target, v) { },
isExtensible(target) { },
preventExtensions(target) { },
getOwnPropertyDescriptor(target, p) { },
has(target, p) { },
set(target, p, value, receiver) { },
deleteProperty(target, p) { },
defineProperty(target, p, attributes) { },
enumerate(target) { },
ownKeys(target) { },
construct(target, argArray, newTarget) { },
});
originalsToProxies.set(original, proxy);
proxiesToOriginals.set(proxy, original);
return proxy;
}
return wrap;
}
function membrane(obj) {
const originalProxies = new WeakMap();
const originalTargets = new WeakMap();
const outerProxies = new WeakMap();
const wrap = createWrapFn(originalProxies, originalTargets, unwrap);
const wrapOuter = createWrapFn(outerProxies, originalProxies, wrap)
function unwrap(proxy) {
return originalTargets.has(proxy)
? originalTargets.get(proxy)
: wrapOuter(proxy);
}
return wrap(obj);
}
const privateSymbol = Symbol.private();
const Left = {
base: {
[privateSymbol]: ''
},
value: '',
field: privateSymbol,
};
const Right = membrane(Left);
const { base: bT, field: fT, value: vT } = Left;
const { base: bP, field: fP, value: vP } = Right;
// # set on left side of membrane
// ## set using left side field name
bT[fT] = vT;
assert(bP[fP] === vP);
bT[fT] = vP;
assert(bP[fP] === vT);
// ## set using right side field name
bT[fP] = vT;
assert(bP[fT] === vP);
bT[fP] = vP;
assert(bP[fT] === vT);
// # set on right side of membrane
// ## set using left side field name
bP[fT] = vT;
assert(bT[fP] === vP);
bP[fT] = vP;
assert(bT[fP] === vT);
// ## set using right side field name
bP[fP] = vT;
assert(bT[fT] === vP);
bP[fP] = vP;
assert(bT[fT] === vT); |
I skipped set on left side of membraneset using left side field namebT[fT] = vT; goes as usual, since happens on one (left) side of membrane assert(bP[fP] === vP);
bT[fT] = vP;
assert(bP[fP] === vT);
set using right side field namebT[fP] = vT;
assert(bP[fT] === vP);
bT[fP] = vP;
assert(bP[fT] === vT);
set on right side of membraneset using left side field namebP[fT] = vT;
assert(bT[fP] === vP);
bP[fT] = vP;
assert(bT[fP] === vT);
set using right side field namebP[fP] = vT;
assert(bT[fT] === vP);
bP[fP] = vP;
assert(bT[fT] === vT);
|
Fun fact: This pattern works regardless of the privacy approach used. Fun fact: According to @ljharb, we've got about a snowball's chance .... of getting TC39 to allow Proxy to be bypassed in a way that isn't used for all scenarios. (Again, correct me if I'm wrong). In the end, this pattern may be the fix for Proxy itself, and all cases involving |
Hi @Igmat , I do not yet understand your code; I am still studying it. I'm not yet sure what's going on, but I do have a question. In your first example test, of bT[fT] = vT; and assert(bP[fP] === vP); does the membrane get access to the private symbol itself? |
@erights, yes but only to those which were explicitly exposed. const exposedSymbol = Symbol.private();
const privateSymbol = Symbol.private();
class Base {
[exposedSymbol] = 'exposed private data';
[privateSymbol] = 'not exposed private data';
methodA() {
return this[exposedSymbol];
}
methodB() {
return this[privateSymbol];
}
}
const Left = {
base: new Base(),
field: exposedSymbol,
};
const Right = membrane(Left);
const bP = Right.base;
bP.methodA(); // `exposedSymbol` isn't trapped
bP.methodB(); // `privateSymbol` isn't trapped
// till this point `exposedSymbol` couldn't be directly used
// from `Right` side of membrane, only indirectly moving to methods
// or function that called on `Left` side of membrane
const fP = Right.field;
// from this point membrane is aware of `exposedSymbol`
// and since it could be directly used from `Right` side of membrane
// we have to trap such usages to keep original code invariants
bP.methodA(); // `exposedSymbol` isn't trapped, since call to it happens on the left side
bP.methodB(); // `privateSymbol` isn't trapped
bP[fP] = 'mutated exposed private data'; // but here `exposedSymbol` IS trapped
// since `privateSymbol` wasn't intentionally exposed by original code
// membrane isn't aware of it and in no way intercepts its usages
// so not exposed symbols still grant hard privacy which couldn't be
// trapped by any code, including membrane So exposing Also, if you want to keep all private symbols unexposed, regardless of what was the intention of code from one side of membrane, you're able to throw an exception when private symbols tries to cross the boundary. |
@erights do you have any other questions to this implementation? |
I will once I have time to get back to it. Hopefully next week. Sorry for the delay. |
Maybe I'm missing something, but isn't it very easy to get to the private Symbols from the outside? Consider this:
|
@p-bakker, Quote from there which describes, why your code won't trap
|
Right, knew I must have missed something :-) How well does this proposal do when thinking about future access modifiers like friend/inherited/protected, as discussed in #122? The # sigil approach does leave a path to adding those, albeit an odd one if the private keyword is not added as mandatory to the current proposal, in the form of
|
IMO much better then existing one. Since const internalSymbol = Symbol.private();
class A {
[internalSymbol] = 'something internal to specific module';
}
class A {
method() {
const instanceOfA = new A();
console.log(instanceOfA[internalSymbol]);
}
}
And this will be available with no additional standard changes, but talking about future extension of language spec, I see few available options. Each of them will require adding something like
Second is my latest idea, probably I'll make more detailed proposal for that. But as you can see, even though there are ideas about more ergonomic shorthand syntax for Only after that, there weill be sense to discuss syntax shorthand.
|
It's technically possible, but as my observation of the background logic of current proposal, the syntax for protected/friend will be even worse than On the other side, private symbol-based solution is much easy to offer a nice, keyword-based syntax. |
@lgmat I think this will suffice for now: just wanted to know if there's a sensible path forward to such access modifiers using this approach, because one area of feedback on the current proposal is the (apparent) lack of a path forward @hax don't think it'll cause a syntax storm, as the private/protected/public keywords could be added to change the meaning of the sigil (as discussed from #122 (comment) onwards), but I agree (as you mentioned in #122 (comment) as well) that A: changing the mental model of what the sigil means down the road ain't good (and might run into objections in the TC39 committee for just that reason) and B: adding access modifiers down the road causing an implicit |
@p-bakker What I call syntax storm is: Actually as I already said in many comments, keyword can solve some problem easily (like confusion of [[Define]] and [[Set]], new ASI hazard, etc.), but the committee refuse to add keyword, even some champions insist on the decision of "no keyword in public field" is not affected by "no keyword in private field", but to be honest, it's very weak from the view of outside of TC39. |
@erights so did you have a chance to take a closer look on this? Do you have any objections to my statement that |
@erights, it was 2 weeks ago. |
@erights I also read the @Igmat 's implementation and I think it should be able to satisfy membrane transparency. The idea is simple, whenever a private symbol is passed from the other side of membrane, use it to trap get/set and do wrap/unwrap. I used to think we need add But, we still need you to confirm whether it's ok (because I remember there is |
Disclaimer
My intent here is to show that there are good alternatives to existing
private
part of this proposal, that were not properly assessed. Probably it happened, because committee has a very limited time and amount of members involved, while such long time for discussion around encapsulation (years or even decades) has lead to loosing clear sight on the problem and possible solutions.Proposal
It's based on the my proposal in #134 and
Symbol.private
- I won't repeat them fully here, but I hope that you may visit these links to get more background.This comment (#147 (comment)) to my initial proposal inspired me to make another one, which dismisses problem with
this
having more complex semantics than it has now.Goals:
private x
/this.x
pairthis.x
andobj.x
Solution
Use
Symbol.private
with existing lookup mechanism, existing mental model for accessing properties and small syntax sugar to make use of such symbols more ergonomic, by declaring symbol variable in lexical scope of a class.Syntax 1 (less verbose):
Syntax 2 (more generic):
This option is a little bit more verbose, and probably such syntax isn't obvious enough to use for declaring private instance data, but as I shown above it could be used for something else, which is probably make some sense.
Advantages:
#
-sigil, so no risk for breaking communityprivate
Disadvantages:
# is the new _
couldn't be used for educationConclusion
Since I don't pretend to replace existing proposal with this one right now, because, obviously, my proposal also needs additional assessment, preparing document that reflects changes to ES spec, creating tests, implementing babel-plugin for transpiling, adding support to JS-engines and most importantly wider discussion with community (BTW, existing also needs it), I only hope that this clearly shows the fact that there are still good alternatives, which weren't taken into account.
What is your opinion on that @littledan, @zenparsing, @jridgewell, @ljharb?
P.S.
This syntax also adds a bonus usage scenario:
And it's only one possible scenario, there could be dozens of them.
The text was updated successfully, but these errors were encountered: