On Sep 17, 2013, at 9:36 AM, Jason Orendorff wrote:
> On Fri, Sep 13, 2013 at 11:36 AM, Tom Van Cutsem <[email protected]> wrote:
>> MarkM and I talked about conditional invocation offline and we convinced
>> ourselves to argue for the status-quo (i.e. continue to encode conditional
>> invocations as [[Get]]+[[Call]]).
>>
>> The most compelling argument we can think of is that [[Get]]+[[Call]] is
>> also the pattern JavaScript programmers use to express conditional method
>> calls today:
>>
>> var f = obj[name];
>> if (f) { f.call(…); } else { … }
>>
>> No matter whether or how we extend the MOP, such code exists and will
>> continue to exist, and well-designed proxies that want to re-bind |this|
>> must already deal with this pattern by implementing the "get" trap
>> correctly.
>
> Tom, it seems to me if there’s such a thing as “implementing the "get"
> trap correctly”, i.e. such that method calls work, that removes the
> main motivation for having [[Invoke]] in the first place. To recap
> what else [[Invoke]] achieves, in the light of that:
>
> * improved performance for proxies, because method calls go through
> one proxy handler trap rather than two, and no temporary function
> object is allocated
>
> * proxies can observe a bit about the caller (whether or not it's an
> [[Invoke]] call site), when a method is called
>
> If that's all, it seems like we should definitely remove [[Invoke]]
> and the .invoke trap. The MOP was already complicated enough. The
> performance argument is a non-starter, and the other “feature” is
> entirely undesirable.
I've actually become convinced that [[Get]] and [[Invoke]] are the correct
primitives and and that the worry about "conditional invoke" was a false
concern.
A method invocation such as:
obj.foo(arg)
is currently specified as being roughly equivalent to:
obj.[[Invoke]]("foo", [arg], obj);
and the ordinary implementation if [[Invoke]] decomposes into:
let func = obj.[[Get]]("foo");
let result = func.[[Call]](obj,[arg])
However, a proxy may do other things in its 'invoke' handler including
replacing the this value and/or arguments passed to the [[Call]]. There are
strong use cases for the variability of such translations which were the
recent motivation for re-introducing [[Invoke]]. It don't think we need to go
around the loop of reconsidering those use cases again. They are valid and we
will end up at the same place.
The problem is that we see within the ES specification a few instances of a
[[Get]]/[[Call]] sequence that look more typically like:
let func = obj.[[Get]]("foo");
let result;
if (typeof(func) == "function") result = func.[[Call]](obj,[arg])
else /* compute result some other way */
Our concern started with" "oh no, the [[Get]]/[[Call]] is inside of [[Invoke]]
how can we get between them. This led to various proposals for new MOP
operations that split up [[Invoke]] or allowed a conditional test to be
injected into [[Invoke]]. I think this is the wrong way to look at the problem.
We were being mislead by legacy [[Get]]/[[Call]] pattern and not looking at
the actual conceptual intent of these code sequences. In pure ECMAScrpt and
dealing at the level of object abstractions, this is what such use cases are
really trying to expression:
If (typeof(obj.foo) == "function") result = obj.foo(arg);
else //something else ...;
or, in prose: If the 'foo' property of obj is a method, invoke that method on
obj with arg as the argument.
or in pseudo-ES-pseudo code:
let func = obj.[[Get]]("foo");
let result;
if (typeof(func) == "function") result = obj.[[Invoke]]("foo",[arg],obj)
else /* compute result some other way */
In other words, the appropriate conversion of the pattern we observed in the ES
spec. isn't [[Get]]+test+conditional call to [[Call]]. It is
[[Get]]+test+conditional call to [[Invoke]].
>From this thread, there are two concerns I anticipate. The first is that a
>double property lookup of "foo" is being performed. Conceptually I don't
>have a problem with that as the double property access accurately represents
>the object level concept that is being expressed. However, there might also
>be a perforce concern about the double lookup, particularly for its usage in
>ToPrimitive. I believe this is a non-problem as implementations can easily
>avoid performing the double lookup it is an actual performance issue. A
>typical specified usage of this pattern can be implemented something like:
// If (typeof(obj.valueOf) == "function") return obj.valueOf();
if (obj is not an ordinary object) goto slowpath;
//in practice some sort of guard like this is likely to be used in front of
every MOP operation
//A more specified test would be if object does not use both the ordinary
[[Get]] and [[Invoke]] implementations
let func = InlinedOrdinaryGet(obj,"valueOf"); //and if "valueOf" resolves to a
getter, invoke it twice,yuck. Perhaps poison optimization if any ordinary obj
valueOf accessors defined
if (func is an ordinary function object) return inlinedOrdinaryCall(obj);
else if (func has a [[Call]] internal method) return func.[[Call]](obj);
//exotic function needs full MOP [[Call]] dispatch
else goto nextcase;
slowpath:
//func is not an ordinary object so need to dispatch MOP calls on it
//only slow path does double lookup
let func = obj.[[Get]]("valueOf");
if (func has a [[Call]] internal method) return obj.[[Invoke]]("valueOf",[ ],
obj); //full MOP [[Invoke]] dispatch
nextcase:
...
In other words, the cases in the specification for ordinary objects can be
implemented roughly like current implementations. No double lookup need be
performed and any extra guards are exactly the guards that are needed to
support the existence of proxies or other exotic objects that over-ride
[[Get]], [[Invoke]], or [[Call]].
I'm not particularly concerned about double lookup performance for similar use
cases code in JS code. For ordinary objects, I expect normal PIC mechanism to
eliminate most of the double lookup overhead and any hot code paths.
However, a concern that was mentioned is that JS programmers routinely express
the conditional method call pattern as:
If (typeof(func=obj.foo) == "function") result = func.call(obj, arg);
rather than
If (typeof(obj.foo) == "function") result = obj.foo(arg);
I appreciate this concern. However, without having or using [[Invoke]] this
pattern may run into the same sort of bugs that motivated us to recently add
[[Invoke]]. We need to educate JS programmers that with ES6 and the
availability of Proxies and other features that obj.foo() is not exactly the
same thing as obj.foo.call(foo) and that the latter formulation should
generally be avoided except in expert situations. the spread operator makes
it particularly easy to use () instead of apply The new "call" function is ()
and except for very special circumstances where a different this values is
being supplied obj.method() is how methods should be invoked.
I really don't think we need to debate this much longer. We just need to stay
the course with [[Invoke]] and I can update the spec. to replace
[[Get]]+[[Invoke]] rather than [[Get]]+[[Call]] for this conditional
situations. I may also added add a note suggesting that the extra [[Get]] can
be eliminated.
Allen
_______________________________________________
es-discuss mailing list
[email protected]
https://mail.mozilla.org/listinfo/es-discuss