On Dec 21, 2012, at 7:01 AM, Tom Van Cutsem wrote:

> 2012/12/21 Allen Wirfs-Brock <[email protected]>
> 
> On Dec 20, 2012, at 12:07 PM, Tom Van Cutsem wrote:
> 
>> 2012/12/20 Allen Wirfs-Brock <[email protected]>
>> 
>> On Dec 20, 2012, at 2:21 AM, Tom Van Cutsem wrote:
>>> If target.baz is a writable data property
>>> proxy.baz = 42 will eventually call Object.defineProperty(proxy, 'baz', 
>>> {value:42})
>>> (and *not* Object.defineProperty(proxy, 'baz', 
>>> {value:42,enumerable:true,writable:true,configurable:true}) as it did 
>>> previously)
>>> This behavior is consistent with the method and accessor case: in all 
>>> cases, the target delegates back to the proxy.
>> 
>> This is actually essential to maintaining ES5 default semantics because 
>> {value:42} and {value: 42, enumerable:true,writable:true,configurable:true} 
>> have quite different meanings to the ordinary [[DefineOwnProperty]].
>> 
>> Indeed. And this is what caused the issue I reported in the OP.
>>  
>> 
>> It is equally important that if target.baz does not exist that 
>> [[DefineOwnProperty]] is called on proxy with with the full  {value: 42, 
>> enumerable:true,writable:true,configurable:true} descriptor to ensure that 
>> the new property gets created using the "crated by assignment" attributes 
>> rather than the [[DefineOwnProperty]] defaults.
>> 
>> Yes, this is the case.
>>> One thing that I learned from all this is that it's simpler to think of the 
>>> proxy as *delegating* (as opposed to forwarding) to its target by default. 
>>> And under the semantics of invoke = get + apply, that is actually the 
>>> simplest option.
>> 
>> Yes, this is the best way I have found to think about them and I've been 
>> patiently waiting for you to see the light :-)   However, a corollary is 
>> that most  internal method calls within derived internal methods/traps need 
>> to delegate back to the original "receiver".  In some cases, we don't 
>> provide the original receiver as an extra argument, so it isn't available to 
>> do this.
>> 
>> A quick scan of the ordinary internal method suggests that we may have this 
>> problem for [[Delete]], [[Enumerate]], [[Keys]], and [[GetOwnProertyKeys]].
>> 
>> More generally, I would argue that all Proxy traps (and the corresponding 
>> Reflect functions) potentially need access to the original "receiver".  We 
>> don't know what somebody is going to do in such traps and they well need to 
>> call back to the original receiver for the same sort of consistency issues 
>> we have encountered with  [[SetP]].  this is particularly apparent if you 
>> think multiple levels of proxies chained through their target slots. 
>> 
>> I'm not sure I follow. In my understanding, the original Receiver is only 
>> needed for traps that involve prototype-chain walking and are thus 
>> |this|-sensitive. That would be just [[GetP]] and [[SetP]]. One can make the 
>> case (David has done so in the past) for [[HasProperty]] and [[Enumerate]] 
>> since they also walk the proto-chain, although it's not strictly necessary 
>> as the language currently does not make these operations |this|-sensitive.
> 
> The proxy target delegation chain is also this-sensitive when it invokes 
> internal methods.  For example, in the revised [[SetP]] step 5 it is 
> important  that the [[DefineOwnProperty]] calls (in 5.e..ii and indirectly in 
> 5.f.i are made on Receiver and not O.
> 
> Let's take a simple case, the [[Keys]] internal method. It needs to do a 
> [[GetOwnProperty]] call for each property to determine whether or not it is 
> enumerable.  If [[Keys]] is invoked on a Proxy and automatically delegated to 
> the target, should the [[GetOwnProperty]] call be made to  the target or to 
> the original receiver (the proxy)?  If it is invoked on the target, we will 
> get a list of what the target thinks are its enumerable properties. The 
> proxy, itself, might have a different idea of which of its properties are 
> enumerable.  Since the original operation was invoked on the proxy, we 
> presumably want [[Keys]] to tells us what that object (the proxy) thinks are 
> its enumerable own properties not what the target actually has as enumerable 
> own properties.   The latter could actually be leaking a secret that the 
> proxy is trying to keep.
> 
> If we're talking secret-keeping-proxies, these probably should just override 
> and implement all traps, and not default to forwarding to the original 
> target. That feels too brittle for an abstraction that's trying to keep a 
> secret.

It's just an example of a very simply use of Proxy's where a ES programmer 
might naively think they only need to over-ride a single fundamental trap in 
order to get their desired effect.  For example, they might want a property 
named "length" to always be treated as non-enumerable.  They might reasonably 
think that all they need to do is a handler that looks like:

       {getOwnPropertyDescrptor(target,key) {
            let desc = Reflect.getOwnPropertyDescriptor(target,key);
            if (desc && name==="length) desc.enumerable=false;
            return desc;
       }}
            
>  
> Here is another way to think about it.  Both the ES prototype chain and the 
> ES proxy target chain can be view as examples of Lieberman style delegation, 
> but at different abstraction levels.   Prototype delegation is about 
> delegation of ES functions.  The self-calls take place at the level of ES 
> functions and the implementation layer is careful to pass the correct this 
> values to inherited (ie delegated) functions.  The mechanisms for describing 
> the semantics (and perhaps implementing it) uses the ES internal methods.  
> But, for prototype delegation purposes, the internal methods do not make true 
> self-calls.  Historically, ES internal methods are not inherited/delegated 
> along the prototype chain.   This is why the Receiver needs to be passed as 
> an explicit parameter to the internal methods that are responsible for 
> implementing ES function level self-call semantics.
> 
> For proxies, things are flipped.  The delegation isn't at the level of ES 
> functions, instead it is at the level of internal methods. The self-calls 
> that are of interest are not to ES level methods but to internal methods. 
> That's why [[Keys]] really should be doing a self-call of [[GetOwnProperty]] 
> through the original receiver rather than to the same target object that 
> fielded the [[Keys]] call.  Similar situations exist within [[Delete]].
> 
> Thanks. That's a nice explanation of the two levels of delegation.
>  
> If you step back a bit and just think about the concepts of Lieberman 
> delegation and self-calls without worry about the specific of the proxies or 
> the ES MOP I think you will come to see that delegated target calls naturally 
> should self-call back to the original object.  That's what Lieberman style 
> delegation is all about.
> 
> Or, here's another way to look at it. You need real self-calls that go back 
> to the original receiver whenever you have an interface with interdependent 
> operations (some of which may be "derived" and other may be "fundamental") 
> and where you permit individual operations to be selectively delegated.  
> Otherwise, leaf objects won't present a "self"-consistent set of operations.  
> This problem goes away, if you do not allow delegation at the individual 
> operation level, but instead only permit either all operations or no 
> operations of the interface to be over-ridden/delegated.  This "no individual 
> over-rides" solution is essentially what the ES spec. historically applied to 
> internal methods.   But direct proxies now allow over-rides at the individual 
> internal method granularity.  This exposes the sorts of inconsistencies I'm 
> describing.
> 
> Your references to "derived" vs "fundamental" and self-consistency make me 
> think about the Handler API again:

(yes, that was intentional)
> 
> If a proxy handler simply subclasses Handler, does not override any derived 
> trap, and overrides all fundamental traps to simply forward to the target, 
> then I think you basically achieve what you would have achieved if the ES6 
> spec used delegation internally.
> 
> If a derived trap is called on such a handler, it inherits the default 
> implementation from Handler which basically mimics all the ES6 built-in 
> algorithms, but with explicit self-sends to call the fundamental traps. 
> Granted, the fundamental traps themselves are still forwarded (not 
> delegated), but for fundamental traps, there is no other derived operation to 
> call back on anyway (they are not Receiver-sensitive, so to speak), so that's 
> OK.
> 
> As Andreas points out, modifying the built-in semantics of normal objects to 
> support delegation is a non-trivial change that may impact the performance of 
> normal, non-proxy objects. At the very least implementations will want to 
> branch on whether the target and the receiver object are the same (then they 
> can fall back on the current implementation), otherwise they must explicitly 
> execute the ES6 spec algorithm.
> 
> I think that with the provision of the Handler API, we provide Proxy authors 
> with the same benefits than if we would add delegation to the ES6 built-ins, 
> without any required changes to existing internal methods for normal Objects.

Yes, I generally agree that Handler is a way out of this.  But if that is our 
solution to the problem then I still think we have a usability issue with the 
current Proxy API design.

Allen

_______________________________________________
es-discuss mailing list
[email protected]
https://mail.mozilla.org/listinfo/es-discuss

Reply via email to