Proposal: Additional Meta Properties for ES7

# Allen Wirfs-Brock (10 years ago)

Here is a new proposal for some additional meta properties that should be considered for ES7 allenwb/ESideas/blob/master/ES7MetaProps.md

I've added this to the agenda for next months TC39 meeting but pre-meeting discussion is always welcomed right here.

# Claude Pache (10 years ago)

Alternative name for function.callee: I find that function.self sounds better in case of recursive call (and probably in other cases as well).

# Tab Atkins Jr. (10 years ago)

On Thu, Feb 26, 2015 at 3:27 PM, Claude Pache <claude.pache at gmail.com> wrote:

Alternative name for function.callee: I find that function.self sounds better in case of recursive call (and probably in other cases as well).

Agreed that "callee", while technically accurate, is a clumsy word and something like "self" sounds better. "function.self", in particular, seems to communicate the right semantic, and implies that you're receiving a function .

It doesn't feel... apt, I think, to bring the caller/callee relationship to mind when you're not trying to deal with that relationship; all you're trying to do is recurse on yourself, in the case where you don't have a name to refer to yourself with. The fact that you're running because someone else called you is irrelevant to this dynamic.

# Andrea Giammarchi (10 years ago)

I personally wouldn't mind self but it has been historically used as context or global reference, while callee ... you don't probably need to explain what's that about.

On the other hand, from arguments object, calleewas more semantic but it feels not th ebest choice from function indeed.

Decisions ... decisions ...

# Andrea Giammarchi (10 years ago)

agreed ... between self and callee probably self is better. Anyone with a different/new idea?

# Domenic Denicola (10 years ago)

function.current?

# Andrea Giammarchi (10 years ago)

had same thought ... that wouldn't need much explanation neither. +1 here

# Matthew Robb (10 years ago)

I think current implies something too dynamic and doesn't seem to carry the impression of identity

# Garrett Smith (10 years ago)

Can you show an example of how callee is used with a fat arrow function?

(()=>{alert(callee);})()

Thanks.

# Allen Wirfs-Brock (10 years ago)

Once you are willing to move beyond "callee", there are a whole lot of available English words:

function.me function.here function.this (I kinda like it but I also don't believe it would be a good choice) function.target function.self (suggested by Claude) function.object (you are accessing the active function function) function.active function.recur function.home function.enclosing

and no doubt many more...

# Allen Wirfs-Brock (10 years ago)

((n)=>n>1? n*function.callee(n-1) : 1)

# Tom Schuster (10 years ago)

I think it's easier to convey the message to never use "callee" instead use function.self.

# Matthew Robb (10 years ago)

I am positive that there will be good reasons I am just curious what they might be, why not: function(){ function(); }

  • Matthew Robb
# Matthew Robb (10 years ago)

Or the following three forms would be great:

// normal form function(); // or function.invoke();

// additionally function.call(); function.apply();

  • Matthew Robb
# Felipe Nascimento de Moura (10 years ago)

Am I the only one who finds it weird to use function.something inside a function?

function doSomething () {
    function.self...
}

That's why I was thinking in something more related to the "scope" than to the function object itself.

# Andrea Giammarchi (10 years ago)

another basic example with listeners

window.addEventListener('load', (e) => {
  // how am I going to remove this listener?
  console.log('everything loaded');
});

Specially on UI world, many libraries do attach tons of things at runtime, within methods, and have no way to remove them if not believing the DOM will take care of everything.

Timers are also less powerful


setTimeout(function later() {
  if (condition) {
    moveOn();
  } else {
    setTimeout(later, 100);
  }
}, 100);

Recursions as Allen wrote already are another example too.

# Leon Arnott (10 years ago)

I feel a little unhappy that one meta-property, new.target, is lexically scoped in arrow functions but these new ones aren't, without much obviously distinguishing them. I guess you could concoct the reasoning that since arrows can't be new'd, new.[meta-property] is "meaningless" in it - but that feels like the sort of explanation that makes more sense retrospectively.

On a different tack: I also feel like new.target has a significant readability advantage which function.[meta-property] sadly doesn't have: the new operator appears in (almost) all actions which set the value of new.target. Its visual continuity reinforces the meaning of the meta-property.

I think instead of function.[meta-property] it should really be ().[meta-property] - which means [call operator].[meta-property]. The round-bracket call operator is in many ways the counterpart to "new" (consider the coincidence that it is optional when "new" is used and no arguments are passed) whereas "function" is much less connected to the notion of calling, and is rapidly growing less connected to function object creation at all.

Also, consider the phrasing of the meta-properties: ().arguments now means "the arguments that the () was given", which to my ears rings clearer than function.arguments "the arguments that this function instantiation (not the function object) was given". (And do you not think it looks a little too similar to Function.arguments, which is semantically and syntactically completely different?)

I'm not sure if it's possible in the grammar for these proposed names to become ().callee, ().count, ().arguments etc. (insofar as the actual call and property access operators strongly resemble them), but I think it should be considered.

# Claus Reinke (10 years ago)

Can you show an example of how callee is used with a fat arrow function? ((n)=>n>1? n*function.callee(n-1) : 1)

meta-level tools are powerful, which makes them ever so tempting.

They are too powerful to be used for tasks for which current language-level tools are sufficient. Using a call-by-value fixpoint combinator

let fix = f=>x=>f(x=>fix(f)(x))(x)
undefined

we can use plain functional abstraction instead of meta properties

let f = self=>n=>n>1?n*self(n-1):1
undefined

fix(f)(6)
720

fix(f)(7)
5040

(if you're worried about optimization, provide a built-in 'fix')

For concise methods, the problem is already solved by 'this', isn't it?

({f(n){return n>1?n*this.f(n-1):1}}.f)(6)
720

Like most powerful tempting things, referring to meta-levels comes at a cost, even though that may not be immediately visible (ie no lexical scoping, cannot extract as part of a function body). So the easiest route (of introducing the most powerful feature) is not necessarily the best route.

You're still working to get rid of anomalies that hamper functional abstraction and composition (arrow functions help with 'this'; and wasn't the missing toMethod an attempt to handle the newly introduced 'super' special case?). I'm surprised to see everyone so eager to introduce new trouble.

just saying... :-) Claus clausreinke.github.com

# Andrea Giammarchi (10 years ago)

and yet you haven't removed any anonymous arrow listener. Assign first? Mostly nobody will do that, it's just less natural then obj.on(something, ()=>happening)

# Allen Wirfs-Brock (10 years ago)

On Feb 27, 2015, at 12:27 AM, Leon Arnott wrote:

I feel a little unhappy that one meta-property, new.target, is lexically scoped in arrow functions but these new ones aren't, without much obviously distinguishing them. I guess you could concoct the reasoning that since arrows can't be new'd, new.[meta-property] is "meaningless" in it - but that feels like the sort of explanation that makes more sense retrospectively.

Each meta property is its own special form with its own unique semantics. Of course, consistency among similarly named meta properties is nice, but we have to balance that against the fact that we have a quite limited set of keywords available that we can use for forming meta properties.

On a different tack: I also feel like new.target has a significant readability advantage which function.[meta-property] sadly doesn't have: the new operator appears in (almost) all actions which set the value of new.target. Its visual continuity reinforces the meaning of the meta-property.

I think instead of function.[meta-property] it should really be ().[meta-property] - which means [call operator].[meta-property]. The round-bracket call operator is in many ways the counterpart to "new" (consider the coincidence that it is optional when "new" is used and no arguments are passed) whereas "function" is much less connected to the notion of calling, and is rapidly growing less connected to function object creation at all.

The meta property syntactic pattern is: <reserved word> . <IdentifierName> . Replacing <reserved word> with a sequence special characters is something that was not discussed when we adopted that pattern for new.target. However, past attempts assign special meaning to special character sequences have not been well received.

# Allen Wirfs-Brock (10 years ago)

On Feb 27, 2015, at 7:59 AM, Claus Reinke wrote:

For concise methods, the problem is already solved by 'this', isn't it?

({f(n){return n>1?n*this.f(n-1):1}}.f)(6) 720

No, not for the general case. You could have arrived here via a 'super' method call in which case 'this.f' will take you back to a subclass' f rather then recurring on this specific function

# Claus Reinke (10 years ago)

For concise methods, the problem is already solved by 'this', isn't it?

({f(n){return n>1?n*this.f(n-1):1}}.f)(6) 720

No, not for the general case. You could have arrived here via a 'super' method call in which case 'this.f' will take you back to a subclass' f rather then recurring on this specific function

Sometimes, this might be what you want in such a case. If it isn't, then how about:

class Super { f(n) {return n>1?n*Super.prototype.f(n-1):1 }}

class Sub extends Super { f(n) { return 1+super.f(n) }};

console.log((new Sub()).f(5));  // 121, rather than 326

Claus

# Claus Reinke (10 years ago)

and yet you haven't removed any anonymous arrow listener. Assign first? Mostly nobody will do that, it's just less natural then obj.on(something, ()=>happening)

personally? Yes, I tend to assign listeners somewhere, at least when I intend to remove them later. I've even been known to assign them to a virtual event object, so that I could translate the event names later (eg, click vs touch). But that is just me.

One could also hide the assignment in an on helper (JQuery does something similar).

function on (obj,event,listener) {
    obj._events[event]=listener;
    return obj.on(event,listener);
}

Claus

# Andrea Giammarchi (10 years ago)

nope, you are limiting your object to have only one listener per event, I think that's not quite how reality is. You gonna lose that listeners next time somebody use same name with the same object.

In (?:io|node)js you have EventEmitter that exposes .on, on web even jQuery needs that reference in order to be able to remove only that listener instead of many.

Sure you have .once or .one that might help here, but removing a listener can also be performed for other reasons.

If there's no way developers will find their own, but it makes arrow function less attractive, at least to my eyes

# Claus Reinke (10 years ago)

nope, you are limiting your object to have only one listener per event, I

think that's not quite how reality is. You gonna lose that listeners next time somebody use same name with the same object.

true. For cases where that isn't enough, i assume you're thinking of canceling from within the handler.

Here goes another attempt, preserving identity while providing a self-reference.

let arg = ff=>{ let arg = {}; let f = ff(arg); arg.callee = f; return f; }; let f = arg=>n=>n>1?n*arg.callee(n-1):1;

console.log(arg(f)(5));

Perhaps i'm going to run out of ideas soon, but my point is that it is worth looking for less powerful alternatives that achieve the same ends. Else we'd all be writing lisp, right?-)

Claus

# Andrea Giammarchi (10 years ago)

I think the fact you had to write two solutions where one attach to the object the listener and another one needs a double arrow (rainbow) already shows we have a hole in the language once covered by arguments.callee.

The more you write examples, the more you convince me we are missing callee ;-)

Again, this is jut my opinion. There are ways to work around this, it just feels wrong we lost the callee feature.

# Claus Reinke (10 years ago)

I think the fact you had to write two solutions where one attach to the object the listener and another one needs a double arrow (rainbow) already shows we have a hole in the language once covered by arguments.callee.

just to make sure we are not misunderstanding each other: I wrote two solutions because the initial examples I saw only needed recursion for anonymous functions, a problem which has standard solutions.

Your example had more stringent conditions in that the self-reference also needed to preserve the identity of the anonymous function (for de-registering listeners). The modified solution should serve both examples as a replacement for arguments.callee (I think).

The only example it hasn't solved is the one with concise methods, where the syntactic sugar keeps us from wrapping the function at the definition site. Since wrapping functions at call-sites is awkward, I suggested alternatives.

The more you write examples, the more you convince me we are missing callee ;-)

Again, this is jut my opinion. There are ways to work around this, it just feels wrong we lost the callee feature.

We just got rid of the 'this' workarounds, and it cost us a whole second set of function expressions. We still haven't solved all of the 'super' issues. Do you really want to multiply these issues by introducing yet more implicitly scoped meta-level references?

Everyone is welcome to their opinions, but I'd rather avoid taking the bait of minor conveniences, only to run into solid issues later.

To me, this looks like one of the cases where language designers have to be more careful than language users in what they wish for.

My reference to Lisp was only half kidding: Lisp was born from meta-level concepts, so everything was possible, programs talking about and rewriting themselves were cool and seemed to offer easy solutions to everything; later languages like Scheme, ML, and Haskell have largely followed a path of trying to achieve comparable (or better) expressiveness while reducing the reliance on meta-level (and other too powerful) features.

Their language designers had to work hard to get there while avoiding the seemingly simple path, but the result is that it is much easier to reason about and refactor a Haskell program than the equivalent Lisp program (which also makes optimization easier, which makes seemingly complex features cheap to use).

Ok, getting off my soapbox now, sorry for the interruption:-) Claus

# Brendan Eich (10 years ago)

Just to be clear, I agree with you that we should not rush to add special forms where combinators may suffice. However:

Claus Reinke wrote:

We just got rid of the 'this' workarounds, and it cost us a whole second set of function expressions.

We would have had this problem anyway. When I did JS in a tearing hurry, I overloaded procedures, methods, constructors, and lambdas on poor old function. Methods require some kind of this binding, whether as in JS or based on classes as types as in Java. Lambdas want TCP upheld for this, as in ES6 arrows. So in the end we'd have a "second" kind of functional form, no matter how you skin the cat.

We still haven't solved all of the 'super' issues. Do you really want to multiply these issues by introducing yet more implicitly scoped meta-level reference

Agreed.

# John Lenz (10 years ago)

I was recently discussion Promise optimizations (specifically, that in any "then" chain at least one Promise object is created that goes unused), this could be solved by some metadata that indicated whether the result of the function would be consumed:

Promise.prototype.then = function(a, b) { ... if (function.called_from_void_result_context) { return undefined; } /* current behavior */ ... };

// example

somePromise.then(); // no chaining promise created by then

var x = somePromise.then(); // chaining promise created x.then(); // no chaining promise created

I'm not really sure what impact this would have on the VMs but I found it an interesting idea.

# Mark S. Miller (10 years ago)

That is an interesting idea. In E's when-catch construct, which is the direct ancestor of JS's .then, the when-catch construct is syntax whose expansion to Kernel-E was dependent on whether the when-catch construct statically occurs in a value consuming context. E is an expression language, but in JS terms, this is essentially the distinction between an expression-statement and other expression contexts, as in your example.

Likewise, the E message send was syntax, as in the proposed-for-ES7 infix ! syntax strawman:concurrency

expanded differently depending on this context difference. (In E it is "<-" which doesn't work for JS because "x <- y" already parses as "x < -y".) In E, the equivalent of "x ! m(y)" would expand to either the equivalent of "x.send('m', y)" as shown on that page, when it occurs in a value consuming context, or the equivalent of "x.sendOnly('m', y)" which differs in not creating a result promise. With your proposal, the one .send method could do both jobs by using this test.

The motivation was 1) to avoid the creation of the extra local promise as you state, and 2) in the distributed case, to use the simpler distributed invocation protocol which avoids the need to create a new remote promise for the eventual remote result. < erights.org/elib/distrib/captp/DeliverOp.html> vs <

erights.org/elib/distrib/captp/DeliverOnlyOp.html>. #1 is a nice

optimization. #2 is a really important one. Before your suggestion, I didn't have a good idea how to reintroduce it to JS promises.

Nice!

# Mark Miller (10 years ago)

On Mon, Mar 2, 2015 at 9:30 AM, John Lenz <concavelenz at gmail.com> wrote:

I was recently discussion Promise optimizations (specifically, that in any "then" chain at least one Promise object is created that goes unused), this could be solved by some metadata that indicated whether the result of the function would be consumed:

Promise.prototype.then = function(a, b) { ... if (function.called_from_void_result_context) { return undefined; } /* current behavior */ ... };

More controversially, we might consider

if (function.called_from_void_result_context) {
  /* proposed .done behavior */

I'm not sure what I think of this. It is kinda scary, but it might be the right thing.

# Allen Wirfs-Brock (10 years ago)

On Mar 2, 2015, at 9:30 AM, John Lenz wrote:

I was recently discussion Promise optimizations (specifically, that in any "then" chain at least one Promise object is created that goes unused), this could be solved by some metadata that indicated whether the result of the function would be consumed: ... if (function.called_from_void_result_context) { ... I'm not really sure what impact this would have on the VMs but I found it an interesting idea.

Essentially, this means that an additional implicit parameter ("calledForValue") needs to be added for every function call. At the MOP level this would manifest as an extra argument to the [[Call]] internal method. It could be defined to default to true and implementation could probably optimize for the default case. But, at least conceptually it requires passing additional information across the call boundary. that might be a bit much if this is only about optimizing promises.

# Mark Miller (10 years ago)

On Mon, Mar 2, 2015 at 10:45 AM, Allen Wirfs-Brock <allen at wirfs-brock.com>

wrote:

On Mar 2, 2015, at 9:30 AM, John Lenz wrote:

I was recently discussion Promise optimizations (specifically, that in any "then" chain at least one Promise object is created that goes unused), this could be solved by some metadata that indicated whether the result of the function would be consumed: ... if (function.called_from_void_result_context) { ... I'm not really sure what impact this would have on the VMs but I found it an interesting idea.

Essentially, this means that an additional implicit parameter ("calledForValue") needs to be added for every function call. At the MOP level this would manifest as an extra argument to the [[Call]] internal method. It could be defined to default to true and implementation could probably optimize for the default case. But, at least conceptually it requires passing additional information across the call boundary. that might be a bit much if this is only about optimizing promises.

Allen, sadly, I expect you are correct about this.

Infix ! could, nevertheless, statically expand to .send or .sendOnly depending on static syntactic context, without any further runtime implication, as in E.

But .then, being a normal method call rather than distinct "when" syntax, would not be able to gain a similar benefit.