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