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

_______________________________________________
es-discuss mailing list
es-discuss@mozilla.org
https://mail.mozilla.org/listinfo/es-discuss

Reply via email to