-
Notifications
You must be signed in to change notification settings - Fork 113
Private fields/properties & the membrane pattern... #158
Comments
Cross posting zenparsing/proposal-private-symbols#7 (comment): If you take const obj = {
prop: {}
};
const membrane = new Membrane(obj);
assert.notEqual(obj.prop, membrane.prop); There must be some way for the proxy to either trap the property and wrap the value, or the proxy must throw. Either is acceptable for blue-yellow. Now imagine using privates across the membrane (this requires a reified const priv = Symbol.private();
const obj = {
[priv]: {},
symbol: priv
};
const membrane = new Membrane(obj);
// pass the private through
const priv = membrane.symbol;
// Private access is non-trapable, using transparent semantics
// This is a break in blue-yellow
assert.equal(obj[priv], membrane[priv]); |
@jridgewell Thanks for the explanation. I was under the impression that when it comes to Private Names, none but the declaring |
Btw, I get the scenario problem for Private Symbols. What I'm not able to comprehend is why this is a problem for allowing a |
@rdking it's not a problem, it just requires a little bit more complex |
@Igmat If it's not a problem, then why the decision to have Proxy break on access of a private? |
@rdking, because if Proxy tunnels privates, then In case of current semantics: class A {
#priv;
someMethod() {
this.#somePriv; // <- brand-check happens here and `this` is definitely built with constructor of A
}
} If proxy will tunnel privates, then class A {
#brand = this;
#brandCheck() {
if (this.#brand !== this) throw `Brand check failed`;
}
brandCheckedMethod() {
this.#brandCheck();
}
} Also, there were thoughts that proper |
@Igmat Maybe we have different definitions of tunneling. When I say tunneling, I mean that the Proxy will (in the case of a get call for example) test the type of the property name. If the type is a PrivateName, then it will immediately forward the call to |
class A {
#priv = 1;
someMethod() {
return this.#priv;
}
}
const a = new A();
const aProxy = Proxy(a, {
get(target, name, receiver) {
if (name = 'somMethod') {
return a[name].bind(aProxy);
}
}
});
// in case of tunneling following line will return 1
// because `this.#priv` when applied to `this` which is Proxy,
// instead of calling any traps, immediately applied to its `target`
// in case of NOT tunneling, following code will throw exception,
// since `aProxy` can't have such `#priv` field
aProxy.someMethod(); In both cases proxy traps DON'T get called when access to |
@Igmat Of course that fails. Didn't you forget to catch the function calls? const handler = {
get(target, name, receiver) {
var retval = Reflect.get(target, name, receiver);
if (retval && ["function", "object"].includes(typeof(retval)) {
if (typeof(retval) == "function") retval = retval.bind(receiver));
retval = new Proxy(retval, handler);
}
return retval;
},
apply(target, thisArg, args) {
for (var i=0; i<args; ++i) {
if (args[i] && ["function", "object"].includes(typeof(args[i]))
args[i] = new Proxy(args[i], handler);
}
var result = Reflect.apply(target, thisArg, args);
if (retval && ["function", "object"].includes(typeof(retval))) {
retval = new Proxy(retval, handler);
}
return retval;
}
}
const aProxy = new Proxy(a, handler); I tend to write like this. If we replace your proxy definition with this, then in the case of tunneling, you'll still get a |
@rdking you missed my point. 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) {
return 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 proxy = new Proxy(original, {
apply(target, thisArg, argArray) {
thisArg = unwrapFn(thisArg);
for (let i = 0; i < argArray; i++) {
if (!isPrimitive(argArray[i])) {
argArray[i] = unwrapFn(argArray[i]);
}
}
const retval = Reflect.apply(target, thisArg, argArray);
return wrap(retval);
},
get(target, p, receiver) {
receiver = unwrapFn(receiver);
let retval = Reflect.get(target, p, receiver);
return wrap(retval);
},
// following methods also should be implemented, but it doesn't matter
// for problem that I'm trying to show
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;
}
}
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 proxyToOriginals.has(proxy)
? proxyToOriginals.get(proxy)
: wrapOuter(proxy);
}
return wrap(obj);
} It's skeleton for Prove:Let's assume we have following class: class A {
#somePriv = 1;
#a() {
return this.#somePriv;
}
b() {
return this.#a();
}
#c() {
return this.b();
}
d() {
return this.#c();
}
e() {
return this.d();
}
} Let's wrap instance of this class in const original = new A();
const a = membrane(original);
console.log(a.e()); // prints `1` And important here is execution flow:
If privates aren't addressable they will be executed always inside of So why do we need tunneling?But while 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 doSomethingUseful(...args) {
// this function could add some useful functionality
// e.g. logging, reactivity, and etc
}
function notMembrane(obj) {
const proxies = new WeakMap();
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 (proxies.has(original)) return proxies.get(original);
const proxy = new Proxy(original, {
apply(target, thisArg, argArray) {
const retval = Reflect.apply(target, thisArg, argArray);
doSomethingUseful('apply', retval, target, thisArg, argArray);
return wrap(retval);
},
get(target, p, receiver) {
const retval = Reflect.get(target, p, receiver);
doSomethingUseful('get', retval, target, p, receiver);
return wrap(retval);
},
// following methods also should be implemented, but it doesn't matter
// for problem that I'm trying to show
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) { },
});
proxies.set(original, proxy);
return proxy;
}
return wrap(obj);
} Main difference comparing to ExampleLet's assume we have following class (actually, it is the same as in section about membrane): class A {
#somePriv = 1;
#a() {
return this.#somePriv;
}
b() {
return this.#a();
}
#c() {
return this.b();
}
d() {
return this.#c();
}
e() {
return this.d();
}
} Let's wrap instance of this class in const original = new A();
const a = notMembrane(original);
console.log(a.e()); // prints `1` if tunneling happens
// throws if tunneling doesn't happen And again important here is execution flow: if
|
@Igmat note that internal slots on builtins also don’t tunnel, so this is already an existing problem - or non-problem, depending on your perspective. Private fields only increase the potential number of objects it can occur on. |
@ljharb thanks for pointing it again and again. I do know about internal slots, but user can't add them to his/her classes (at least, I'm not aware of any common possibilities for it), so only thing I need to do is to add special-case handling for such objects from standard library. |
This is 1 part of the reason why I raised the question.
I'm aware of this as well. I make use of this fact quite often. As for your execution flow in the tunneled case, I was expecting something different at 4.viii. Tunneling the way I conceived it would mean the context would be What I still don't see is why this has anything to do at all with brand-checking? Isn't just a duck-type test to see if a particular PrivateName is known to a class instance? |
The user can extend builtins, which allows their own class instances to have those very internal slots. |
@ljharb Even if they extend builtins it's easy enough to do |
@shannon no, |
@ljharb Ok, are there are other cross realm ways to detect these builtins? Similar to https://github.com/ljharb/is-typed-array |
Nothing generic; all of the "is-" modules I publish are built for precisely that reason - to provide production-ready solutions to cross-realm brand-checking. |
Yeah sorry, I get that. But if you have a small set of well defined objects it's not quite the same as an arbitrary number of arbitrary objects that would be introduced with privates. If you are careful you may be able to work around all the built-ins. I just don't know if I would say the problem already exists. |
I'll agree it's a different scale. Keep in mind there's language and browser/engine builtins to consider, all of which might have internal slot-like behavior. As for user objects, if they're already using a WeakMap to simulate private fields, the same problems would occur on an arbitrary number of arbitrary objects. |
I wouldn't say the problem already exists. Scale is essential to consider in this case. Once private fields are introduced everyone is going to start using them everywhere. That's going to greatly increase the chance of issues. While Aurelia and its community can potentially work around this a bit and provide guidance, it's going to be painful and we'd still prefer a solution that allows tunneling or something like it. I also think that a less error-prone integration of private field and Proxy is better for the language and the average JavaScript developer. Call me crazy, but if branding and cross-realm detection of brands is the main thing holding back something like tunnelling, then perhaps these concerns should be separated and a specific solution to the cross-realm brand problem should be proposed. It seems to me that there's a conflation of issues which could be avoided. |
@EisenbergEffect note that I’m fully in favor of figuring out a way to address this tunneling issue - i just don’t think it needs to block class fields from progressing, since any solution to it will also handle all those other cases. |
@ljharb Of all the issues I and others have raised with class-fields, no single issue is enough to block class-fields from progressing. On this, we agree. What's bothersome, however, is that the amalgamation of all of those issues doesn't appear to be enough in the perspective of the proponents. But let me not drift off topic... I get that Proxy already has issues with handling built-ins other than Array. What does that have to do with tunneling private fields? From what I've read in here, the issue of tunneling has nothing to do with the issue of brand-checking. If a Proxy properly tunnels access to private fields, then it is responding to them in the same way as the target object would, making any attempt to brand-check the proxy equivalent to brand-checking the target. So if that's not right, what did I miss? |
If you do brand checking now, with WeakMaps, i believe you’d run into the same issue - the receiver would be the proxy, which isn’t in the weakmap, and the brand check would throw. |
@ljharb, I said that I'm not aware of common possibilities.
This isn't common use case.
Even though this is true, it doesn't dismiss the fact that only thing I have to do in order to workaround issue with
This is also very uncommon practice - in most cases users simulate private state (actually And even though I agree that
First of all, this is problem for brand-checking. Proper brand-check should throw, when brand-checked method called on proxy and not instance for which it was designed. class A {
#brand;
constructor() {
this.#brand = this;
}
brandCheckedMethod() {
if (this.#brand !== this) throw "Brand check is failed";
// do some useful stuff, knowing that you work with
// direct instance of A and proxy around such instance
}
} or using |
If it helps. When I need to workaround the Proxy / WeakMap issue I use an extended WeakMap and a specialized Proxy that allows me to tunnel to the target. Something like this: const targetMap = new WeakMap();
function createProxy(target, handler) {
const proxy = new Proxy(target, handler);
targetMap.set(proxy, target);
return proxy;
}
class ProxyWeakMap extends WeakMap {
get(key) {
key = targetMap.get(key) || key;
return super.get(key);
}
set(key, value){
key = targetMap.get(key) || key;
return super.set(key, value);
}
has(key) {
key = targetMap.get(key) || key;
return super.has(key);
}
delete(key) {
key = targetMap.get(key) || key;
return super.delete(key);
}
}
const obj = {};
const map = new ProxyWeakMap();
const proxy = createProxy(obj, {});
map.set(proxy, true);
console.assert(map.has(obj) === true);
console.assert(map.has(proxy) === true); |
I do this all the time with no issue. That's not to say it works effortlessly. I intercept This is how I do private fields via WeakMap, and also one of the reasons why I've been campaigning so hard against the "field" concept. If it's not part of the prototype, I can't see it in a Proxy while the constructor is running. As I've often repeated, there are good use-cases for data properties on prototypes. |
@rdking @Igmat @jridgewell @hax @littledan @ljharb @isiahmeadows @zenparsing @caridy @bakkot , sorry for the long delay! I am trying to understand what semantics is being proposed for private symbols. Please forget proxies and membranes for the moment while we clarify some more basic things. What does the following code do: const x = Symbol.private();
const y = Symbol.private();
const z = Symbol.private();
const obj = Object.freeze({
[x]: 1
});
// assume the following are tried separately, say at a command line,
// so that if one throws the next is still tried.
obj[x] = 2;
obj[y] = 3;
typeof z;
Object(z) === z; Thanks. |
@erights obj[x] = 2; //gets silently ignored, or throws if strict mode is on
obj[y] = 3; //gets silently ignored, or throws if strict mode is on
typeof z; //I don't know, probably returns "symbol"
Object(z) === z; //returns false. A private symbol is a primitive In general, unless I'm mistaken, private symbols are identical to public symbols with the exception that no attempts to reflect properties created with them will work. They don't even show up in getOwnPropertySymbols. Proxy handler functions also forward access attempts against private symbols directly to the engine invariant function, bypassing the proxy handler. |
@rdking Many people have asked us why "wet"/"dry" or "blue"/"yellow"? Even when talking among ourselves in the same room with whiteboard, when we said things like "opposite side" we always meant the other opposite. I know I still sometimes do it myself anyway, but I try not to. I don't think of a membrane's proxy as being any kind of target, so I'm still puzzled. |
That's exactly what I'm exploring: what is the point this proposal is actually trying to achieve, and what are incidental points that are coming along for the ride, perhaps because we had not known how to unbundle them?
This is actually one of the points that got me thinking again in these directions. Somewhere, on one of these threads, someone (perhaps you?) pointed out that the non-membrane use of proxies to keep track of state change does not have a problem with WeakMaps, because it would wrap the WeakMap in a proxy, and so see the updates to the WeakMap. I suggest that it should be so as well for PrivateName-like special objects. If anyone knows where I may have seen this point, please post the link. Thanks. |
That's close to what I said. Not quite right though. Go look here to see what I meant. There is a way in user-land code to make Proxy and WeakMap compatible. It essentially unwraps the Proxy before handing it off to WeakMap. Think of it as an indirect Membrane around WeakMap. |
Here's the link to the original comment. |
I may be confusing two things, but I think the one I'm thinking of was less invasive --- putting proxies around individual WeakMap instances rather than replacing the whole WeakMap class. |
I only remember seeing a prose description, not code. And I only vaguely remember that :( |
I did mention it one other time before this, but the post I linked to is the first time where I revealed the code that makes it work. |
Link please? |
In any case, whether we dereference my vague and possibly misunderstood memory or not, the important question is, would this work and satisfy the goals? I don't really understand the constraints of this data-binding use case for proxies. For WeakMap-based private state, could you just put a proxy around weakmap instances in order to observe the updates there? If so, would the same work for PrivateName-like special objects? |
@erights Wow. It's amazing how hard it is to find a single post in these threads. When I find it again, I'll post the link here. I could implement that logic I showed you using Proxy, but it would take more work than the way I did it. The logic I created could easily be adapted to transform such objects into keys. I can take the logic I've already created and whip up an example if you'd like. |
That would be awesome, thanks! It should really help us figure out what we would technically lose if we move from a) your private symbols or b) tc39's current stage 3 proposal to c) PrivateName-like special objects with name-side faulting (which needs a better name than PNLSOWNSF ;) ). Process-wise, I don't deny that any such change in direction would be painful and difficult. But I certainly agree we should explore the technical issues first, and build any case from there. |
@erights You know what's funny? I remembered it so vividly because it was the last post in this thread before you started posting today! |
Yes, that was it! Rereading it, I still don't understand all of it, but the central point is there. |
Basically, if I wrap the Proxy constructor and map all created Proxy objects back to the RT, provide a has() function to check to see if an object can be unwrapped, and an unwrap function to retrieve the RT, then extend WeakMap so that each of the CRUD methods calls Proxy.unwrap() for each object that passes Proxy.has(), then all code that uses the extended WeakMap and wrapped Proxy will simply work. Since both WeakMap and Proxy can be replaced with their alternates, no code in the wild need be augmented to gain this capability. |
Wait. I just re-read my own post without the distractions around me. That's a different concept. In that post I was referring to how I use Proxy to peek in on a constructor's actions. With that kind of logic, I can catch any public property being set. What I can't do, however, is observe any private actions. This kind of code would not be able to catch the setting of private symbols. I'll work this into the example code and post a link here when it's finished. |
It seems that I missed very important conversation while was asleep. @erights, it seems that part of the conversation and your questions here is very related to #183. Answering some of your question from this thread:
No, we can't. The only way to fully workaround it is monkeypatching of
With my implementation of a
The real constraint is that we need to use |
@erights This demo should reasonably describe the behavior of private symbols, tunneling Proxies, Proxy-safe WeakMaps, and how to snoop in on what the constructor is doing to its context object. The one thing you'll notice is that private symbols simply don't show up anywhere outside where they are defined, unless they are leaked. The only odd things about my implementation of private symbols are that:
|
I have to say, I am very confused by this thread. I pursued WeakMap semantics partly because @erights has repeatedly said over the past 4-5 years that it is the only acceptable semantics for private. I wanted to get consensus in committee, and that included consensus with @erights' requirements. I summarized some reasons that I thought were very important for the committee about this choice at https://gist.github.com/littledan/f4f6b698813f813ee64a6fb271173fc8 -- I don't see why this particular aspect of membrane-ability invalidates any of those reasons. In the end, has this all been about Symbols being primitives rather than objects? |
@littledan If I understand correct, yes. And strictly speaking, it's about "primitives can not be proxied" (but PrivateName can). But @Igmat invent a trick to overcome it. And adding "whitelist of private symbols" for proxies can also solve it. (It seems @jridgewell would prefer that way?) |
@hax @littledan Unless I'm mistaken, 2 solutions were forwarded for this problem:
IMO, whitelists are a nice but easily forgotten solution to the problem. If the use of a constructor to declare public instance variable can be considered a foot-gun due to ignorance of or forgetting the correct way to call super, then a whitelist for Proxy would be equally such a foot gun. It would require that for each private symbol returned from a public function of the class, that the same symbol would need to appear in the whitelist of the membrane. That doesn't seem like something people will readily remember to do. Building membranes to be aware of private symbols makes more sense, and doesn't require extra language modification to support. While I'm more in favor of not allowing private symbols to be leaked at all, that's more of a topic for a different thread. |
I never agree that 😂 . Especially it's very easy to discover the "bug" of forgetting, so it should never be treat as real "foot-gun" as my understanding of "foot-gun".
Also disagree. Only membrane usage need whitelist, all other main use cases of Proxies never need it. Consider the complexity of membrane implementation, I think the author of membrane library will never "forget" it. 😆 |
@hax That comment was targeted toward those who think public-fields as currently defined in this proposal is a good idea. I just mentioned on of the reasons I was given for why pseudo-declarative instance-specific properties (fields) is a good idea. For those of us who don't buy it, of course that argument doesn't hold. 😜 |
In case anyone is watching this thread but not #183 , I should mention that this discussion is currently continuing there. |
I think we've concluded by now, through discussion in this thread, other threads, and at the January 2019 TC39 meeting, that we're leaving the Proxy semantics of private fields as is. |
Ok. Can someone explain to me what the difficulty is with the membrane pattern and private fields, because I'm not sure that I understand the problem at all. For me, it seems fairly obvious that the correct choice is to tunnel private field access through Proxy. If private fields are supposed to be completely transparent to running code other than
Function.prototype.toString
, then Proxy should forward all requests involving a "PrivateName" through to the correspondingReflect
handler.So what am I missing?
I get that there's some concern that tunneling will expose private fields and allow data to pass through the membrane unwrapped, but I just don't see how.
If private fields were allowed to tunnel, I don't see how I'd get back anything other than:
The text was updated successfully, but these errors were encountered: