[Harmony proxies] Revisiting the forwarding proxy pattern
As I understand it, the difference between the two patterns you presented is not so much in using a "full" forwarding handler versus an "own" forwarding handler, but in the fact that you pass different initial prototypes.
Consider: var p = Proxy.create(fullForwardingHandler(target), Object.getPrototypeOf(target));
Here, p and target also refer to the same prototype, it's orthogonal to specifying a "full" or "own" handler. Regardless of the kind of forwarding handler, inheritance will work just fine. I understand they work fine for different reasons, but is the difference really worth complicating the API by introducing two kinds of forwarding handlers?
To be clear: I'm not opposed to your idea of distinguishing "own" from "full" forwarding handlers, but since the former can easily be defined in terms of the latter (delete all derived traps), the question becomes: should we standardize both or should we standardize just the minimum features, and allow developers to build other useful abstractions on top?
Cheers, Tom
2011/3/22 David Bruant <david.bruant at labri.fr>
Le 23/03/2011 11:03, Tom Van Cutsem a écrit :
Hi David,
As I understand it, the difference between the two patterns you presented is not so much in using a "full" forwarding handler versus an "own" forwarding handler, but in the fact that you pass different initial prototypes.
Consider: var p = Proxy.create(fullForwardingHandler(target), Object.getPrototypeOf(target));
Here, p and target also refer to the same prototype, it's orthogonal to specifying a "full" or "own" handler. Regardless of the kind of forwarding handler, inheritance will work just fine. I understand they work fine for different reasons, but is the difference really worth complicating the API by introducing two kinds of forwarding handlers?
Yes sorry, I was a bit drafty. The concerns are orthogonal, I agree. And I forgot to say it, but my point was not to standardize the ownForwardingHandler. I just used the notation as a shortcut. I just wanted to discuss forwarding patterns when it comes to inheritance because the current forwarding proxy doesn't discuss the point and when it comes to fixing the proxy, surprising results could arise (since inheritance changes because target and proxy prototypes aren't the same object). Actually, if forwarding handlers were part of the spec, would it make sense to fix them? shouldn't "return undefined;" be the default fix trap? (harmony:proxy_defaulthandler) (just the default, of course, the user can change it anytime)
To be clear: I'm not opposed to your idea of distinguishing "own" from "full" forwarding handlers, but since the former can easily be defined in terms of the latter (delete all derived traps)
I would like to point out that "hasOwn" and "keys" are both own and derived traps, so you can get from a full handler to a own handler by removing all proto-climbing traps. Even if very close from each other, "own"VS"proto-climbing" traps is different from "fundamental"VS"derived" (even if getPrototypeNames and getPropertyDescriptor becomes derived)
, the question becomes: should we standardize both or should we standardize just the minimum features, and allow developers to build other useful abstractions on top?
I do not have a definite opinion on the question yet, but since there is already a strawman on maybe cutting the forwarding handler into two (strawman:derived_traps_forwarding_handler), I thought it would be a good idea to mention that other concerns may affect the decision.
The concern that led my discussion was that the current proposal since to consider that "forwarding proxy" has a unique definition which is "forward everything to the target". So my question is: what is a "forwarding proxy"?
- something forwarding all operations (own and inherited) to a target object? (including the case where the target is itself a proxy)
- something forwarding own-layer operations and "delegating inheritance to the engine"?
- something forwarding own-layer operations and refering to its own prototype chain for inheritance?
I agree with you that the current definition allows all 3 to be achieved. But since I have pointed different definitions of what a forwarding proxy is, other people may think about it differently too.
I'm sorry for being a bit drafty sometimes. I think I should have started with the following question: What is a "forwarding proxy"? And my concerns aren't really about what to spec yet, but rather to discuss on what different "forwarding proxy" patterns people could expect and make sure the spec first address them (it will certainly always be the case) and second, can the spec help out if some cases are common or make sense.
I just wanted to discuss forwarding patterns when it comes to inheritance because the current forwarding proxy doesn't discuss the point and when it comes to fixing the proxy, surprising results could arise (since inheritance changes because target and proxy prototypes aren't the same object). Actually, if forwarding handlers were part of the spec, would it make sense to fix them? shouldn't "return undefined;" be the default fix trap? ( harmony:proxy_defaulthandler) (just the default, of course, the user can change it anytime)
"return undefined;" is definitely a sensible default. The current default goes a bit further: if the target object is frozen, it will fix the forwarding proxy in such a way that the proxy and the target have the exact same own properties. I don't recall us giving much thought to this default implementation though. In fact, I think it's broken since it doesn't take into account any custom proxying behavior. As an example: say I implement a "logging" proxy that just logs all operations performed on "target". It's sensible to extend the default forwarding handler to do this. Now, if both the target and the proxy are frozen, the logging behavior will be lost! The solution is for my logging proxy to override the fix() trap and perhaps return a property descriptor map that replaces each own property in target with a wrapped version that still performs the logging. If fix() returns "undefined" by default, the above scenario would lead to an exception instead of silently doing the wrong thing.
To be clear: I'm not opposed to your idea of distinguishing "own" from "full" forwarding handlers, but since the former can easily be defined in terms of the latter (delete all derived traps)
I would like to point out that "hasOwn" and "keys" are both own and derived traps, so you can get from a full handler to a own handler by removing all proto-climbing traps. Even if very close from each other, "own"VS"proto-climbing" traps is different from "fundamental"VS"derived" (even if getPrototypeNames and getPropertyDescriptor becomes derived)
, the question becomes: should we standardize both or should we standardize just the minimum features, and allow developers to build other useful abstractions on top?
I do not have a definite opinion on the question yet, but since there is already a strawman on maybe cutting the forwarding handler into two ( strawman:derived_traps_forwarding_handler), I thought it would be a good idea to mention that other concerns may affect the decision.
The concern that led my discussion was that the current proposal since to consider that "forwarding proxy" has a unique definition which is "forward everything to the target". So my question is: what is a "forwarding proxy"?
- something forwarding all operations (own and inherited) to a target object? (including the case where the target is itself a proxy)
- something forwarding own-layer operations and "delegating inheritance to the engine"?
- something forwarding own-layer operations and refering to its own prototype chain for inheritance?
I agree with you that the current definition allows all 3 to be achieved. But since I have pointed different definitions of what a forwarding proxy is, other people may think about it differently too.
The current proposed version was definitely designed according to version .#1: something forwarding all operations to the target. It has the advantage that if |target| is itself a proxy, the forwarding handler will trigger the most specific (derived) traps. OTOH you've raised good points about surprises that occur if a developer only "half overrides" the default forwarding handler (i.e. only overrides the fundamental trap but not the derived one).
I'm not in favor of splitting the forwarding API into multiple types of forwarding proxies. I would still prefer option #1 as I feel it's the simplest and most transparent definition. Option #2 seems to me an OK alternative, trading one issue for another.
As for option #3: when it comes to forwarding proxies (i.e. proxies that forward operations to a specific target object), I never did consider a scenario where the proxy's own prototype would be different from Object.getPrototypeOf(target). In case the proxy's prototype is different from the target's prototype, it's hard to tell which one to prefer in general.
In the Proxy API's current design, the proxy's prototype is used for two things only:
- Object.getPrototypeOf(proxy) returns it
- proxy instanceof Constructor uses it
We opted for a design where the handler is in full control of properly implementing property inheritance. Currently, the Proxy API does not correlate property inheritance with the proxy's prototype. Option #3 would introduce such a correlation (for forwarding handlers, at least). I think option #3 makes sense for an alternative Proxy design where the proxy's prototype is also used to implement property inheritance. It's not consistent with the current design, however.
2011/3/24 Tom Van Cutsem <tomvc.be at gmail.com>
I just wanted to discuss forwarding patterns when it comes to inheritance
because the current forwarding proxy doesn't discuss the point and when it comes to fixing the proxy, surprising results could arise (since inheritance changes because target and proxy prototypes aren't the same object). Actually, if forwarding handlers were part of the spec, would it make sense to fix them? shouldn't "return undefined;" be the default fix trap? ( harmony:proxy_defaulthandler) (just the default, of course, the user can change it anytime)
"return undefined;" is definitely a sensible default. The current default goes a bit further: if the target object is frozen, it will fix the forwarding proxy in such a way that the proxy and the target have the exact same own properties. I don't recall us giving much thought to this default implementation though. In fact, I think it's broken since it doesn't take into account any custom proxying behavior. As an example: say I implement a "logging" proxy that just logs all operations performed on "target". It's sensible to extend the default forwarding handler to do this. Now, if both the target and the proxy are frozen, the logging behavior will be lost! The solution is for my logging proxy to override the fix() trap and perhaps return a property descriptor map that replaces each own property in target with a wrapped version that still performs the logging. If fix() returns "undefined" by default, the above scenario would lead to an exception instead of silently doing the wrong thing.
I've been thinking about the default implementation of the |fix()| trap. Another sensible default, other than just returning undefined unconditionally, is to freeze the proxy's structure, but to replace all original properties with accessor properties that still trigger the proxy's handler. A fixed forwarding proxy can then still intercept property access, but other than its get and set traps, none of its other traps would be called anymore:
fix: function() { // As long as target is not frozen, the proxy won't allow itself to be fixed if (!Object.isFrozen(this.target)) return undefined; var props = {}; var handler = this; Object.getOwnPropertyNames(this.target).forEach(function (name) { var desc = Object.getOwnPropertyDescriptor(this.target, name); // turn descriptor into a trapping accessor property props[name] = { get: function( ) { return handler.get(this, name); }, set: function(v) { return handler.set(this, name, v); }, enumerable: desc.enumerable, configurable: desc.configurable }; }); return props; },
(full forwarding handler available at < code.google.com/p/es-lab/source/browse/trunk/src/proxies/forwardingHandler.js
)
(untested because fix() isn't yet supported in fx4, see < bugzilla.mozilla.org/show_bug.cgi?id=600677>)
(reposting source code from previous message as plain text)
fix: function() { // As long as target is not frozen, the proxy won't allow itself to be fixed if (!Object.isFrozen(this.target)) return undefined; var props = {}; var handler = this; Object.getOwnPropertyNames(this.target).forEach(function (name) { var desc = Object.getOwnPropertyDescriptor(this.target, name); // turn descriptor into a trapping accessor property props[name] = { get: function( ) { return handler.get(this, name); }, set: function(v) { return handler.set(this, name, v); }, enumerable: desc.enumerable, configurable: desc.configurable }; }); return props; },
(reposting source code from previous message as plain text)
fix: function() { // As long as target is not frozen, the proxy won't allow itself to be fixed if (!Object.isFrozen(this.target)) return undefined; var props = {}; var handler = this; Object.getOwnPropertyNames(this.target).forEach(function (name) { var desc = Object.getOwnPropertyDescriptor(this.target, name); // turn descriptor into a trapping accessor property props[name] = { get: function( ) { return handler.get(this, name); }, set: function(v) { return handler.set(this, name, v); }, enumerable: desc.enumerable, configurable: desc.configurable }; }); return props; },
I really love this. The Object.isFrozen call is unfortunate but necessary since we only have one trap for the three extension-preventing methods. Throwing a TypeError on Object.preventExtension(proxy) because the target isn't frozen sounds a bit wrong to me though. Maybe that something could be passed as an argument to fix in order to discriminate which call happened.
From what I understand, all interaction that set [[Extensible]] to false
will end up calling the fix trap (+ TypeError||become) for proxies. As we can't know now what will later be added to the language to do that, it may be a good thing to be able to discriminate the origin of the fix trap call.
Also, an idea for so-called "forwarding" proxies could be that the proxy forwards preventExtension/seal/freeze calls to the target too. It could be a nice thing if target is itself a proxy.
Combining the ideas, we could have something like:
fix: function(type){ if (type === Proxy.PREVENT_EXTENSION) // syntax example Object.preventExtension(this.target); // a switch case could be a good idea too // same code than you starting at "var props" }
Even if not forwarding the call, it could allow better granularity than Object.isFrozen.
Now that I think about it, if forwarding Object.seal/freeze calls, the target and previously-proxy would act the same with very few exception. The non-configurability of all properties ensure that both keep the exact same property set will remain (non-deletable) with same [[enumerable]] for each (non-configurable). Your get/set method ensure that property access react the same in both objects. Besides Object.getOwnPropertyDescriptor and their identity, I can't think of any way to distinguish them.
2011/4/5 David Bruant <david.bruant at labri.fr>
(reposting source code from previous message as plain text)
fix: function() { // As long as target is not frozen, the proxy won't allow itself to be fixed if (!Object.isFrozen(this.target)) return undefined; var props = {}; var handler = this; Object.getOwnPropertyNames(this.target).forEach(function (name) { var desc = Object.getOwnPropertyDescriptor(this.target, name); // turn descriptor into a trapping accessor property props[name] = { get: function( ) { return handler.get(this, name); }, set: function(v) { return handler.set(this, name, v); }, enumerable: desc.enumerable, configurable: desc.configurable }; }); return props; },
I really love this. The Object.isFrozen call is unfortunate but necessary since we only have one trap for the three extension-preventing methods. Throwing a TypeError on Object.preventExtension(proxy) because the target isn't frozen sounds a bit wrong to me though. Maybe that something could be passed as an argument to fix in order to discriminate which call happened. From what I understand, all interaction that set [[Extensible]] to false will end up calling the fix trap (+ TypeError||become) for proxies. As we can't know now what will later be added to the language to do that, it may be a good thing to be able to discriminate the origin of the fix trap call.
Also, an idea for so-called "forwarding" proxies could be that the proxy forwards preventExtension/seal/freeze calls to the target too. It could be a nice thing if target is itself a proxy.
Indeed. I guess both approaches are equally feasible. In a secure wrapper scenario, where the 'target' is a trusted object and untrusted code can only access it through its forwarding proxy, I can imagine that it could be more desirable not to forward freeze|seal|preventExtensions calls to the target. In other scenarios, the opposite may hold true.
Combining the ideas, we could have something like:
fix: function(type){ if (type === Proxy.PREVENT_EXTENSION) // syntax example Object.preventExtension(this.target); // a switch case could be a good idea too // same code than you starting at "var props" }
Even if not forwarding the call, it could allow better granularity than Object.isFrozen.
The design point of whether fix() should be able to distinguish between preventExtensions, seal or freeze has come up before. IIRC, we didn't have a use case that would justify it. Perhaps your comment about allowing the forwarding handler to better query the state of its target object (via isFrozen,isSealed,isExtensible) is the first proper use case.
Now that I think about it, if forwarding Object.seal/freeze calls, the target and previously-proxy would act the same with very few exception. The non-configurability of all properties ensure that both keep the exact same property set will remain (non-deletable) with same [[enumerable]] for each (non-configurable). Your get/set method ensure that property access react the same in both objects. Besides Object.getOwnPropertyDescriptor and their identity, I can't think of any way to distinguish them.
Yep, that's the idea: fixed proxy and target are nearly indistinguishable. The big difference being, of course, that all property access on the proxy still traps the handler's get/set methods, which can continue to do arbitrarily different things than just forwarding the property access, e.g. logging, redirecting, ignoring, ...
I'd like to discuss forwarding proxy patterns. I'll call "full handler" a handler with all current traps and "own handler" a handler with just the own properties layer traps. The current forwarding proxy pattern is:
var p = Proxy.create(fullForwardingHandler(target));
Any interaction with p is delegated to target. To visualize the prototype chain, we have:
p --> null
target --> Object.getPrototypeOf(target) --> ... --> null
Notably, all prototype-climbing calls are delegated to the target prototype chain regardless of what p prototype is (I'll get back to that later)
We have seen a limitation of providing a full forwarding handler systematically (strawman:derived_traps_forwarding_handler). Regardless of what is decided on the strawman, some people may want to only provide own traps. One "lighter" way to provide the same forwarding handler would be to do: var p = Proxy.create(ownForwardingHandler(target), Object.getPrototypeOf(target)); Which would visualize as:
p -----------
target --> Object.getPrototypeOf(target) --> ... --> null
So at the own layer, p and target act exactly the same. They also act the same when it comes to inheritance but not thanks to prototype climbing traps. They do so because they natively delegate to the same prototype object and use the default trap behavior. So, they are also consistent with instanceof and Object.getPrototypeOf (and the prototype is correct when the proxy is fixed which is a consistent side-effect. This doesn't happen with current full forwarding and null prototype).
Ok, now that we are able to be in sync only at the own layer, one interesting pattern is the following
p --> Object.getPrototypeOf(p) --> ... --> null
target --> Object.getPrototypeOf(target) --> ... --> null
with p and target in sync at the own layer. For all own operations, p would forward to target, however, for all proto-climbing operations p would use its own properties. Actually, this works without any effort with the OwnForwardingHandler since its default derived traps delegate to the correct prototype by default. This pattern could be used for a new prototype inheritance pattern where o1 could inherit from p, o2 from target. They would both feel they inherit from the same object (since p and target are in sync) while, one step further inheriting from completely different objects.
All of this is just a discussion on the forwarding proxy pattern. But I think it was worth pointing the potential limitations of how this pattern is currently presented (limitations which are different from the one previously noticed which led to strawman:derived_traps_forwarding_handler).