Proxies: get+fn vs. invoke

Dmitry A. Soshnikov dmitry.soshnikov at gmail.com
Thu Oct 14 07:54:21 PDT 2010


  On 14.10.2010 4:14, Brendan Eich wrote:
> On Oct 13, 2010, at 6:56 AM, Dmitry A. Soshnikov wrote:
>
>> Also I think now, that what was named as pros, i.e. ability to have 
>> funargs and call/apply invariants, in real, not so pros. Because 
>> users more likely want to catch exactly missing methods (if you don't 
>> like the word "methods", since there're no methods, there are 
>> properties, let's say -- missing properties which ended with `call 
>> expression` at the call-site).
>
> That's not our experience with E4X (ECMA-357), which specifies XML 
> methods as invoke-only. They seem to be normal function-valued 
> properties of XML.prototype, but getting one by name on an XML 
> instance in a non-callee expression context instead tries to find an 
> XML child element or elements of the method's name, returned as a list.
>
> Some of this is peculiar to E4X, but the invoke-only nature of the 
> methods, per-spec, is not. And it breaks apply and functional 
> programming, so we extended E4X with the function:: pseudo-namespace 
> to allow one to extract methods from XML instances.
>

Yes, I'm aware of it. However, you mention a similar end result 
(inability to extract a function with a normal (accessor) syntax), but 
with completely different reason. In case of EX4 you talk about the 
existing real methods. In case of proxies, we talk about non-existing 
property (which is activated with a next call expression). The 
difference is: in first case a user really deals with existing stuff and 
expect the functions to be extracted (of course in this case ECMA-357 
had to do something --  provide :: -- to allow this). In the later one, 
at the first place, a user wants to catch the call expression.

Yeah, it's a good example, but I see that similarity of the end result 
is used to apply it to the different _reasons_ (messing the concepts). 
And in case of the first reason -- yes, it's critical. In case of the 
second one -- not so or even non-critical. Because, repeat, catching 
such cases (missing methods) a user may not want to deal with an alive 
function, since it's just a signal to do to something (to handle the 
_case_). Below I provide test sources (as you asked) to complete my 
position (showing that implementations with 'get+fn' + 'noSuchMethod' 
can even _co-exist_ -- and everyone will be happy).

> Others using __noSuchMethod__ are happier as you say, because (for 
> example) they are Smalltalkers (Bill Edney is on this list) who 
> pretend there are only method calls (message sends), never properties 
> or first-class functions.
>

Yes, in systems which has second-class functions it's easier to handler 
this case (there is no need to return a function). However, it's not 
just because there are first-class function. E.g. Ruby also has them, 
but having them, it distinguishes call expression syntax in different 
cases: a method is called with (), a lambda is called with `.call` method:

# global catcher for missed methods

def method_missing(name, args)
   p "Method: ", name, "Args: ", args
end

# a method "foo"
# which returns a lambda --
# a functional first-class object (also, a closure);
# the lambda itself just prints 10

def foo
   lambda {
     p 10
   }
end

# we call "foo" method (notice, with () syntax),
# and then call with different syntax -- via .call, the
# returned lambda

foo().call # 10

# however this case is
# caught with method_missing

nonExisting(2) # "Method: " :nonExisting, "Args: " 2

But this is just -- a "by the way", Ruby is irrelevant with ES and this 
mailing list (this example is just to mention that the discussed issue 
is not just because there are first-class functions). I like more though 
that ES has the same syntax for these cases.

Besides, I understand that ES has similar to Python implementation with 
"only properties", and moreover, Python also has no __no_such_method__ 
hook, only its __get__ and __getattr__ (also around the Internet there 
are some shims of Ruby's `method_missing` for Python with returning 
every time a function from the __get__). But, having similar to Python 
implementation, JS can go further and better.

> But that happiness is not universal, so your "not so pros" judgment is 
> not true for everyone.
>

I understand, however I'd like to notice that I'm not judging, but 
objectively analyzing.

> Should we support everyone even if it makes the Proxy system more 
> complicated and allows for not-quite-function methods?
>
> Our decision was "no". You're asking us to revisit to support the some 
> (not all) developers who want to make not-quite-function methods. 
> That's a fair request but I think you need to do more than assert that 
> the resulting complexity is not a problem. Further below, I'll do some 
> legwork for you.
>

OK, I understand this position quite clearly. I'll also show further 
below that there can be possible compromise with co-existing both 
approaches.

>
>> And funargs/apply invariants should be leaved for _real functions_ 
>> (existing or ad-hoc, explicitly returned from the `get`).
>
> Why shouldn't all methods including missing ones be _real functions_? 
> Why complicate the domain of discourse with real and not-quite-real 
> functions?
>
>

Assuming this, I try to see on the issue from the position that catching 
a missed method, a user deals with a _fact_, with just a _signal_ about 
this _situation_ (that a method is missing), but not with a method 
itself. And he can handle this situation.

What in contrast proposes the implementation with returning each time a 
function?

(I in advance apologize for such a simplified style of description and 
long text below, it's just easier for _myself_ (and first of all -- only 
for myself), for not to confuse with all cases; I just try to analyze 
and see all available pros and cons).

- I want to catch missing method, can you (a system) handle this 
_situation_?
- Which missing methods? You don't have any missing method.
- Really?
- Yes, try it yourself:

var o = {};

o.n();
o.foo();
o.bar();

... 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?

- Hm... use `in` operator as a variant then for this case:

if (!("n" in o)) {
   o.n = function () {};
}

- Yeah, right, it may help. But you conclude that the case with reading 
a property for such a check is broken? Btw, I saw it widely used in 
current scripts for providing missing stuff (e.g. if 
(!Array.prototype.forEach) { ... }).
- Unfortunately. it's broken.
- Hm, i.e. a property "exists" -- `o.n` (it's a function as I see), and 
at the same time -- does not -- "n" in o -- false?
- Unfortunately. Yep.

- Interesting... And what about if I want to handle both -- reading a 
property and calling a method in one `get` of a proxy?
- No, you can't. Didn't you realized it still? -- You have always only 
functions in this case. Forget about non-functional properties. There is 
no such API. You can handle _either_ properties, _or_ functions via `get`.

- Well, OK... Let's assume it... And what about the === operator? Is it 
also broken?
- No, why is it broken? Just cache your functions by the name. Yeah, it 
will take a bit of code (which you possibly will repeat every time in 
such cases, but...)
- Yeah, right. Fair enough (though I thought the same). Moreover, it 
will work with assigning to another name:

foo.bar == foo.bar; // true
foo.baz = foo.bar;
foo.baz == foo.bar; // also true, since foo.baz exists now (of course if 
we return _existing_ properties _as is_)

Seems OK. Though, I see one more place which should be patched:

// a non-existing "foo.bar"
foo.bar; // cache it at first reading
foo.bar(); // alerts e.g. 1, first implementation

// it's existing now
foo.bar = function () {
   alert(2);
};

foo.bar(); // alerts 2

// delete it
delete foo.bar;

foo.bar(); // alert 1?

So, besides that small code with caching in `get`, we need some 
_invalidating cache_ logic in the `delete` trap. It seems that all this 
"magic" code combines in some pattern (possibly, there is a sense to 
encapsulate and abstract it in some sugar, don't know).

- Another minor thing -- `delete` does not really delete.

delete foo.bar;
foo.bar; // function

- Right, but what are you trying to delete? A non-existing property?
- Yes, I understand, but it just looks a bit strange -- non-existing, 
but still always is equal to some _function_. Moreover, with our caching 
system, I see that this is a _very consistent_ property in it's equality 
invariant:

foo.bar == foo.bar; // always (correctly) true

but it always a _function_. I could understand that it can be for 
non-existing properties where undefined === undefined, but here are the 
"existing" functions.
- Well...

- 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

- Doesn't `o` inherit from the `Object.prototype`?
- No, it does inherit, but since you don't have a function call, you 
won't reach `Object.prototype`. Though, you can reach Object.prototype's 
methods (yes, with a bit overhead 'cause it's reached via our wrapper).

- So, o.toString() calls Object.prototype.toString (in case of course I 
inherit `toString` from the Object.prototype), but at the same time 
o.toString !== Object.prototype.toString, right? It seems === is broken 
again.
- Unfortunately.

Did I miss something?

OK, so what pros and cons we have:

Pros:

1. we can handle call-expressions: foo.bar()
2. functions may be applied, passed as functional values (functional 
WTF!): foo.bar
3. with a little "magic" (caching by name) we can even have them equal 
to each other: foo.bar === foo.bar

Cons:

1. a non-existing property is always a function: foo.bar // function
2. at the same time, it behaves as _consistently existing one_, 
including equality: foo.bar === foo.bar (with some the mentioned 
"magic"); and at the same time we can't delete it.
3. if we want to apply some patch for an object depending on existence 
of some property -- we can't do it using reading accessor of the 
property (i.e. cases with if (!foo.bar) or if (typeof foo.bar ... won't 
pass), only `in` operator may help. Yeah old scripts with testing if 
(!foo.bar) {...} should be rewritten (Is WEB really shouldn't be broken?)
4. regarding the same `in`, a property isn't here -- "foo" in bar -- 
false, but it's always here -- foo.bar // always a function
5. we can't read correctly _existing_ properties from the prototype 
chain regarding objects which are deeper than our proxy, because the 
proxy will catch them and return its function. In case when a 
prototype's property is really a function, it's OK -- we just wrap it 
(with a bit overhead, let it be). But in case of _non-functions_, sorry, 
please get a function from a proxy anyway. This step assumes that a 
proxy object should be placed as deeper in the prototype chain as 
possible. Though, it cannot be placed deeper than Object.prototype.

- OK, we have more cons, I see. What do you propose than? You can't just 
judge this approach without suggestions.

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);
};

Then we have:

var foo = {};

// reading of a non-existing property
foo.bar; // correctly undefined!, but not a function

foo.baz; // function, ad-hoc trapped by the `get`
foo.baz(); // OK
foo.baz.apply(null); // OK
foo.baz === foo.baz; // true with caching

// try to call non-existing property:
// first, `get` is  fully trapped. At this
// step, `get` may return also a function -- then
// noSuchMethod won't be called;
// However, in our case `undefined` is returned, and we have:

foo.bar(1, 2, 3); // noSuchMethod is called with passing message to a 
delegate object

Pros:

1. we can handle call-expressions: foo.bar()
2. we can differentiate (by the call-site context) missing properties 
from missing "methods"

ALL mentioned above invariants do work:

3. foo.bar === foo.bar // in any case! (with logical `get` trap of course)

4. if (!foo.bar) foo.bar = {} // also OK

5. foo.toString === Object.prototype.toString; // true!

6. "bar" in foo; // false, also OK

Cons:

1. Unable to apply/call `foo.bar`, but at the same time able to call it 
-- foo.bar()

* Note: excluding a case of ad-hoc `get`: in this case if non-existing 
"bar" is returned from `get`, then there is no even this cons -- we can 
apply it and call it.

That's it.

E.g. who want to build it as in previously proposed scheme -- with using 
[ only `get` + caching + invalidating the cache + broken ===, in, 
reading property tests ] -- they may still use it. All is needed just 
_not to define noSuchMethod_ on the handler. If nevertheless it's 
defined on the handler _and_ a callee context is a call expression -- 
it's called. It seems quite straightforward and _practical_.

(Sorry again for this long description, however it's needed, I'll refer 
it when will explain to JS programmers the current sate of noSuchMethod 
in ES and why it's so).


>> Moreover, as it has been mentioned, such returning has broken === 
>> invariant anyway (and also broken invariant with non-existing 
>> properties).
>
> Proxy implementors can memoize so === works. It is not a ton of code 
> to write, and it gives the expected function-valued-property-is-method 
> semantics. Here is the not-all-that-inconvenient proxy code:
>
> function makeLazyMethodCloner(eager) {
>     var cache = Object.create(null);
>     var handler = {
>         get: function (self, name) {
>             if (!cache[name])
>                 cache[name] = Proxy.createFunction({}, function () {
>                     return eager[name].apply(eager, arguments);
>                 });
>             return cache[name];
>         }
>     };
>     return Proxy.create(handler, Object.getPrototypeOf(eager));
> }
>
> A little test code:
>
> var o = {m1: function () { return "m1"}, m2: function () { return 
> "m2"; }};
> var p = makeLazyMethodCloner(o);
> print(p.m1());
> print(p.m2());
>

Yes, thank you Brendan, I completely understand it. As you possibly saw 
I also talked about caching in the previous letters.

> Some subtle things here:
>
> * The missing fundamental traps in the handlers are filled in by the 
> system. This is a recent change to the spec, implemented in 
> SpiderMonkey in Firefox 4 betas.
>

Is that standard forwarding `noopHandler` mentioned before? Yeah, great. 
It is useful. Though, possibly all implicit traps may bring a little 
overhead (if e.g. a user does not want to trap `delete`, but it will be 
trapped). From the other hand, yes, it's very convenient.

> * Even p.hasOwnProperty('m1') works, because the get trap fires on 
> 'hasOwnProperty' and clones eager['hasOwnProperty'] using a function 
> proxy, even though that method comes from Object.prototype (eager's 
> Object.prototype). The hasOwnProperty proxy then applies 
> Object.prototype.hasOwnProperty to eager with id 'm1'. No get on 'm1' 
> traps yet -- no function proxy creation just to ask hasOwnProperty.
>
Yep, at least properties with call-expressions are caught. But 
unfortunately, not other properties. And also p.hasOwnProperty !== 
Object.prototype.hasOwnProperty

> * Both p.m1 and p.m1() work as expected. Only one kind of function.
>
Yes, this is a pros mentioned above.

> Now consider if you had a third parameter to the 'get' trap to signal 
> callee context vs. non-callee context. You'd still want to proxy the 
> functions, that doesn't get simpler just due to a change of trap 
> parameters. You'd still want to cache for identity. But you would have 
> made invoke-only methods.
>
> Ok, let's give up on functional programming and cached methods. Here' 
> s my version written to use only __noSuchMethod__, no proxies:
>
> function makeLazyMethodCloner(eager) {
>     return Object.create(Object.getPrototypeOf(eager), {
>         __noSuchMethod__: {
>             value: function (name, args) {
>                 return eager[name].apply(eager, arguments);
>             }
>         }
>     });
> }
>
> 9 lines instead of 13, but broken functional programming semantics -- 
> you cannot extract p.m1 or p.m2 and apply them later, pass them 
> around, etc.
>
> What good would result from this? Again, our view in TC39 is "not much".
>

But I propose to have `noSuchMethod` trap _in addition_ to `get`. And 
this `noSuchMethod` should be called _only_ if (1) it's defined on the 
handler AND (2) _only_ after `get` _completely finished_ its work AND 
(3) if `get` returned undefined AND (4) call-site has a call-expression 
AND (5) requested property is not in object (to diffirentiate from the 
real undefined value).

What the reason that this is bad somehow? Only pros. Combination of 
`get` and `noSuchMethod` is a good way which may cover most cases 
(including with apply invariant -- in this case an ad-hoc case for 
non-existing property is written in `get`).

Everyone seems will be happy from this position, from the compromise. 
Those who don't wanna see noSuchMethod -- please, nobody prevents you, 
just don't use it, but use the previous scheme with `get+fn` -- it's 
still _completely avaliable_ with _all its pros and cons_ (mostly cons 
as we see), nobody said that it shouldn't be used. We don't need even 
third argument for `get` 'cause it really will just complicate the 
handling. But to have _additionally_ noSuchMethod -- is good. Let it be. 
Who will want to use it -- they will use. Who won't -- it's their right, 
they won't.

Where am I wrong?

> Note that I used a mechanical, consistent coding style ("JSK&R",  { on 
> same line as function, newline after {), so the comparison is apples 
> to apples. Is the broken semantics really worth four lines of savings?
>
Yeah, right, but the first approach has also broken semantics in 
some/many places.

> So, no fair asserting "practically it's unsoundly complicated and 
> inconvenient". And please stop invoking "ideology" as a one-sided 
> epithet to throw against Tom or TC39.

With all respect, let me mention that I do not discuss here persons (and 
moreover do not throw against everyone), I'm not so interested in 
discussing persons here -- neither Tom, nor (excuse me), you, nor TC39. 
I also do not judge. What I do, is try to explain which issues I found 
during was playing with proxies, which pros and cons objectively I see 
and propose alternative variants. I polite with everyone here and talk 
with respect, but at the same time, excuse me, I do not need a 
permission to ask questions -- independently, whether questions seems 
pleasant or not for someone.

> Please do start showing examples, specifically an apples-to-apples 
> comparison with __noSuchMethod__ that is significantly simpler. I 
> don't see it.
>

Yes, right. So I did above.

Dmitry.

> /be

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


More information about the es-discuss mailing list