Proxies: get+fn vs. invoke

Dmitry A. Soshnikov dmitry.soshnikov at gmail.com
Fri Oct 15 04:38:13 PDT 2010


  On 14.10.2010 22:57, Tom Van Cutsem wrote:
>
>     ... All do work. I.e. any missing property, for you, is a method.
>     Do whatever you want with it. Call e.g. your noSuchMethod function
>     inside it.
>     - Hm, but how can I test whether a some method (or a property)
>     exists on my object?
>
>     Obviously, the approach:
>
>     if (!o.n) {
>       o.n = function () {};
>     }
>
>     or even so:
>
>     if (typeof o.n != "function") {
>       o.n = function () {};
>     }
>
>     won't work. Why should I get always a "function" for every reading
>     of a (non-existing) property?
>
>
> Ok, I finally see what issue you are addressing. I will try to 
> summarize (for you to see if I get it right)
> - o is a proxy that proxies for another object o2, but in addition, it 
> wants to treat missing methods on o2 specially (e.g. return a no-op 
> function to prevent errors or return a method of some other object)
> - its get method would look something like:
> get: function(r, name) {
>   var prop = target[name];
>   if (prop) return prop;
>   // else deal with the missing method, probably by returning a function
> }
> - your feature-test using !o.n would fail because o.n returns a 
> function, so the then-branch of the if-statement will not trigger.
>

Yes.

> - what you would like to do is to return 'undefined' from the 'get' 
> trap if the missing property is only accessed, and return a function 
> only when the property is invoked.
>
> First: good point. AFAICT, this can't be done using the current proxy 
> API, and adding a flag to `get` or another trap would make this possible.
>
> It is, however, debatable whether it is appropriate to override `o.n` 
> with the external function just because it does not exist on o2. After 
> all, the proxy can handle missing methods. Presumably, the code in the 
> else-branch is going to make use of `o.n` (either as a funarg, or it 
> may call it as a method `o.n(...)`. This will not crash, the proxy 
> will deal with it. It's not clear that overriding the `n` method with 
> the function of the then-branch is the right thing to do. Normally 
> such feature-testing is done to make sure that later calls to 
> `o.n(...)` won't crash. When using proxies that deal with missing 
> methods, calling `o.n(...)` won't crash the code, so why should the 
> method be replaced?

That's the main thing and the issue -- you say: "When using proxies that 
deal with missing methods". However, you miss the very major word -- 
"only": "When using proxies that deal *only* with missing methods" and 
then addition: "...because we can't have at the same time dealing with 
missing properties and missing methods" (thus, "missing methods" means 
"missing properties with a call expressions at call-sites").

The use case is:

1. One lib provides some object `foo`;

2. In terms and principles of an abstraction I _shouldn't_ care _how_ 
this `foo` is implemented and which internal structure it has (i.e. 
whether it's a proxy (with possible implementation of noSuchMethod) or 
not -- _does not matter_ for me as a user of the lib);

3. I invent a good patch for the lib and in particular for the object 
`foo`. I inform about it an author of the lib (or possibly don't inform, 
he'll new it later himself, when the patch will be de-facto standard -- 
yeah, hello, `Function.prototype.bind`). However, the author will 
implement it _not soon_ (the simplest example -- patches for array 
extras are existed for years in any framework, but only now authors of 
the engines provide them, in order to conform ES5 spec).

4. I don't wanna wait 5 years, I write my own patch. Using the best 
practice patterns, I provide a check for the native implementation and 
do not create my own if the native already implemented (actually, the 
casual and everyday situation -- in any framework):

if (!foo.forEach) {
   // OK, let's go
}

Result: (we don't go to `then` branch): damn, seems I underestimated the 
author and he implemented it not after 5 years, but already now. Let's 
use the native then... Hey, hold on! It's not `forEach` I expect, it's 
some strange anonymous function instead. WTH? Where it comes from. Hey, 
wait the second!:

foo.blaBlaBla
foo.WTF
foo.heyIDontUnderstandWhatsGoingOn

All of them are _some strange functions_? What happened here? Every 
"non-existing" property _does_ exist! Don't know how to program then... 
the logic is corrupted.

So obviously, distinction of existing and non-existing property is 
needed. And invariant "forEach" in foo == false && foo.forEach === 
undefined should be _the invariant_ (i.e shouldn't be broken).

(I understand, that working with proxies -- we can break any rules, as 
e.g. implementation specific host object. I.e. we can "lie" in `in` 
operator or in `hasOwnProperty` check, but at the same time return some 
stuff from the `get`. However, the situation takes place and we should 
think how to solve it. If we admit that proxies have complete right to 
break every logic like host objects -- it may be leaved for the 
conscience of a proxy developer).

>     - Another minor thing -- `delete` does not really delete.
>
>     delete foo.bar;
>     foo.bar; // function
>
>
> Well, it depends on how you implement the proxy. It could keep track 
> of deleted property names (I agree this would be cumbersome).

Yeah, count, how many already additional code (including 
caching/invalidating the cache) this implementation is required. Again, 
from the abstraction viewpoint -- if all this will be correctly 
encapsulated from a user -- then there is no difference how it is 
implemented. If the end result is the same by semantics, from the 
semantics viewpoint all implementations are equal.

> But would a separate `noSuchMethod` trap really help here? Consider:
>
> delete foo.bar;
> foo.bar(); // I expect this to crash now, but it will still call 
> `noSuchMethod`
>

I specially mentioned, there the case with `delete` is not essential 
(the words: "What are you trying to delete? Non-existing property? It 
doesn't exist from the beginning"). However, in case of using 
noSuchMethod, this invariant doesn't broken, since either with applying 
`delete` or without it -- correctly `undefined` is returned at reading. 
In case of `get+fn` always a function is return. So this minor step I 
think can be reduced to the first issue described above -- a reading a 
"non-existing" property, which actually _does_ always exist and is a 
function.

Regarding your expectation, no, there should be no any crash, because 
"bar" _did not exist before, and it does not exist now_.

Actually, I see the issue of why there is a discussion of "just invoking 
phantoms" vs. "real funargs". It's because of the _same syntax_ for 
_calling a real function_ and _informing the hook_ that there is no such 
method on an object. I said several times before, and repeat it now 
again: there is a conceptual difference between these two approaches.

1. With handling the situation using noSuchMethod hook we deal exactly 
with the _situation_, with the _event_ that something goes wrong. And we 
have a special hook for that. We may don't wanna deal with a (some?) 
function. In this case, we just _notify_ our handler about this _fact_ 
(about this _even_, a _situation_). And exactly in this case there is 
contradiction because for notifying our handler, we use a _call 
expression_, which also is used to call _real function_. In fact, this 
case is equivalent to the check:

if (typeof foo.bar != "function") {
   // the code of noSuchMethod passing "bar" and args
}

And just for not repeating this each time, it can be encapsulated for a 
some sugar, e.g. with using call expression for it:

foo.bar() // which desugars to the mentioned above code

This is the main problem. If there where used other syntax for this 
sugar, e.g.:

foo.bar?(1, 2, 3):defaultMethod("bar", [1, 2, 3])

then there were no this philosophical dilemma with funargs/apply.

2. The approach with supporting funargs/apply assumes that we deal not 
with _nonflying_ the handler about the _missing method even_, but with 
some newly created function. But repeat, possibly a user didn't mean at 
all the work with a function. I.e. this scheme assumes the same 
desugarred code, but with previous creation of a function:

if (typeof foo.bar != "function") {
   foo.bar = function () {
     return noSuchMethod.apply(...);
   };
}

(Notice, this typeof check is assumed on the lower implementation level; 
i.e. at higher level of abstraction which uses a user typeof already 
always return `false` for the check since the method is already created 
by the proxy, i.e. by the lower abstraction level).

Theoretically, both approaches are acceptable. It'd be great though, to 
have invariants with funargs/apply. But. Only if reading of non-existing 
properties are fixed, i.e. does not return always a function for every 
non-existing property. However, this is a _vicious circle_. On one hand 
-- we have always a function at reading non-existing property (that 
seems broken behavior). On the other hand -- it's required to do the 
first case to have funargs!

That's it.

>     - OK, and what about the prototype chain? Where should I put this
>     proxy object in order to prevent of catching of all my missing
>     properties (because I want to catch them from other objects in the
>     prototype chain, to which these properties belong)?
>
>     Object.prototype.foo = 10;
>
>     "foo" in o // true, OK
>     o.foo; // but it's a _function_, not 10
>
>
> If o is a proxy that first queries another target object (like the 
> noopHandler does), it will find 'foo' and it will return 10.
>

Yeah, this case seems OK. Just a correct `in` check is required before 
returning a functions.

>     What about to have `noSuchMethod` _additionally_ to the `get`? It
>     will catch only missing properties, but: not _just_ missing
>     properties, but missing properties which use a call expressions at
>     call-sites. Thus, we can combine two approaches allowing a user to
>     choose how to handle the case of missing _method_.
>
>
>     handler.get = function (r, name) {
>       if (name == "baz") {
>         return function () { ... }; // and cache "baz" name if you wish
>       }
>       // other cases
>       return object[name];
>     };
>
>     handler.noSuchMethod = function (name, args) {
>       return this.delegate[name].apply(this, args);
>     };
>
>
> Could you specify when noSuchMethod is called? I think the algorithm 
> inside the proxy's [[Get]] method would look something like:
>
> If the "get" trap on the handler returns undefined AND the handler 
> defines a "noSuchMethod" trap AND the [[Get]] was triggered by a call 
> expression, then instead of returning undefined, return the result of 
> calling the "noSuchMethod" trap.
>
> Correct?
>

Yes, absolutely correct. And having such a scheme, I don't see what do 
we lose? Obviously -- nothing. But just gain. I.e. the scheme with 
"get+fn" is _still here_ (I repeat it again and already repeated several 
times -- nobody ask do not use it! Please, use) -- please, use it with 
all mentioned pros and cons. However, in _addition_, a noSuchMethod hook 
for a _proxy handler_ can be provided. Who don't need it / don't like it 
-- they won't use it and will use the scheme with "get+fn". Other -- 
will use it. I don't see any issues here. Is it hard just to add this 
hook in addition? And then it will be fair to check which of these two 
approaches will be used more often by users and what do they *really* 
need in mostly cases.

It's the ideal variant seems -- wanna work with partial applications 
passing funargs? -- No problem! -- Use get+fn scheme! Wanna just simple 
notification of a missing method event? -- Also no problem! -- We have 
additionally noSuchMethod hook for a proxy.

Dmitry.

> Cheers,
> Tom
>

-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://mail.mozilla.org/pipermail/es-discuss/attachments/20101015/5e4e99c6/attachment-0001.html>


More information about the es-discuss mailing list