[[Call]] vs. [[Construct]] using symbols
David Bruant wrote:
2012/10/3 Herby Vojčík <herby at mailbox.sk <mailto:herby at mailbox.sk>>
Hello, now that we have symbols (and use some of them language-wise, like @iterator or proposed @toStringTag), I'd say we can probably dissect [[Call]] and [[Construct]] semantics fairly easily. Assuming there are global constant symbols @construct and @call,
And I assume being able to arbitrarily change the [[Call]] and [[Construct]] of a function at any time?
for min-max classes the issue is very simple: class Foo { @construct(bar) { this.bar = bar; } @call() { // do something else } }
I'm not sure this is appropriate for min-max classes. These are not so much private properties as they are internal properties.
They are not private symbols, they are unique symbols. Like @iterator. And they are not used to define properties (see below).
I'd propose the above as the minimal proposal for [[Call]] and [[Construct]] separation (even when it only works for classes; and so they can be abused to create function with [[Call]] and [[Construct]] separated). Implementation would be to create Foo such that it chooses one or the other based on whether [[Call]] or [[Construct]] is invoked.
So I guess I misanderstand. Do you want @call and @construct to be properties of instances of the constructor?
I want them to be properties of nothing. constructor
is also just a
convention (stemming from the fact that, in fact, property named
constructor is created in the prototype in case of plain constructor
function and holds a pointer to the constructor function).
I want the @construct and @call symbols to be used as a convention to specify [[Construction]] and [[Call]] behaviour for the class.
Some sensible defaults being applied when only one is present, like [[Construct]] will reuse @call (in [[Construct]] way) if only @call is present, and [[Call]] will throw is only @construct is present (it is a class, after all).
But otherwise, they should only work as convention names and the resulting (class? constructor function?) created so that it has appropriate [[Construct]] and [[Call]] behaviour.
It could go further, like this: var f = { @construct() {console.log(1)}, @call() {console.log(2)} }; f(); // logs 2 new f; // logs 1
var f = new Proxy(function(){console.log(1)}, {construct: function(){console.log(2)}}); Arguably, this is as readable (and efficient) and yields the same
result.
Hm, interesting, if it is this short, it is hard to beat. It is a proxy, though, is it really efficient? But yes, it is simple.
Before we add more unstratified traps, I'd like Tom and Mark to comment. They've thought a lot about invariants to preserve even with proxies in the picture, and also for non-proxies. And they have a use-case to test against: SES.
Herby Vojčík wrote:
But this has some quirks to solve, like typeof must be "function" for every object having @call, but what about object having only @construct?
If we do this, then let's hope testing (@construct in obj) isn't too bad. I suspect it won't be all that common.
Brendan Eich wrote:
Before we add more unstratified traps, I'd like Tom and Mark to comment. They've thought a lot about invariants to preserve even with proxies in the picture, and also for non-proxies. And they have a use-case to test against: SES.
Herby Vojčík wrote:
But this has some quirks to solve, like typeof must be "function" for every object having @call, but what about object having only @construct?
If we do this, then let's hope testing (@construct in obj) isn't too bad. I suspect it won't be all that common.
These were just "extended" thoughs.
The main thing I wanted to propose / get feedback is the simple [[Call]] vs [[Construct]] separation for max-min classes using const systemwide symbols @call and @construct, since we already have the precedent of @iterator (the rest were thoughts about possibility of generalizing it).
In other words, no real traps at all. Just convenient names @construct and/or @call used instead of (also convenient) name 'constructor'.
Like in: class Foo { @construct(x, y) { // initialize the instance } @call(obj) { // convert obj into Foo } // the rest of the methods } with the result of Foo constuctor function implemented so it can distingiush between [[Call]] and [[Construct]] context and doing the respective code.
On Sun, Oct 7, 2012 at 5:10 AM, Herby Vojčík <herby at mailbox.sk> wrote:
Brendan Eich wrote:
Before we add more unstratified traps, I'd like Tom and Mark to comment. They've thought a lot about invariants to preserve even with proxies in the picture, and also for non-proxies. And they have a use-case to test against: SES.
Herby Vojčík wrote:
But this has some quirks to solve, like typeof must be "function" for every object having @call, but what about object having only @construct?
If we do this, then let's hope testing (@construct in obj) isn't too bad. I suspect it won't be all that common.
These were just "extended" thoughs.
The main thing I wanted to propose / get feedback is the simple [[Call]] vs [[Construct]] separation for max-min classes using const systemwide symbols @call and @construct, since we already have the precedent of @iterator (the rest were thoughts about possibility of generalizing it).
In other words, no real traps at all. Just convenient names @construct and/or @call used instead of (also convenient) name 'constructor'.
Like in: class Foo { @construct(x, y) { // initialize the instance } @call(obj) { // convert obj into Foo } // the rest of the methods } with the result of Foo constuctor function implemented so it can distingiush between [[Call]] and [[Construct]] context and doing the respective code.
I actually really like this idea.
It addresses a common pattern today, that looks like:
function Led( opts ) { if ( !(this instanceof Led) ) { return new Led( opts ); }
// ... }
And it also fits a number of use cases of the spec itself, for built-ins:
call, contruct, object, spec section link
15.2.1, 15.2.2, Object ecma-international.org/ecma-262/5.1/#sec-15.2.1 15.3.1, 15.3.2, Function ecma-international.org/ecma-262/5.1/#sec-15.3.1 15.4.1, 15.4.2, Array ecma-international.org/ecma-262/5.1/#sec-15.4.1 15.5.1, 15.5.2, String ecma-international.org/ecma-262/5.1/#sec-15.5.1 15.6.1, 15.6.2, Boolean ecma-international.org/ecma-262/5.1/#sec-15.6.1 15.7.1, 15.7.2, Number ecma-international.org/ecma-262/5.1/#sec-15.7.1
It addresses a common pattern today, that looks like:
function Led( opts ) { if ( !(this instanceof Led) ) { return new Led( opts ); }
// ... }
Yes - I explored this idea back when we were discussing classes several months ago. The thing that you have to consider is that sometimes users legitimately want to call [[Construct]] to initialize an existing object, and there needs to be a way to do that.
FWIW, we'd better stick with some reduced number of methods for meta-traps.
As it turns out, we already have several different techniques which solve nearly the same problems:
- Proxies,
- Object.observe
- Unstratified proposals like this one (which BTW, have been already discussed many times; IIRC, the last time we were talking about unstratified traps, it was said like "no one needs these Pythonish names, we need proxies". Although, I don't see nothing too bad if the traps will be defined directly on objects, especially if with some @"private" semantics which cannot be just read as a simple property).
What I'm saying -- better not to create too many the same entities for similar purposes. If we'll have some unstratified private hooks, I can predict no one will need proxies or Object.observe.
Dmitry
Kevin Smith wrote:
It addresses a common pattern today, that looks like: function Led( opts ) { if ( !(this instanceof Led) ) { return new Led( opts ); } // ... }
Yes - I explored this idea back when we were discussing classes several months ago. The thing that you have to consider is that sometimes users legitimately want to call [[Construct]] to initialize an existing object, and there needs to be a way to do that.
Firstly, there is super call which should invoke [[Construct]] and covers a lot of the cases for this, I presume.
And of course, there needs to be an API equivalent to call/apply (call-esque is enough now that we have spread).
Dmitry Soshnikov wrote:
FWIW, we'd better stick with some reduced number of methods for meta-traps.
As it turns out, we already have several different techniques which solve nearly the same problems:
- Proxies,
- Object.observe
- Unstratified proposals like this one (which BTW, have been already discussed many times; IIRC, the last time we were talking about unstratified traps, it was said like "no one needs these Pythonish names, we need proxies". Although, I don't see nothing too bad if the traps will be defined directly on objects, especially if with some @"private" semantics which cannot be just read as a simple property). What I'm saying -- better not to create too many the same entities for similar purposes. If we'll have some unstratified private hooks, I can predict no one will need proxies or Object.observe.
I still have the feeling proxies are too big bullet for this. I see the place of proxies in meta-programming pre se, simulating whole protocals, filling missing properties not envision before etc.
To use a proxy to do such a natural thing as splitting [[Call]] and [[Construct]] seems to be a bit of misuse. For me.
But if there is a "no init" movement there, ok. I just pointed to that fact of @iterator, and honestly I think @call and @construct are on the same level - enabling declaration of behaviour for basic, not-so-meta, language constructs (be it for-of or new/super).
Herby Vojčík wrote:
Dmitry Soshnikov wrote:
FWIW, we'd better stick with some reduced number of methods for meta-traps.
As it turns out, we already have several different techniques which solve nearly the same problems:
- Proxies,
- Object.observe
- Unstratified proposals like this one (which BTW, have been already discussed many times; IIRC, the last time we were talking about unstratified traps, it was said like "no one needs these Pythonish names, we need proxies". Although, I don't see nothing too bad if the traps will be defined directly on objects, especially if with some @"private" semantics which cannot be just read as a simple property). What I'm saying -- better not to create too many the same entities for similar purposes. If we'll have some unstratified private hooks, I can predict no one will need proxies or Object.observe.
I still have the feeling proxies are too big bullet for this. I see the place of proxies in meta-programming pre se, simulating whole protocals,
s/pre se/per se/
On Mon, Oct 8, 2012 at 1:04 AM, Herby Vojčík <herby at mailbox.sk> wrote:
Dmitry Soshnikov wrote:
FWIW, we'd better stick with some reduced number of methods for meta-traps.
As it turns out, we already have several different techniques which solve nearly the same problems:
- Proxies,
- Object.observe
- Unstratified proposals like this one (which BTW, have been already discussed many times; IIRC, the last time we were talking about unstratified traps, it was said like "no one needs these Pythonish names, we need proxies". Although, I don't see nothing too bad if the traps will be defined directly on objects, especially if with some @"private" semantics which cannot be just read as a simple property). What I'm saying -- better not to create too many the same entities for similar purposes. If we'll have some unstratified private hooks, I can predict no one will need proxies or Object.observe.
I still have the feeling proxies are too big bullet for this. I see the place of proxies in meta-programming pre se, simulating whole protocals, filling missing properties not envision before etc.
To use a proxy to do such a natural thing as splitting [[Call]] and [[Construct]] seems to be a bit of misuse. For me.
But if there is a "no init" movement there, ok. I just pointed to that fact of @iterator, and honestly I think @call and @construct are on the same level - enabling declaration of behaviour for basic, not-so-meta, language constructs (be it for-of or new/super).
Notice though, that class's constructor(...) method (or @construct as you propose) does not reflect [[Construct]]. The constructor method is just the constructor function code itself. I.e. the only thing it does is initializes already created instance. So we cannot control instance allocation in here, etc (this is what [[Construct]] does and this is how it works in proxies).
Although, I agree, if @iterator is on the objects/classes, then why not other special traps?
Other languages, BTW, like Ruby, allows separation of trapping allocation
and initialization steps. There classes' new
method is just a wrapper
over allocate
(ES's [[Construct]] and initialize
(already user-level
constructor function code).
I could easily imaging things like this:
class Foo {
// meta-level hh
// just a wrapper @new(...args) { this. at allocate(...args); this. at constructor(...args); }
// aka [[Construct]] @allocate(...args) { // own allocation }
@construct(...args) { // user-level init }
@call(...args) { // simple call }
@iterator(...args) { ... }
// user-level methods
fromBar(bar) { // calls Foo. at new.call(this, bar); return new Foo(bar); }
// ETC.
}
Though, that's said, then we'll have several the same semantically entities (proxies, unstratified traps, Object.observe) with different approaches. And it's better to reduce them. I like unstratified @"private" hooks. Don't see the reason why should I use Proxy in this case (and how? -- by proxying what? -- prototype property of the class object manually? come on...)
Dmitry
I think we've been over this before. See the April thread "callable objects?"
Brendan at that point also proposed to use private names (we didn't then have the distinction between private and unique symbols): < esdiscuss/2012-April/022368>
I don't see any problem with this since the call and construct traps don't have any invariant checks associated with them. And I agree it would be weird if proxies were the only available tool to discern [[Call]] from [[Construct]]. Brendan gave more good reasons here: < esdiscuss/2012-April/022394>.
I think the important points w.r.t. invariants are:
- you can't override the built-in [[Call]] and [[Construct]] behavior of an ECMAScript function object.
- the result of the typeof operator shouldn't depend on the presence/absence of the @call/@construct unique symbols.
Cheers, Tom
2012/10/6 Brendan Eich <brendan at mozilla.org>
Dmitry Soshnikov wrote:
On Mon, Oct 8, 2012 at 1:04 AM, Herby Vojčík <herby at mailbox.sk <mailto:herby at mailbox.sk>> wrote:
Dmitry Soshnikov wrote:
Notice though, that class's constructor(...) method (or @construct as you propose) does not reflect [[Construct]]. The constructor method is just the constructor function code itself. I.e. the only thing it does is initializes already created instance. So we cannot control
What I propose is to use different code for [[Call]] and instance-initialization in [[Construct]]. Maybe I was mistaken in what [[Construct]] does, see below.
instance allocation in here, etc (this is what [[Construct]] does and this is how it works in proxies).
Although, I agree, if @iterator is on the objects/classes, then why not other special traps?
class Foo {
// meta-level hh
// just a wrapper @new(...args) { this. at allocate(...args); this. at constructor(...args); }
// aka [[Construct]] @allocate(...args) { // own allocation }
@construct(...args) { // user-level init }
@call(...args) { // simple call }
@iterator(...args) { ... }
// user-level methods
fromBar(bar) { // calls Foo. at new.call(this, bar); return new Foo(bar); }
// ETC.
}
This seems too complicated, very detailed granularity. I was in impression that
- allocation is done in new operator itself
- [[Construct]] is invoked inside new as well as in super
- [[Call]] is obvious.
I may have mistaken this. No problem if it is called @init instead of
@construct (maybe even introducing [[Init]] thereby making [[Construct]]
and super running the same initialization code by referring to
[[Init]]). What I wanted to achieve, is to separate [[Call]] from
instance-initialization of new/super declaratively, and allow to replace
the if (!(this instanceof Foo))
method (which fails if I call it with
already initialized Foo instance, anyway).
Seems to me like a nice little clean. And in par with replacing
SuperFoo.apply(this, arguments)
with super(...arguments)
.
On Mon, Oct 8, 2012 at 1:20 PM, Herby Vojčík <herby at mailbox.sk> wrote:
Dmitry Soshnikov wrote:
On Mon, Oct 8, 2012 at 1:04 AM, Herby Vojčík <herby at mailbox.sk <mailto:herby at mailbox.sk>> wrote:
Dmitry Soshnikov wrote:
Notice though, that class's constructor(...) method (or @construct as you propose) does not reflect [[Construct]]. The constructor method is just the constructor function code itself. I.e. the only thing it does is initializes already created instance. So we cannot control
What I propose is to use different code for [[Call]] and instance-initialization in [[Construct]]. Maybe I was mistaken in what [[Construct]] does, see below.
instance allocation in here, etc (this is what [[Construct]] does and
this is how it works in proxies).
Although, I agree, if @iterator is on the objects/classes, then why not other special traps?
class Foo {
// meta-level hh
// just a wrapper @new(...args) { this. at allocate(...args); this. at constructor(...args); }
// aka [[Construct]] @allocate(...args) { // own allocation }
@construct(...args) { // user-level init }
@call(...args) { // simple call }
@iterator(...args) { ... }
// user-level methods
fromBar(bar) { // calls Foo. at new.call(this, bar); return new Foo(bar); }
// ETC.
}
This seems too complicated, very detailed granularity. I was in impression that
- allocation is done in new operator itself
- [[Construct]] is invoked inside new as well as in super
- [[Call]] is obvious.
I may have mistaken this. No problem if it is called @init instead of @construct (maybe even introducing [[Init]] thereby making [[Construct]] and super running the same initialization code by referring to [[Init]]). What I wanted to achieve, is to separate [[Call]] from instance-initialization of new/super declaratively, and allow to replace the
if (!(this instanceof Foo))
method (which fails if I call it with already initialized Foo instance, anyway).Seems to me like a nice little clean. And in par with replacing
SuperFoo.apply(this, arguments)
withsuper(...arguments)
.
The example above was just an example. Yes, in real ES semantics allocation
is done in the [[Construct]] which is called by new
. Then [[Construct]]
already applies the function (via [[Call]]) in the context of the created
instance.
What I thought though -- why do we need @call at all in this semantics? I'd rather disallowed calling classes at all at semantics level (aren't this only to cover this problem use-case with if (!(this instanceof Foo)) ?). If to reflect the behavior of embedded classes -- when you may call new Array(...) and just Array(), then I don't think so either. The only good use-case of calling native constructors as functions is the type conversion. To what exactly we want to convert the type when will call the class?
So probably to disallowing calls at all (with even early errors) may look better.
PHP, e.g. has __construct(...) and __call(...). The former is our constructor(...), but the later is not about class calling, but about missed methods catching (aka noSuchMethod).
So -- what are good use cases for providing call(...) for user-defined classes?
Dmitry
Dmitry Soshnikov wrote:
What I thought though -- why do we need @call at all in this semantics? I'd rather disallowed calling classes at all at semantics level (aren't this only to cover this problem use-case with if (!(this instanceof Foo)) ?). If to reflect the behavior of embedded classes -- when you may call new Array(...) and just Array(), then I don't think so either. The only good use-case of calling native constructors as functions is the type conversion. To what exactly we want to convert the type when will call the class?
So probably to disallowing calls at all (with even early errors) may look better.
So -- what are good use cases for providing call(...) for user-defined classes?
- They are max-min classes. Maximally minimal means it should restrict semantics as least as possible, so it can be used as a cornerstone for as much ways of future improvement as possible. @call and @construct do not restrict the behaviour if all options are left.
- As I posted a few posts before, sensible defaults should provide such restriction if wished:
- if both @construct and @call are present, distinguish the behaviour for new/super and plain call.
- if @call is present but @construct is not, let new/super reuse the code of @call
- if @construct is present but @call is not, new/super should work but plain call should throw.
Dmitry Soshnikov wrote:
Though, that's said, then we'll have several the same semantically entities (proxies, unstratified traps, Object.observe) with different approaches. And it's better to reduce them. I like unstratified @"private" hooks. Don't see the reason why should I use Proxy in this
What I in fact propose in not a hook, at least how I see it. Just the (sort-of) declarative way to produce good old constructor function (but which in its implementation can safely distinguish whether called by new/super or by plain invocation and act accordingly).
Thanks, Tom.
At this point, without a TC39 exception, even with unique symbols, I think this is a strawman (so ES7 at earliest). Anyone willing to write it up?
Brendan Eich wrote:
Thanks, Tom.
At this point, without a TC39 exception, even with unique symbols, I think this is a strawman (so ES7 at earliest). Anyone willing to write it up?
As a generic callable objects proposal, yes.
But I am still saying there is a max-min proposal, which only puts this semantics into max-min classes. Where it, imho, has its value, and is easy to implement.
This could be part of ES6, wouldn't it? And it could be also the first step to full-blown callable objects, should they appear later.
Hello,
now that we have symbols (and use some of them language-wise, like @iterator or proposed @toStringTag), I'd say we can probably dissect [[Call]] and [[Construct]] semantics fairly easily. Assuming there are global constant symbols @construct and @call, for min-max classes the issue is very simple:
class Foo { @construct(bar) { this.bar = bar; } @call() { // do something else } }
I'd propose the above as the minimal proposal for [[Call]] and [[Construct]] separation (even when it only works for classes; and so they can be abused to create function with [[Call]] and [[Construct]] separated). Implementation would be to create Foo such that it chooses one or the other based on whether [[Call]] or [[Construct]] is invoked.
It could go further, like this:
var f = { @construct() {console.log(1)}, @call() {console.log(2)} }; f(); // logs 2 new f; // logs 1
In case of "native functions" (function, =>) do [[Call]] and [[Construct]] as today, in case of objects, check for @construct / @call presence.
But this has some quirks to solve, like typeof must be "function" for every object having @call, but what about object having only @construct?
Thanks,