Notification proxies (Was: possible excessive proxy invariants for Object.keys/etc??)
I think it could be argued that the two categories of proxy uses have different enough fundamentals that they would be better suited broken into two separate types of proxies. They use a common protocol for dispatching notifications/requests, but the result is incompatible.
For a Notification Proxy the trap is notified and then action proceeds against the ProxyTarget as it would normally. This is the simple case since it merely adds a pre-notification to the front of all trapped operations and nothing else changes.
For a Virtual Proxy the invariant-sensitive attributes of the virtual target act like those of a Notification Proxy. Any time something becomes invariant, whether it be a property or the whole object, the value is reified from being virtual to being an actual property. From that point on the property or the whole Proxy changes behavior from Virtual to Notification.
More in depth, here's a concept for separating Virtualization from Notification:
Assumes: using the same list of traps as Direct Proxies, a proxy has internal [[Target]] and [[Handler]] properties, proxy.[[IsExtensible]] and proxy.[[GetInheritance]] forward to the target.
TrapVirtual(proxy, trap, args): dispatches to GetTrap(proxy.[[ProxyHandler]], trap) and uses the return value to fulfill the original request TrapNotify(proxy, trap, args): dispatches to GetTrap(proxy.[[ProxyHandler]], trap) dis the result, and then forward the request to proxy.[[ProxyTarget]] returning that result IsPropertyFrozen(proxy, property): returns true if property is in proxy.[[ProxyTarget]].[[FrozenProperties]]. Otherwise false.
A Notifier Proxy uses TrapNotify for all traps and does not have any additional state.
A Virtual Object is a kind of dummy object that is used for Virtual Proxies. When created, it takes on the internal properties of the given target and additionally shallow clones all the properties. If the target is a proxy itself, then the internal methods it takes are from the [[ProxyTarget]] (recursively until hitting a real object). It does not otherwise keep a reference or any relation to the target. A Virtual Object has an additional internal property [[FrozenProperties]] which is a list of property names that are non-configurable. The [[DefineOwnProperty]] method of a Virtual Object invokes the [[DefineOwnProperty]] method taken from the target, and additionally adds the property to its [[FrozenProperties]] if a property becomes non-configurable.
A Virtual Proxy is a proxy that conditionally uses TrapNotify or TrapVirtual and who's target is a new Virtual Object created from the provided target. Whether TrapNotify or TrapVirtual is used is determined by the following set of rules:
When virtualproxy.[IsExtensible] is true:
- TrapVirtual is used for 'getOwnPropertyNames', 'enumerate', and 'keys'. The return is modified to ensure every name in virtualproxy.[[ProxyTarget]].[[FrozenProperties]]. is included.
- For 'deleteProperty', 'hasOwnProperty', and 'set', if IsPropertyFrozen(virtualproxy, property) is true TrapNotify is used. Otherwise TrapVirtual is used.
- For 'defineProperty', if the provided descriptor has 'configurable' set to false,or if IsPropertyFrozen(virtualproxy, property) is true then TrapNotify is used, otherwise TrapVirtual is used.
- For 'getOwnPropertyDescriptor', if IsPropertyFrozen(virtualproxy, property) is true then TrapNotify is used. Otherwise TrapVirtual is used and if the result has 'configurable' set to false, the property is defined on virtualproxy.[[ProxyTarget]].
- For 'get' if IsPropertyFrozen(virtualproxy, property) is true then TrapNotify is used. Otherwise TrapVirtual is used.
When virtualproxy.[IsExtensible] is false:
- TrapNotify is used for 'getOwnPropertyNames', 'enumerate', 'keys', 'deleteProperty', 'hasOwnProperty'
- For 'defineProperty' and 'set', if the property does not exist in virtualproxy.[[ProxyTarget]] or if IsPropertyFrozen(virtualproxy, property) is true then TrapNotify is used. Otherwise TrapVirtual is used.
- For 'getOwnPropertyDescriptor', if the property does not exist in virtualproxy.[[ProxyTarget]] or if IsPropertyFrozen(virtualproxy, property) then TrapNotify is used. Otherwise TrapVirtual is used and if the result has 'configurable' set to false, the property is defined on virtualproxy.[[ProxyTarget]].
- For 'get', if the property does not exist in virtualproxy.[[ProxyTarget]] or if IsPropertyFrozen(virtualproxy, property) then TrapNotify is used. Otherwise TrapVirtual is used.
If indeed both kinds of proxy are useful and direct proxies are more powerful, then why not only have a foundational direct proxy API and implement a tool type NotificationProxy that is based on that API.
[[[Sent from a mobile device. Please forgive brevity and typos.]]]
Dr. Axel Rauschmayer axel at rauschma.de Home: rauschma.de Blog: 2ality.com
On Nov 25, 2012, at 4:15 AM, Brandon Benvie wrote:
I think it could be argued that the two categories of proxy uses have different enough fundamentals that they would be better suited broken into two separate types of proxies. They use a common protocol for dispatching notifications/requests, but the result is incompatible.
For a Notification Proxy the trap is notified and then action proceeds against the ProxyTarget as it would normally. This is the simple case since it merely adds a pre-notification to the front of all trapped operations and nothing else changes.
For a Virtual Proxy the invariant-sensitive attributes of the virtual target act like those of a Notification Proxy. Any time something becomes invariant, whether it be a property or the whole object, the value is reified from being virtual to being an actual property. From that point on the property or the whole Proxy changes behavior from Virtual to Notification.
And using the new specification MOP this would be quite easy to specify. I even suspect that it would be fairly easy to implement, assuming that engines actually dynamically dispatch internal method calls at approximately the granularity of those specified by the MOP.
I actually had the thought as I was working through the MOP and the Proxy internal methods, that I'd wager that somebody is going to come up with other proxy like abstractions that are optimized for different scenarios and that they'd eventually get implemented.
On Nov 25, 2012, at 3:44 AM, Tom Van Cutsem wrote:
Hi,
I will refer to Dean's proposal as "notification proxies" (where traps essentially become notification callbacks), and will continue to use "direct proxies" for the current design where the trap can return a result (which is then verified).
I agree with Mark that Dean has a quite clever proposal...
...
However, some reservations:
me too...although I think notification proxies are optimized for a real use case and might be worth supporting.
if traps become mere notifications, perhaps their names should change to reflect this, e.g. "notifyGetOwnPropertyNames" instead of "getOwnPropertyNames". This is to alert handler writers that the return value of these traps will be ignored.
I think we do lose some expressiveness in the case of pure virtual object abstractions that don't pretend to uphold any invariants. With notification proxies, the handler must always (even in the case of configurable properties) define concrete properties on the target. Any virtual object abstraction is thus forced to maintain a "shadow" which must eventually be represented as a plain Javascript object. In other words: all virtual object abstractions, whether they pretend to be frozen or not, have to make use of the "shadow target" technique, with the burden of synchronizing the shadow upon each operation. That burden currently doesn't exist for non-frozen virtual object abstractions (I'm using "frozen" vs "non-frozen" here as a shorthand for "has non-configurable/non-extensible invariants" vs "has no such invariants").
I have a couple "virtual object" use cases in mind where I don't think I would want to make all properties concrete on the target.
- A bit vector abstraction where individual bits are accessible as numerically indexed properties.
Assume I have a bit string of fairly large size (as little as 128 bits) and I would like to abstract it as an array of single bit numbers where the indexes correspond to bit positions in the bit string. Using Proxies I want be able to use Get and Put traps to direct such indexed access to a binary data backing store I maintain. I believe that having to reify on the target each bit that is actually accessed would be too expensive in both time and space to justify using this approach.
BTW, this is a scenario where I might not even brother trying to make sure that Object.getOwnPropertyNames listed all of the bit indexes. I could, include them in an array of own property names, but would anybody really care if I didn't?
- Multiple Inheritance
I'm playing with what it takes to support self-like multiple inheritance using proxies. One approach that looks promising is to use a Proxy-based object as the immediate [[Prototype]] of leaf objects that have multiple logical inheritance parents. That lets put/gets of own property operate at native speed and the Put/Get handlers only get invoked for inherited properties. The MI parent proxy keeps track (using its own private state) of the multiple parents and doesn't really use it own [[Prototype]] ( actually it's target object's [[Prototype]]) as a lookup path for proto climbing. It encapsulates this entire mechanism such that from the perspective of the leaf object, all of its inherited properties look like own properties of the MI parent proxy. I would hate to have to "copy down" every accessed inherited property. There are situations were I might want to copy down some of them, but probably not all.
... I think the storage costs are largely the same. However, with notification proxies, if the properties were "virtual", those properties do linger as "concrete" properties on the target. Yes, the handler can delete them later, but when is later? Should the handler schedule clean-up actions using setTimeout(0)? This somehow does not feel right.
Yes indeed. Storage cost may be comparable for getOwnProperyNames that that is presumably a rare operation. If all traps had that characteristic I think many "virtual object" use cases would be too expensive to make the practical.
I like the simplicity of notification proxies, but we should think carefully what operations we turn into notifications only.
More generally, notification proxies are indeed "even-more-direct-proxies". They make the "wrapping" use case (logging, profiling, contract checking, etc.) simpler, at the expense of "virtual objects" (remote objects, test mock-ups), which are forced to always "concretize" the virtual object's properties on a real Javascript object.
Yes, I also like the simplicity of notification proxies but don't want to give up the power of virtual objects. Maybe having both would be a reasonable alternative.
On Nov 25, 2012, at 6:32 AM, Axel Rauschmayer wrote:
If indeed both kinds of proxy are useful and direct proxies are more powerful, then why not only have a foundational direct proxy API and implement a tool type NotificationProxy that is based on that API.
That might work. In that case you would probably do no invariant enforcement in the direct proxy internal methods and the notification proxy APIs would supply an intermediate layer of traps that called notification traps and then delegated to the target to get the actual results...
Le 25/11/2012 15:32, Axel Rauschmayer a écrit :
If indeed both kinds of proxy are useful and direct proxies are more powerful, then why not only have a foundational direct proxy API and implement a tool type NotificationProxy that is based on that API.
An interesting question I still haven't found a satisfying answer to is: is the additional power of current proxies useful? and worth the cost? Because the current freedom of proxies is the root cause of invariant checks that even good proxy citizens have to pay.
Le 25/11/2012 12:44, Tom Van Cutsem a écrit :
(...) I agree that the big benefit of notification proxies is that they get rid of all the complex validation logic.
However, some reservations:
(...)
- I think we do lose some expressiveness in the case of pure virtual object abstractions that don't pretend to uphold any invariants.
I don't think this is true. As Mark said: "The trick is that placing a configurable property on the target doesn't commit the handler to anything, since the handler can remove or change this property freely as of the next trap." This is true for any trap I think. I however agree that this 2-trap consistency may make the exercise of writing traps harder (but it'd take user testing to be 100% sure it's significantly harder).
I think there might be a loss in expressiveness if forwarding to the target throws an error. With current proxies, the trap can catch this exception; with notification proxies, the error is thrown after the trap, so it can't. Now, it would require an in-depth analysis, but I intuit that all cases where an internal operation throws have a corresponding invariant checks (which errors aren't caught by current proxies). If that was the case, it would mean that there is actually no loss (or a minor loss consisting in loosing the ability to catch an error and rethrow it yourself)
Also, if you can only forward to target, there is no way you can define custom property descriptor attributes which would be a shame in my opinion.
With notification proxies, the handler must always (even in the case of configurable properties) define concrete properties on the target. Any virtual object abstraction is thus forced to maintain a "shadow" which must eventually be represented as a plain Javascript object.
I think that any virtual object abstraction which claims to maintain some sort of consistency has to store the necessary bits of this consistency somewhere. The target is as good as anywhere else. I have to note that if the target is forced to be used as a regular object, then things like ProxyMap [1] become pointless (because the map is just used as a regular object, not as a map).
(...) However, with notification proxies, if the properties were "virtual", those properties do linger as "concrete" properties on the target. Yes, the handler can delete them later, but when is later? Should the handler schedule clean-up actions using setTimeout(0)? This somehow does not feel right.
According to Mark's comment, "later" is "next trap". It's possible to maintain consistency, but writing the traps is a bit less easy.
I like the simplicity of notification proxies, but we should think carefully what operations we turn into notifications only.
More generally, notification proxies are indeed "even-more-direct-proxies". They make the "wrapping" use case (logging, profiling, contract checking, etc.) simpler, at the expense of "virtual objects" (remote objects, test mock-ups), which are forced to always "concretize" the virtual object's properties on a real Javascript object.
I have what I think to be a interesting middleground allowing to bypass invariant checks while keeping direct proxies as they are. My experience with writing traps with direct proxies is that traps (for non-virtual cases) almost all and always look like [2]:
trap: function(...args){
// do something
return Reflect.trap(...args); // or equivalent built-in/syntax
}
Let's take the Object.getOwnPropertyDescriptor trap as example. With current proxies, the trap would often end with Reflect.getOwnPropertyDescriptor(target, name) (or "Object.", whatev's), after the call on the target is finished, then, the engine checks the result against the target... but that's a bit dumb. We know we wanted to get the target result and we made the most straightforward thing to do that guarantees invariants, but the engine still has to check, because it can't know the result comes from calling the Reflect operation on the target. We could define a symbolic value (like StopIteration for iterators) that would mean "forward to target". By essence of what forwarding to the target means, there would be no need to perform the least invariant check. We can call it ForwardToTarget :-) Then traps involved in proxies preserving some consistency would look like:
trap: function(...args){
// do something
return ForwardToTarget;
}
The rule would become: "if you want to do something simple, ForwardToTarget at the end of your trap and the invariant check cost is gone. If you want to use direct proxies freedom (virtual objects), you have to pay the cost of invariant checks to make sure you don't go too wild with your freedom"
Best of both worlds for the cost of a symbol.
As a followup to my above comment about custom property descriptor attributes, it would be nice to be able to return something like ForwardToTargetAndCombineWith({custom1: val1, custom2:val2}) for the getOwnPropertyDescriptor trap. More thoughts is necessary for this case.
David
[1] gist.github.com/3918227 [2] DavidBruant/HarmonyProxyLab/blob/EventedObjectsOnDirectProxies/EventedObject/EventedObject.js#L144
On Mon, Nov 26, 2012 at 3:36 AM, David Bruant <bruant.d at gmail.com> wrote:
Le 25/11/2012 15:32, Axel Rauschmayer a écrit :
If indeed both kinds of proxy are useful and direct proxies are more powerful, then why not only have a foundational direct proxy API and implement a tool type NotificationProxy that is based on that API.
An interesting question I still haven't found a satisfying answer to is: is the additional power of current proxies useful? and worth the cost? Because the current freedom of proxies is the root cause of invariant checks that even good proxy citizens have to pay.
At a minimum, the direct proxies allow remote objects and other entirely virtual objects to be implemented with no allocation overhead. In contrast, notification proxies require allocation proportional to the size of the remote object being proxied. In Racket, whose chaperone system has some of the flavor of notification proxies, this makes remote objects more costly and difficult to implement. This is less of a problem in Racket, since the primary use case for chaperones is the implementation of contracts, and virtual objects would usually be handled differently.
-- sam th samth at ccs.neu.edu
On Nov 26, 2012, at 12:36 AM, David Bruant wrote:
Le 25/11/2012 15:32, Axel Rauschmayer a écrit :
If indeed both kinds of proxy are useful and direct proxies are more powerful, then why not only have a foundational direct proxy API and implement a tool type NotificationProxy that is based on that API. An interesting question I still haven't found a satisfying answer to is: is the additional power of current proxies useful? and worth the cost? Because the current freedom of proxies is the root cause of invariant checks that even good proxy citizens have to pay.
One of the motivating use cases for Proxies is self-hosting exotic built-ins and host objects such as current Web API objects. If the standard built-in Proxy abstraction isn't expressive enough for that job (and also efficient enough) then we haven't achieve the goal of supporting that use case.
If that happens what I suspect will happen is that some implementations will provide a non-standard, less restrictive, more expressive alternative to the standard Proxy. That seems quite doable because it would just be another alternative exotic object MOP binding for the engine to support. I'd prefer that we provide a standard, interoperable abstraction rather than seeing non-interooperable solutions to this use case.
Le 26/11/2012 19:58, Allen Wirfs-Brock a écrit :
On Nov 26, 2012, at 12:36 AM, David Bruant wrote:
Le 25/11/2012 15:32, Axel Rauschmayer a écrit :
If indeed both kinds of proxy are useful and direct proxies are more powerful, then why not only have a foundational direct proxy API and implement a tool type NotificationProxy that is based on that API. An interesting question I still haven't found a satisfying answer to is: is the additional power of current proxies useful? and worth the cost? Because the current freedom of proxies is the root cause of invariant checks that even good proxy citizens have to pay.
One of the motivating use cases for Proxies is self-hosting exotic built-ins and host objects such as current Web API objects. If the standard built-in Proxy abstraction isn't expressive enough for that job (and also efficient enough) then we haven't achieve the goal of supporting that use case.
I agree, but I haven't read evidence on the thread that notification proxies would have insufficient power to do so. In my other answer, I pointed to a couple of expressiveness (minor) losses, but nothing that would prevent self-hosting Web APIs. Do you have particular examples in mind?
2012/11/25 Allen Wirfs-Brock <allen at wirfs-brock.com>
I have a couple "virtual object" use cases in mind where I don't think I would want to make all properties concrete on the target.
Thanks for spelling out these examples. While they still don't feel like actual important use cases to support, they give a good flavor of the kinds of compromises we'd need to make when turning to notification-only proxies.
- A bit vector abstraction where individual bits are accessible as numerically indexed properties.
Assume I have a bit string of fairly large size (as little as 128 bits) and I would like to abstract it as an array of single bit numbers where the indexes correspond to bit positions in the bit string. Using Proxies I want be able to use Get and Put traps to direct such indexed access to a binary data backing store I maintain. I believe that having to reify on the target each bit that is actually accessed would be too expensive in both time and space to justify using this approach.
Yes. As another example, consider a self-hosted sparse Array implementation.
The paradox here is that it's precisely those abstractions that seek to store/retrieve properties in a more compact/efficient way than allowed by the standard JS object model would turn to proxies, yet having to reify each accessed property precisely voids the more compact/efficient storage of properties.
BTW, this is a scenario where I might not even brother trying to make sure that Object.getOwnPropertyNames listed all of the bit indexes. I could, include them in an array of own property names, but would anybody really care if I didn't?
Well, yes and no.
Yes, in the sense that your object abstraction will break when used with some tools and libraries. For instance, consider a debugger that uses [[GetOwnPropertyNames]] to populate its inspector view, or a library that contains generic algorithms that operate on arbitrary objects (say copying an object, or serializing it, by using Object.getOwnPropertyNames).
No, in the sense that even if you would implement getOwnPropertyNames consistently, copying or serializing your bit vector abstraction would not lead to the desired result anyway (the copy or deserialized version would be a normal object without the optimized bit representation) (although the result might still be usable!)
More generally, notification proxies are indeed "even-more-direct-proxies". They make the "wrapping" use case (logging, profiling, contract checking, etc.) simpler, at the expense of "virtual objects" (remote objects, test mock-ups), which are forced to always "concretize" the virtual object's properties on a real Javascript object.
Yes, I also like the simplicity of notification proxies but don't want to give up the power of virtual objects. Maybe having both would be a reasonable alternative.
Brandon beat me to it, but indeed, having two kinds of proxies for the two different use cases makes sense. Except that there's a complexity budget we need to take into account. If we can avoid the cost of two APIs, we should.
Brandon's proposal tries to reduce the API bloat by keeping the exact same API for both direct proxies and notification proxies, and changing the rules dynamically based on the presence/absence of invariants. One issue I have with that is that it will make it very hard for people writing proxies to understand when the trap return value is ignored, and when it is not.
2012/11/26 David Bruant <bruant.d at gmail.com>
Le 25/11/2012 12:44, Tom Van Cutsem a écrit : I think there might be a loss in expressiveness if forwarding to the target throws an error. With current proxies, the trap can catch this exception; with notification proxies, the error is thrown after the trap, so it can't. Now, it would require an in-depth analysis, but I intuit that all cases where an internal operation throws have a corresponding invariant checks (which errors aren't caught by current proxies). If that was the case, it would mean that there is actually no loss (or a minor loss consisting in loosing the ability to catch an error and rethrow it yourself)
No, there is a loss. E.g. calling Reflect.get or Reflect.set on the target may throw arbitrary exceptions, not only exceptions related to broken invariants. With direct proxies, it's easy to write a "silencer" proxy that eats all exceptions. Can't do that with notification proxies. (Not saying such a silencer is a useful abstraction, but we're talking absolute expressiveness here, so it counts.)
More generally, notification proxies simplify "before"-style wrapping (e.g. revocable references become a breeze to implement), but they complicate "after" and "around" style wrapping. For instance, how would one implement a logging proxy that wants to log the result of forwarded operations? (forwarding the operation twice is cheating)
Also, if you can only forward to target, there is no way you can define custom property descriptor attributes which would be a shame in my opinion.
Right, I hadn't noticed that. Good point.
We could define a symbolic value (like StopIteration for iterators) that
would mean "forward to target". By essence of what forwarding to the target means, there would be no need to perform the least invariant check. We can call it ForwardToTarget :-)
I think we've previously entertained a similar proposal when a handler was encountering the .public property of a private property it didn't know, and then wanted to signal to the proxy "I don't know this private name, please forward". I recall one issue was that you'd really want a unique token per trap invocation, which costs.
There's a few goals I have in mind when thinking about proxies:
- Ability to forward to target at minimal cost.
- Minimal cost for invariant enforcement, preferably automatic since the result is predetermined
- Near zero cost for invariant enforcement in the majority of cases where it's not needed
- Ability to virtualize properties with minimal forced overhead (if someone eagerly deep clones a membrane then that's their cost, not the API's)
When something becomes invariant, then the runtime essentially takes ownership of it and it no longer belongs to the proxy. The proxy's only remaining power is being the first to know whenever something is about to happen. As the proxy creator, I don't really want to spend resources being responsible for something I no longer have any control over.
I started to respond to Allan's message, but I'll combine them to here. Note the additional proposal in the middle of the message.
On Mon, Nov 26, 2012 at 11:33 AM, Tom Van Cutsem <tomvc.be at gmail.com> wrote:
2012/11/25 Allen Wirfs-Brock <allen at wirfs-brock.com>
I have a couple "virtual object" use cases in mind where I don't think I would want to make all properties concrete on the target.
Thanks for spelling out these examples. While they still don't feel like actual important use cases to support, they give a good flavor of the kinds of compromises we'd need to make when turning to notification-only proxies.
I agree. My usual expectation for proxies is to support remote and persistent objects. While supporting other scenarios is great, usually that's incidental. Is there a broader list of aspirations for proxies? or is this just a "all else being equal it would be good if we can do this"?
- A bit vector abstraction where individual bits are accessible as numerically indexed properties.
Assume I have a bit string of fairly large size (as little as 128 bits) and I would like to abstract it as an array of single bit numbers where the indexes correspond to bit positions in the bit string. Using Proxies I want be able to use Get and Put traps to direct such indexed access to a binary data backing store I maintain. I believe that having to reify on the target each bit that is actually accessed would be too expensive in both time and space to justify using this approach.
Yes. As another example, consider a self-hosted sparse Array implementation.
The paradox here is that it's precisely those abstractions that seek to store/retrieve properties in a more compact/efficient way than allowed by the standard JS object model would turn to proxies, yet having to reify each accessed property precisely voids the more compact/efficient storage of properties.
I don't have a good sense of how often and for what purpose clients call getOwnPropertyNames and the like. That frankly seems like a terrible operation for any client to be calling; it's architecturally necessarily inefficient; especially since it currently demands a fresh array. Worst case, I'd like to see it have a frozen result or be deprecated in favor of an operation that is more architecturally efficient (e.g., return an iterator of names so they need never all be reified). If the operation is typically only called for debugging and inspection, or once per type or some such, then the performance questions are less important. If libraries constantly call it for web services, then having an improved API might be a big win.
BTW, this is a scenario where I might not even brother trying to make sure
that Object.getOwnPropertyNames listed all of the bit indexes. I could, include them in an array of own property names, but would anybody really care if I didn't?
So for this example, you might want to suppress the integer properties from getOwnPropertyNames *regardless *of the proxy approach. Otherwise you are indeed doing O(N) work for all your otherwise efficiently-implemented bit fields. Such a hack would work poorly with meta-driven tools (e.g., something that maps fields to a display table for object inspection), but that's not because of the proxy support.
(It is conceivable to me that integer-indexed fields deserve explicit support in a meta-protocol anyway, since their usage patterns are typically so different from that of named fields.)
Well, yes and no.
Yes, in the sense that your object abstraction will break when used with some tools and libraries. For instance, consider a debugger that uses [[GetOwnPropertyNames]] to populate its inspector view, or a library that contains generic algorithms that operate on arbitrary objects (say copying an object, or serializing it, by using Object.getOwnPropertyNames).
No, in the sense that even if you would implement getOwnPropertyNames consistently, copying or serializing your bit vector abstraction would not lead to the desired result anyway (the copy or deserialized version would be a normal object without the optimized bit representation) (although the result might still be usable!)
More generally, notification proxies are indeed "even-more-direct-proxies". They make the "wrapping" use case (logging, profiling, contract checking, etc.) simpler, at the expense of "virtual objects" (remote objects, test mock-ups), which are forced to always "concretize" the virtual object's properties on a real Javascript object.
Yes, I also like the simplicity of notification proxies but don't want to give up the power of virtual objects. Maybe having both would be a reasonable alternative.
Brandon beat me to it, but indeed, having two kinds of proxies for the two different use cases makes sense. Except that there's a complexity budget we need to take into account. If we can avoid the cost of two APIs, we should.
I too would like to avoid two kinds of proxies. And if there are, building the simpler one out of the more expressive must be done carefully to avoid giving away performance and other benefits of the simpler approach.
Brandon's proposal tries to reduce the API bloat by keeping the exact same API for both direct proxies and notification proxies, and changing the rules dynamically based on the presence/absence of invariants. One issue I have with that is that it will make it very hard for people writing proxies to understand when the trap return value is ignored, and when it is not.
I would rather avoid that. You still have the same more-complicated semantics, but with new, hidden dangers :). I had a variant, however, that might address both concerns:
The trapGetOwnPropertyNames can return a list of additional properties to add to the ones on the target. These are then validated and added to the collection returned by the primitive operation on the target. In the "notify" case, the collection is empty, and that should be typical. In the more complicated case, the collection is iterated (and so could be any kind of collection) and the elements are added (with dup detection) by the proxy to the set of properties it got from the underlying target. Since there is dup detection, you can't change a property that is already on the object, but you can add "virtual" properties with appropriate restrictions. Thus, the trap is a "delta" operation.
- Multiple Inheritance
I'm playing with what it takes to support self-like multiple inheritance using proxies. One approach that looks promising is to use a Proxy-based object as the immediate [[Prototype]] of leaf objects that have multiple logical inheritance parents. That lets put/gets of own property operate at native speed and the Put/Get handlers only get invoked for inherited properties. The MI parent proxy keeps track (using its own private state) of the multiple parents and doesn't really use it own [[Prototype]] ( actually it's target object's [[Prototype]]) as a lookup path for proto climbing. It encapsulates this entire mechanism such that from the perspective of the leaf object, all of its inherited properties look like own properties of the MI parent proxy. I would hate to have to "copy down" every accessed inherited property. There are situations were I might want to copy down some of them, but probably not all.
In any case, even in the simplest approach, the target object doesn't need to actually duplicate the contents of the fields, merely their definitions (i.e., they can point to null). It would have to iterate the parents to find out their property names anyway to make a combined list, so again the work seems the same order of magnitude. That is usually a useful goal since it then means that optimization makes it better, but is not essential for making it usable.
Le 26/11/2012 20:59, Tom Van Cutsem a écrit :
2012/11/26 David Bruant <bruant.d at gmail.com <mailto:bruant.d at gmail.com>>
We could define a symbolic value (like StopIteration for iterators) that would mean "forward to target". By essence of what forwarding to the target means, there would be no need to perform the least invariant check. We can call it ForwardToTarget :-)
I think we've previously entertained a similar proposal when a handler was encountering the .public property of a private property it didn't know, and then wanted to signal to the proxy "I don't know this private name, please forward".
True. I had the feeling the idea wasn't entirely knew, but I couldn't recall what was the inspiration for it.
I recall one issue was that you'd really want a unique token per trap invocation, which costs.
I don't understand why a unique token per trap invocation would be necessary.
By the way, very much like for iterators, it would have to be "throw ForwardToTarget" instead of "return ForwardToTarget" because the symbol could be a value people would want to return while it would be a bad practice to expect a specific value to be thrown in a normal execution.
2012/11/26 Dean Tribble <dtribble at gmail.com>
On Mon, Nov 26, 2012 at 11:33 AM, Tom Van Cutsem <tomvc.be at gmail.com>wrote:
Thanks for spelling out these examples. While they still don't feel like actual important use cases to support, they give a good flavor of the kinds of compromises we'd need to make when turning to notification-only proxies.
I agree. My usual expectation for proxies is to support remote and persistent objects. While supporting other scenarios is great, usually that's incidental. Is there a broader list of aspirations for proxies? or is this just a "all else being equal it would be good if we can do this"?
Let's talk about aspirations for proxies. It will help us set priorities.
First, some terminology (originating from CLOS, the "mother of all MOPs" ;-) CLOS method combinations allow a composer to distinguish between "before", "after" and "around"-style composition:
- "before"-style wrapping gives you only the ability to get notified before an operation happens. You can abort, but not change, the result of the operation. This is what notification-proxies offer.
- "after"-style wrapping allows you to get notified of an operation after-the-fact. Depending on the API, the "after"-wrapper may or may not get to see the outcome of the operation, and may or may not change the final outcome passed on to clients.
- "around"-style wrapping is the most general and allows the composer to decide if and when to forward, and what result to return. It subsumes before/after wrapping. This is what direct proxies currently provide.
As far as I can tell, virtual object abstractions like remote/persistent objects require "around"-style wrapping, because there's otherwise no meaningful target to automatically forward to.
Here's a list of use cases that I frequently have in mind when thinking about proxies, categorized according to whether the use case requires before/after/around wrapping:
Virtual objects, hence "around"-style:
- self-hosting "exotic" objects such as Date, Array (i.e. self-host an ES5/ES6 environment)
- self-hosting DOM/WebIDL objects such as NodeList
Around-style wrapping (need to be able to change the result of an operation):
- membranes
- higher-order contracts
Before-style wrapping:
- revocable references
What else?
Object.observe would fit under "after" correct?
Le 26/11/2012 21:39, David Bruant a écrit :
Le 26/11/2012 20:59, Tom Van Cutsem a écrit :
2012/11/26 David Bruant <bruant.d at gmail.com <mailto:bruant.d at gmail.com>>
We could define a symbolic value (like StopIteration for iterators) that would mean "forward to target". By essence of what forwarding to the target means, there would be no need to perform the least invariant check. We can call it ForwardToTarget :-)
I think we've previously entertained a similar proposal when a handler was encountering the .public property of a private property it didn't know, and then wanted to signal to the proxy "I don't know this private name, please forward". True. I had the feeling the idea wasn't entirely knew, but I couldn't recall what was the inspiration for it.
I recall one issue was that you'd really want a unique token per trap invocation, which costs. I don't understand why a unique token per trap invocation would be necessary.
I still don't understand this point. I've gone spelunking. I've found:
- the wiki page [1] (reflecting July meeting) which mentions that returning undefined would express "I don't know the private name, please forward"
- Confirmed on July 31st [2].
- Introduction of the idea of putting the verification of known names somewhere else than for each trap return [3]. Some discussion in between about this idea. Introduction of the idea of adding a third argument [4] after which I think stops all discussions about returning something in traps to prove knowledge of a private name or forwarding when not knowing.
I don't remember the point about a token per trap invocation and I haven't been able to find it (but I haven't read everything).
In any case, for "throw ForwardToTarget", I don't see why it would be necessary. It seems it would work unambiguously with meta-handlers, with target-as-a-proxy or with manipulate-any-other-proxy-inside-a-trap (which target-as-a-proxy is an instance of).
David
[1] harmony:direct_proxies#discussed_during_tc39_july_2012_meeting_microsoft_redmond [2] esdiscuss/2012-July/024246 [3] esdiscuss/2012-July/024256 [4] esdiscuss/2012-August/024313
On Wed, Nov 28, 2012 at 1:09 PM, Tom Van Cutsem <tomvc.be at gmail.com> wrote:
2012/11/26 Dean Tribble <dtribble at gmail.com>
... I agree. My usual expectation for proxies is to support remote and persistent objects. While supporting other scenarios is great, usually that's incidental. Is there a broader list of aspirations for proxies? or is this just a "all else being equal it would be good if we can do this"?
Let's talk about aspirations for proxies. It will help us set priorities. First, some terminology (originating from CLOS, the "mother of all MOPs" ;-)
As a general point, I encourage you to look for other inspiration than CLOS MOP for doign proxies (whose mother was really InterlispD). Meta-level access deeply impacts security,maintainability, reliability, understandability, etc. The tighter and more structured you can make your meta-level access, the easier it will be to to implement, use, and maintain (e.g., both coroutines and downward functions are more understandable, easier to implement, easier to secure, etc. than general continuations and call-cc).
CLOS method combinations allow a composer to distinguish between "before", "after" and "around"-style composition:
- "before"-style wrapping gives you only the ability to get notified before an operation happens. You can abort, but not change, the result of the operation. This is what notification-proxies offer.
You can change the result of the operation. You do so by modifying the state before the operation proceeds, of course. You could also extend the notification support to notify after so you could clenup (avoiding a callback hack).
- "after"-style wrapping allows you to get notified of an operation after-the-fact. Depending on the API, the "after"-wrapper may or may not get to see the outcome of the operation, and may or may not change the final outcome passed on to clients.
- "around"-style wrapping is the most general and allows the composer to decide if and when to forward, and what result to return. It subsumes before/after wrapping. This is what direct proxies currently provide.
It does not subsume before/after wrapping, because it loses the integrity of before/after (e.g., the wrapper can lie and cheat, where the before and after cannot). That may be worth it, but it is substantially different.
Another variant is the "differential" version: the "differential" trap is like a notification, but it can also return virtual additions (or an iterator of additions). The proxy then invokes the primitive on the target, and appends (with de-dupping, etc.) the virtual additions. This allows the simple case to just use hte target, but also allows all of Allen's additional cases.
As far as I can tell, virtual object abstractions like remote/persistent objects require "around"-style wrapping, because there's otherwise no meaningful target to automatically forward to.
I thought the target in that case is an internal object to represent or reify the meta-state of the remote or persistent object. I think that still makes sense in both the persistent object and remote object cases.
Here's a list of use cases that I frequently have in mind when thinking about proxies, categorized according to whether the use case requires before/after/around wrapping:
Virtual objects, hence "around"-style:
- self-hosting "exotic" objects such as Date, Array (i.e. self-host an ES5/ES6 environment)
- self-hosting DOM/WebIDL objects such as NodeList
I should note that I'm not advocating a notification-only style for all your proxy needs; having get operations able to generate virtual results makes lots of sense. I primary suggest it for operations that are currently implemented by the system (i.e., user code cannot normally intervene) and that might be relied on for security-relevant behavior. wrapping return results of user operations in a proxy makes perfect sense to me.
Around-style wrapping (need to be able to change the result of an operation):
- membranes
- higher-order contracts
Before-style wrapping:
- revocable references
You can validate arguments, the state of the destination object (e.g., if you were implementing a state machine), logging
What else?
There is the pattern derived from the meter pattern in KeyKOS: the handler is only invoked on exception (e.g., like a page fault). For example, a primitive stream gets read operations against. Normally they proceed as a primitive against an implementation-provided buffer so that "next" is really darned fast. When the buffer is exhausted, instead of throwing an error to the caller, the error is thrown to the handler (called a keeper) which goes through some user-defined effort to refill the buffer, then the read is retried. This allows most data transfer to such a stream to use fast, batch-oriented primitives, while supporting an arbitrary source of contents.
On Nov 28, 2012, at 1:09 PM, Tom Van Cutsem wrote:
2012/11/26 Dean Tribble <dtribble at gmail.com> On Mon, Nov 26, 2012 at 11:33 AM, Tom Van Cutsem <tomvc.be at gmail.com> wrote: Thanks for spelling out these examples. While they still don't feel like actual important use cases to support, they give a good flavor of the kinds of compromises we'd need to make when turning to notification-only proxies.
I agree. My usual expectation for proxies is to support remote and persistent objects. While supporting other scenarios is great, usually that's incidental. Is there a broader list of aspirations for proxies? or is this just a "all else being equal it would be good if we can do this"?
Let's talk about aspirations for proxies. It will help us set priorities.
First, some terminology (originating from CLOS, the "mother of all MOPs" ;-) CLOS method combinations allow a composer to distinguish between "before", "after" and "around"-style composition:
- "before"-style wrapping gives you only the ability to get notified before an operation happens. You can abort, but not change, the result of the operation. This is what notification-proxies offer.
- "after"-style wrapping allows you to get notified of an operation after-the-fact. Depending on the API, the "after"-wrapper may or may not get to see the outcome of the operation, and may or may not change the final outcome passed on to clients.
- "around"-style wrapping is the most general and allows the composer to decide if and when to forward, and what result to return. It subsumes before/after wrapping. This is what direct proxies currently provide.
As far as I can tell, virtual object abstractions like remote/persistent objects require "around"-style wrapping, because there's otherwise no meaningful target to automatically forward to.
Here's a list of use cases that I frequently have in mind when thinking about proxies, categorized according to whether the use case requires before/after/around wrapping:
Virtual objects, hence "around"-style:
- self-hosting "exotic" objects such as Date, Array (i.e. self-host an ES5/ES6 environment)
- self-hosting DOM/WebIDL objects such as NodeList -virtualizing a backing store as properties. -supported extended property attributes
Around-style wrapping (need to be able to change the result of an operation):
- membranes
- higher-order contracts
introducing new inheritance schemes, eg, multiple inheritance
Before-style wrapping:
- revocable references
After-style
a "doesNotUnderstand" wrapper -- run the operation, but if the result is undefined check if missing property and if so, call DNU handler
2012/11/28 Brandon Benvie <brandon at brandonbenvie.com>
Object.observe would fit under "after" correct?
Yes, with two remarks:
- it only traps updating operations
- the traps are called in a separate turn (i.e. asynchronously), so they cannot change the result of the operation
2012/11/28 David Bruant <bruant.d at gmail.com>
I don't understand why a unique token per trap invocation would be necessary.
I still don't understand this point. I've gone spelunking. I've found:
- the wiki page [1] (reflecting July meeting) which mentions that returning undefined would express "I don't know the private name, please forward"
- Confirmed on July 31st [2].
- Introduction of the idea of putting the verification of known names somewhere else than for each trap return [3]. Some discussion in between about this idea. Introduction of the idea of adding a third argument [4] after which I think stops all discussions about returning something in traps to prove knowledge of a private name or forwarding when not knowing.
I don't remember the point about a token per trap invocation and I haven't been able to find it (but I haven't read everything).
In any case, for "throw ForwardToTarget", I don't see why it would be necessary. It seems it would work unambiguously with meta-handlers, with target-as-a-proxy or with manipulate-any-other-proxy-inside-a-trap (which target-as-a-proxy is an instance of).
I think throwing a special token, as is done with StopIteration would probably work in practice (little risk of confusing it with a legitimately returned value). However, it does require every trap invocation to be wrapped in a try-catch block to potentially catch that error. Maybe I'm too influenced by the JVM, but my understanding is that wrapping every call to a trap with a try-catch block won't be free. Perhaps implementors can comment.
2012/11/29 Dean Tribble <dtribble at gmail.com>
As a general point, I encourage you to look for other inspiration than CLOS MOP for doign proxies (whose mother was really InterlispD). Meta-level access deeply impacts security,maintainability, reliability, understandability, etc. The tighter and more structured you can make your meta-level access, the easier it will be to to implement, use, and maintain (e.g., both coroutines and downward functions are more understandable, easier to implement, easier to secure, etc. than general continuations and call-cc).
I agree (side note: the CLOS MOP didn't form a direct inspiration for JS proxies, although I'm probably influenced by having studied it).
CLOS method combinations allow a composer to distinguish between "before", "after" and "around"-style composition:
- "before"-style wrapping gives you only the ability to get notified before an operation happens. You can abort, but not change, the result of the operation. This is what notification-proxies offer.
You can change the result of the operation. You do so by modifying the state before the operation proceeds, of course. You could also extend the notification support to notify after so you could clenup (avoiding a callback hack).
Indeed. I think notification proxies would benefit from both before + after notification so any cleanup of virtual properties can be done directly.
That just leaves the thorny issue that for virtual object abstractions, having to "set-up" the target in a before-handler and "clean-up" the target in an after-handler is really a very indirect way of expressing the abstraction, especially if the proxy doesn't need to virtualize any invariants.
- "after"-style wrapping allows you to get notified of an operation after-the-fact. Depending on the API, the "after"-wrapper may or may not get to see the outcome of the operation, and may or may not change the final outcome passed on to clients.
- "around"-style wrapping is the most general and allows the composer to decide if and when to forward, and what result to return. It subsumes before/after wrapping. This is what direct proxies currently provide.
It does not subsume before/after wrapping, because it loses the integrity of before/after (e.g., the wrapper can lie and cheat, where the before and after cannot). That may be worth it, but it is substantially different.
You're right, around doesn't subsume before+after in that regard. Thanks for clarifying.
Another variant is the "differential" version: the "differential" trap is like a notification, but it can also return virtual additions (or an iterator of additions). The proxy then invokes the primitive on the target, and appends (with de-dupping, etc.) the virtual additions. This allows the simple case to just use hte target, but also allows all of Allen's additional cases.
As far as I can tell, virtual object abstractions like remote/persistent
objects require "around"-style wrapping, because there's otherwise no meaningful target to automatically forward to.
I thought the target in that case is an internal object to represent or reify the meta-state of the remote or persistent object. I think that still makes sense in both the persistent object and remote object cases.
It does. It just feels awkward, after having been able to express these abstractions more directly with the current Proxy API for so long.
Here's a list of use cases that I frequently have in mind when thinking about proxies, categorized according to whether the use case requires before/after/around wrapping:
Virtual objects, hence "around"-style:
- self-hosting "exotic" objects such as Date, Array (i.e. self-host an ES5/ES6 environment)
- self-hosting DOM/WebIDL objects such as NodeList
I should note that I'm not advocating a notification-only style for all your proxy needs; having get operations able to generate virtual results makes lots of sense. I primary suggest it for operations that are currently implemented by the system (i.e., user code cannot normally intervene) and that might be relied on for security-relevant behavior. wrapping return results of user operations in a proxy makes perfect sense to me.
It's hard to force the two different use cases (wrapping vs virtual objects) into a single API. I don't have a good answer yet on how to resolve the trade-offs.
Le 02/12/2012 17:27, Tom Van Cutsem a écrit :
2012/11/28 David Bruant <bruant.d at gmail.com <mailto:bruant.d at gmail.com>>
I don't understand why a unique token per trap invocation would be necessary.
I still don't understand this point. I've gone spelunking. I've found: * the wiki page [1] (reflecting July meeting) which mentions that returning undefined would express "I don't know the private name, please forward" * Confirmed on July 31st [2]. * Introduction of the idea of putting the verification of known names somewhere else than for each trap return [3]. Some discussion in between about this idea. Introduction of the idea of adding a third argument [4] after which I think stops all discussions about returning something in traps to prove knowledge of a private name or forwarding when not knowing. I don't remember the point about a token per trap invocation and I haven't been able to find it (but I haven't read everything). In any case, for "throw ForwardToTarget", I don't see why it would be necessary. It seems it would work unambiguously with meta-handlers, with target-as-a-proxy or with manipulate-any-other-proxy-inside-a-trap (which target-as-a-proxy is an instance of).
I think throwing a special token, as is done with StopIteration would probably work in practice (little risk of confusing it with a legitimately returned value). However, it does require every trap invocation to be wrapped in a try-catch block to potentially catch that error.
Yes and no. The "try-catch" is inside the engine, very much like for StorIteration in for-of loops. In case current implementations had performance drawbacks, I feel they could special-case when they know they are calling a function in the context of a particular protocol (iterator or proxy trap for instance).
Maybe I'm too influenced by the JVM, but my understanding is that wrapping every call to a trap with a try-catch block won't be free.
The more interesting question is whether it would be significantly cheaper than 'returning a value+invariant checks' because that's the reason I suggested the addition of "throw ForwardToTarget".
Perhaps implementors can comment.
Yep.
Le 02/12/2012 17:43, David Bruant a écrit :
Maybe I'm too influenced by the JVM, but my understanding is that wrapping every call to a trap with a try-catch block won't be free. The more interesting question is whether it would be significantly cheaper than 'returning a value+invariant checks' because that's the reason I suggested the addition of "throw ForwardToTarget".
On Sun, Dec 2, 2012 at 8:40 AM, Tom Van Cutsem <tomvc.be at gmail.com> wrote:
- "after"-style wrapping allows you to get notified of an operation after-the-fact. Depending on the API, the "after"-wrapper may or may not get to see the outcome of the operation, and may or may not change the final outcome passed on to clients.
- "around"-style wrapping is the most general and allows the composer to decide if and when to forward, and what result to return. It subsumes before/after wrapping. This is what direct proxies currently provide.
It does not subsume before/after wrapping, because it loses the integrity of before/after (e.g., the wrapper can lie and cheat, where the before and after cannot). That may be worth it, but it is substantially different.
You're right, around doesn't subsume before+after in that regard. Thanks for clarifying.
I think we can rescue around wrapping. I'll call this approach "opaque around wrapping". The idea is that the proxy passes into the handler trap a proxy-generated no-argument function (a thunk) called "action". The normal case is that the trap does stuff before calling action, calls action with no arguments and ignoring action's results, does stuff after action, and returns nothing. The call to action performs the original operation on target and remembers the result. After the trap returns, the proxy returns the remembered result of action.
Open questions that take us to different design possibilities:
- what happens if the trap does not call action?
- what happens if the trap calls action more than once?
- do actions ever return anything, in case the trap wants to pay attention to it?
- are there any return values from the trap that the proxy would pay attention to?
- if the original operation performed by action throws, should the action still just return to the trap normally?
- what happens if the trap throws rather that returning?
Two attractive possibilities:
A) A simple pure notification approach:
- the proxy throws, indicating that the action was not performed
- only the last successful action counts
- a boolean, indicating whether the original operation succeeded on target
- no
- yes, but with false rather than true, as in #A.3 above.
- the proxy throws, even though the action was performed.
B) Notification with fallback to virtual properties with invariant checking: Like #A, except
- if action is never called, then the trap return's invariants are checked, as now.
- only if action was never called.
I prefer #A to #B as it gains the full benefits of simplifying the overall spec and implementation. However, #B still seems better than current direct proxies, as the normal case gets target-maintenance and invariant checking for free.
2012/12/2 Mark S. Miller <erights at google.com>
I think we can rescue around wrapping. I'll call this approach "opaque around wrapping". The idea is that the proxy passes into the handler trap a proxy-generated no-argument function (a thunk) called "action".
Interesting. I had thought of a similar approach, but hadn't pursued it because I thought allocating a thunk per trap invocation would be deemed too expensive. I'll go ahead and sketch the design, which I think corresponds closely with your approach B)
First, from the point-of-view of the trap implementor, things would work as follows (I show the "defineProperty" trap only as an example)
All traps take an extra thunk as last argument, which I will name "forward", since calling that thunk has the effect of forwarding the operation to the target, returning the original result.
defineProperty: function(target, name, desc, forward) { // before var result = forward(); // after return result; }
Now, from the point-of-view of the proxy implementation, how is the above trap called?
// in the proxy, intercepting Object.defineProperty(proxy, name, desc) // where proxy = Proxy(target, handler) var result = Uninitialized; var thunk = function() { if (result === Disabled) throw new Error("can only call forward() during trap invocation"); if (result !== Uninitialized) throw new Error("forward() can be called only once"); result = Reflect.defineProperty(target, name, desc); // forward to target return result; }; var trapResult = handler["defineProperty"].call(handler, target, name, desc, thunk); if (result !== Uninitialized && result === trapResult) return result; // no invariant checks when original result is returned if (result === Uninitialized || result !== trapResult) { /* do invariant checks */ ; return result; } result = Disabled; // ensures one cannot call forward() outside of dynamic extent of the invocation
I'll answer your questions for the above design:
Open questions that take us to different design possibilities:
- what happens if the trap does not call action?
Invariant checks are performed on the returned result.
- what happens if the trap calls action more than once?
An exception is thrown.
- do actions ever return anything, in case the trap wants to pay attention to it?
Yes, it returns the value of the operation, applied to the target.
- are there any return values from the trap that the proxy would pay attention to?
Yes. If the trap returns the original result, it doesn't do any extra checks. Otherwise it does.
- if the original operation performed by action throws, should the action still just return to the trap normally?
A thrown exception would escape and abort the entire operation, unless the trap wraps the call to forward() in a try-catch block.
- what happens if the trap throws rather that returning?
Thrown exceptions are always propagated to clients.
I prefer #A to #B as it gains the full benefits of simplifying the overall spec and implementation. However, #B still seems better than current direct proxies, as the normal case gets target-maintenance and invariant checking for free.
I agree that #B doesn't really simplify anything in the spec (the invariant checks are still there sometimes). I'm not sure if it is inherently better than the current design though: we gain performance in terms of avoiding invariant checks, but we lose performance by having to allocate a per-call thunk, + the API becomes more complex (an additional parameter to every trap)
I do think that passing an action or "forward()" thunk to a trap as extra argument beats David's proposed "throw ForwardToTarget" trick in terms of elegance and usability (maybe not in terms of performance).
On Dec 2, 2012, at 11:40 AM, Tom Van Cutsem wrote:
2012/12/2 Mark S. Miller <erights at google.com> I think we can rescue around wrapping. I'll call this approach "opaque around wrapping". The idea is that the proxy passes into the handler trap a proxy-generated no-argument function (a thunk) called "action".
Interesting. I had thought of a similar approach, but hadn't pursued it because I thought allocating a thunk per trap invocation would be deemed too expensive. I'll go ahead and sketch the design, which I think corresponds closely with your approach B)
This is my first reaction too. I want Proxies to be lightweight enough so they really can be used be used for things like self hosting built-ins and web API objects. The need to create an pass in a action thunk on each trap calls initially feels too expensive.
But that is only a first reaction. Perhaps the expense really isn't that great. Or perhaps a lot of the cases where I have assumed the Proxies would be needed could be handled by lightweight mechanisms such such as the @elementGet/@elementSet hooks described in strawman:object_model_reformation (see Implementing Built-inArray without Proxy and Rationalizing DOM HTMLCollections examples)
Le 02/12/2012 18:34, Mark S. Miller a écrit :
On Sun, Dec 2, 2012 at 8:40 AM, Tom Van Cutsem <tomvc.be at gmail.com> wrote:
- "after"-style wrapping allows you to get notified of an operation after-the-fact. Depending on the API, the "after"-wrapper may or may not get to see the outcome of the operation, and may or may not change the final outcome passed on to clients.
- "around"-style wrapping is the most general and allows the composer to decide if and when to forward, and what result to return. It subsumes before/after wrapping. This is what direct proxies currently provide.
It does not subsume before/after wrapping, because it loses the integrity of before/after (e.g., the wrapper can lie and cheat, where the before and after cannot). That may be worth it, but it is substantially different.
You're right, around doesn't subsume before+after in that regard. Thanks for clarifying. I think we can rescue around wrapping. I'll call this approach "opaque around wrapping". The idea is that the proxy passes into the handler trap a proxy-generated no-argument function (a thunk) called "action". The normal case is that the trap does stuff before calling action, calls action with no arguments and ignoring action's results, does stuff after action, and returns nothing.
The point of "around" is to do something with the result in the middle of the trap, isn't it? For question 3, I'd answer that the "action" should return the result.
The call to action performs the original operation on target and remembers the result. After the trap returns, the proxy returns the remembered result of action. target = {a:1}; var p = new Proxy(target, { get: function(target, name, action){ var v = action(); target[name] = 2; } })
p.a; // ?
If p.a is 1 because the call to "action" remembered the "1", then the target and the proxy look awkwardly desynchronized. To know what's being returned from the trap, one needs to remember when the action is being called which makes writing traps harder I feel. With the current design, looking at the return statements is enough. If p.a is 2, I don't understand the point of "action". Or at least, I don't see how it's better than just calling Reflect[trap].
- what happens if the trap does not call action?
If calling action was made compulsory, then, it would have the same downsides than the notification proxies when it comes like virtual objects including the necessity of a physical backing.
Le 02/12/2012 20:40, Tom Van Cutsem a écrit :
2012/12/2 Mark S. Miller <erights at google.com <mailto:erights at google.com>>
I think we can rescue around wrapping. I'll call this approach "opaque around wrapping". The idea is that the proxy passes into the handler trap a proxy-generated no-argument function (a thunk) called "action".
Interesting. I had thought of a similar approach, but hadn't pursued it because I thought allocating a thunk per trap invocation would be deemed too expensive. I'll go ahead and sketch the design, which I think corresponds closely with your approach B)
I think that for most traps, "action" could be a deeply frozen wrapped version of Reflect.trap. It would reduce "action" functions to one per trap which would be much better than one per trap invocation.
4) are there any return values from the trap that the proxy would pay attention to?
Yes. If the trap returns the original result, it doesn't do any extra checks. Otherwise it does.
Said a bit differently, the extra check is reduced to the equality between the return value of "action" and trap return value.
I prefer #A to #B as it gains the full benefits of simplifying the overall spec and implementation. However, #B still seems better than current direct proxies, as the normal case gets target-maintenance and invariant checking for free.
I agree that #B doesn't really simplify anything in the spec (the invariant checks are still there sometimes). I'm not sure if it is inherently better than the current design though: we gain performance in terms of avoiding invariant checks, but we lose performance by having to allocate a per-call thunk, + the API becomes more complex (an additional parameter to every trap)
I do think that passing an action or "forward()" thunk to a trap as extra argument beats David's proposed "throw ForwardToTarget" trick in terms of elegance and usability (maybe not in terms of performance).
I agree it looks less hacky. But I'm not entirely convinced when it comes to usability. As mentioned in the other answer, it makes understanding the trap a bit harder because what comes out of the trap may be less clear: since there is no explicit return statement, one has to figure out, what the value of the "action" function specifically when it's called for the "around" case.
Le 03/12/2012 00:06, David Bruant a écrit :
The call to action performs the original operation on target and remembers the result. After the trap returns, the proxy returns the remembered result of action. target = {a:1}; var p = new Proxy(target, { get: function(target, name, action){ var v = action(); target[name] = 2; } })
p.a; // ?
If p.a is 1 because the call to "action" remembered the "1", then the target and the proxy look awkwardly desynchronized. To know what's being returned from the trap, one needs to remember when the action is being called which makes writing traps harder I feel. With the current design, looking at the return statements is enough. If p.a is 2, I don't understand the point of "action". Or at least, I don't see how it's better than just calling Reflect[trap].
I've found much MUCH worse:
target = {a:1};
var p = new Proxy(target, {
get: function(target, name, action){
var v = action();
Object.defineProperty(target, name, {value:2, configurable:
false, writable:false}) } })
p.a; // 1 ?
In that case, the mechanism used to bypass invariant checking is a way to bypass invariants entirely.
What eternal[1] invariant does this bypass?
Le 03/12/2012 16:38, Mark S. Miller a écrit :
What eternal[1] invariant does this bypass?
Apparently none... Yet, additionally to the last case I showed, there is also:
var p = new Proxy({a:1}, {
isExtensible: function(target, action){
var v = action();
Object.preventExtensions(target);
}
})
// real life-like code?
if(Object.isExtensible(target)){
Object.defineProperty(target, 'b', {value: 37}); // throws
}
I agree it's not an eternal invariant, but it's quite surprising. [cc'ing Tom to make sure he reads this part] Arguably, the isExtensible, seal and freeze trap could have no "action" and just forward to the target. That's what the current invariant enforcement suggests anyway ("Invariant check: check whether the boolean trap result is equal to isFrozen(target), isSealed(target) or isExtensible(target)"). This applies to current proxies actually. Maybe their return value could just be ignored. Trap authors have all the information they need with the trap arguments to decide whether they want to throw. For the rest, the operation can just be forwarded to the target (which it has to for invariant check already).
Proxies-with-action have this weird taste of "one-move ahead"; like if they could run what they're expected and then play a bit more of the game before actually showing their before-last move. It makes sense they do not violate eternal invariants
Although not rigorously necessary when it comes to the very minimalistic eternal invariants, the current proxies provides some guarantees by design which are nice both for whoever writes handlers and whoever manipulates proxies.
Unrelated, but I think that custom property descriptor attributes are lost with action-proxies. I'm not sure yet what is a good way to recover them.
To pick up this thread again, I gave a talk about the trade-offs in designing JS proxies at a meeting last week. The people following this thread may be interested in seeing the slides: < soft.vub.ac.be/~tvcutsem/invokedynamic/presentations/Tradeoffs_WGLD2012_Austin.pdf> (the talk tremendously simplifies the story to cram it in 25 minutes. I added some minimal notes to make the slides easier to follow.
The talk is more or less a story about how we started out with non-direct proxies and ended up with direct-proxies. It doesn't provide any new insights or discuss the recent notification proxies or "action-proxies".
Cheers, Tom
2012/12/3 David Bruant <bruant.d at gmail.com>
I will refer to Dean's proposal as "notification proxies" (where traps essentially become notification callbacks), and will continue to use "direct proxies" for the current design where the trap can return a result (which is then verified).
These notification proxies remind me a lot of how one must implement membranes with direct proxies. The general idea here is that the proxy must use a "shadow" target as the proxy target, and the handler must refer to the "real" wrapped target. Traps that have to do with invariants (e.g. freeze/isFrozen, or querying a non-configurable property descriptor) require the proxy to "synchronize" the state of the real and shadow targets, because the proxy will verify the trap result against the shadow target.
For such membranes, let's consider how direct proxies and notification proxies trap an operation:
Direct proxies:
Notification proxies:
I agree that the big benefit of notification proxies is that they get rid of all the complex validation logic.
However, some reservations:
if traps become mere notifications, perhaps their names should change to reflect this, e.g. "notifyGetOwnPropertyNames" instead of "getOwnPropertyNames". This is to alert handler writers that the return value of these traps will be ignored.
I think we do lose some expressiveness in the case of pure virtual object abstractions that don't pretend to uphold any invariants. With notification proxies, the handler must always (even in the case of configurable properties) define concrete properties on the target. Any virtual object abstraction is thus forced to maintain a "shadow" which must eventually be represented as a plain Javascript object. In other words: all virtual object abstractions, whether they pretend to be frozen or not, have to make use of the "shadow target" technique, with the burden of synchronizing the shadow upon each operation. That burden currently doesn't exist for non-frozen virtual object abstractions (I'm using "frozen" vs "non-frozen" here as a shorthand for "has non-configurable/non-extensible invariants" vs "has no such invariants").
Regarding the overhead of the getOwnPropertyNames trap having to create a defensive copy of the trap result: With notification proxies, upon trapping "notifyGetOwnPropertyNames":
In the current design:
I think the storage costs are largely the same. However, with notification proxies, if the properties were "virtual", those properties do linger as "concrete" properties on the target. Yes, the handler can delete them later, but when is later? Should the handler schedule clean-up actions using setTimeout(0)? This somehow does not feel right.
I like the simplicity of notification proxies, but we should think carefully what operations we turn into notifications only.
More generally, notification proxies are indeed "even-more-direct-proxies". They make the "wrapping" use case (logging, profiling, contract checking, etc.) simpler, at the expense of "virtual objects" (remote objects, test mock-ups), which are forced to always "concretize" the virtual object's properties on a real Javascript object.
Cheers, Tom
2012/11/25 Mark S. Miller <erights at google.com>