New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Better infer the name of constant-like function expressions #1367
Comments
Function assigned to variable, property or somewhere else is not kind of things that can be safely optimized since those can be easily, incl. conditionally, reassigned in random places. Tracking all those places would be almost equal to interpreting entire codebase which would be painfully slow, so this is left as-is. Power of transpilers is kinda limited :) |
This is intentional. You'd need to deopt on reassignments to the binding anyway which would confuse the shit out of people when it doesn't work. This doesn't really make it smarter, it just makes it less dumb. Which is fine but the cost of additional magic isn't worth it. |
You're saying you can't optimize the function that gets passed into the named wrapper? That would not rely on the outer assignment... would it? |
No, it could be optimised but it will lead people to believe that Babel is smarter than it is. For example:
Could be handled and detected as a recursive tail call but then people will think it's smart as shit and start throwing dynamic code at it and expect it to work. |
hmm.. this case is also broken for |
Not broken :) Just not implemented. Up for a PR? |
How could it possibly be more confusing than silently failing to optimize the tail call, resulting in exploding stack frames? =) |
I know that transpilers have their limits, but spec not implemented = broken from the user's perspective. ;) |
I'm not sure I have time for a PR, but if you leave it to me, I'll optimize the function that gets passed into the wrapper, which you've already said you don't want. If I'm gonna fix something, I'll fix it right. ;) |
Why are you being so coarse? |
Sorry, I meant it in a playful tone. I'm not trying to be insulting. I find the current behavior very surprising, and I would consider the conditional reassignment to be an edge-case and an anti-pattern, anyway. =) In other words, if the decision were up to me, I'd succeed for the common case and fail for the edge case (and mention the limitation in the docs). |
No worries. “Tail call recursion optimisation only works for named functions" is easier to explain than “tail call recursion optimisation only works on named functions and on locally bound constant inferred named functions." On Tue, Apr 28, 2015 at 12:12 PM, Eric Elliott notifications@github.com
|
Fair enough. I guess if I don't have time to commit a sensible PR, I don't have time to complain, either. ;) Cheers! |
Actually, this should automatically work if the |
The |
Released as of 5.2.0, thanks! |
Yay! That's a great start, but the method literal form still does the bullshit function wrapper, which falls on its face:
const repeater = {
name: "Repeater",
types: [
{ f: 'function' },
{ n: 'number' }
],
repeat: function (f, n) {
if (typeof f === 'function') {
f();
} else {
throw new Error('repeat: A Function is required.');
}
if (!n) {
return;
}
n -= 1;
return repeat(f, n);
}
};
repeater.repeat(function repeat() {console.log('foo');}, 10); Compiles to: 'use strict';
var repeater = {
name: 'Repeater',
types: [{ f: 'function' }, { n: 'number' }],
repeat: (function (_repeat) {
function repeat(_x, _x2) {
return _repeat.apply(this, arguments);
}
repeat.toString = function () {
return _repeat.toString();
};
return repeat;
})(function (f, n) {
if (typeof f === 'function') {
f();
} else {
throw new Error('repeat: A Function is required.');
}
if (!n) {
return;
}
n -= 1;
return repeat(f, n);
})
};
repeater.repeat(function repeat() {
console.log('foo');
}, 10); Output:
|
@ericelliott Nope, that's correct behaviour. Object literal concise methods don't create a lexical binding like normal named function expressions. The function wrapper is only used in one scenario, and that's where you reference a binding that isn't actually bound to the current file, this means it's exposed as a global and you need to explicitly reference it by the same identifier for it to work since you obviously can't rename it. |
Wait... WHAT? Could you link me to where it says that in the spec, 'cause I must be reading it wrong:
Are you trying to tell me that this is correct behavior, too? (Note that it's NOT a concise method so presumably, your response does not apply): const repeater = {
name: "Repeater",
types: [
{ f: 'function' },
{ n: 'number' }
],
repeat: function (f, n) {
if (typeof f === 'function') {
f();
} else {
throw new Error('repeat: A Function is required.');
}
if (!n) {
return;
}
n -= 1;
return repeat(f, n);
}
};
repeater.repeat(function repeat() {console.log('foo');}, 10); Output:
I'm really confused about how you could think this is spec-compliant behavior. It seems pretty clear to me that it's a bug. |
Actually yes - why do you think |
@getify did an entire article on it which was spawned from the discussion about why Babel inferred function names in the first place.
Where do you think |
@ericelliott Unsure why you referenced step 4, it says nothing about the |
In ES5 it wouldn't. But in ES6, for all function expressions, regardless of context, whether it's let, const, var, or method: this:
is equivalent to this:
And if you disagree with that assertion, instead of just saying, "no, you're wrong", please quote and link to the text in the spec that says different.
I referenced step 4 because concise methods should conform to Runtime Semantics: DefineMethod. |
It's not. Why do you think so? It would be a backward-incompatible change which could break some code.
If you say Babel is not following the spec, please quote the part of spec which proves that. |
Isn't this exactly what I'm trying to say? You are the one who brought up bindings. You don't need a binding to infer a function name, right? |
No, you're the one saying that const repeater = {
name: "Repeater",
types: [
{ f: 'function' },
{ n: 'number' }
],
repeat:
function (f, n) { <--- This function expression is **not** named
if (typeof f === 'function') {
f();
} else {
throw new Error('repeat: A Function is required.');
}
if (!n) {
return;
}
n -= 1;
return repeat(f, n); <-- `repeat` is not defined.
}
};
repeater.repeat(function repeat() {console.log('foo');}, 10); |
Here are some more examples: var foo = "foo";
var bar = "bar";
var obj = {
foo: function () {
return foo;
},
bar() {
return bar;
}
};
console.log(obj.foo()); // result is "foo", it does not refer to the function
console.log(obj.bar()); // result is "bar", it does not refer to the function |
Isn't it spelled out that the method name is inferred by the spec here, or am I missing something?
https://people.mozilla.org/~jorendorff/es6-draft.html#sec-setfunctionname |
No, it just tells to set internal
is still untrue. |
I think we can all agree that what the spec definitely does not say is that you should create a "bullshit function wrapper" (not my words) around function expressions as Babel currently does for methods: var repeater = {
name: 'Repeater',
types: [{ f: 'function' }, { n: 'number' }],
repeat: (function (_repeat) { // <--- WTF IS THIS FOR? vvv
function repeat(_x, _x2) {
return _repeat.apply(this, arguments);
}
repeat.toString = function () {
return _repeat.toString();
};
return repeat;
})(function (f, n) { // <--- WTF IS THIS FOR? ^^^
if (typeof f === 'function') {
f();
} else {
throw new Error('repeat: A Function is required.');
}
if (!n) {
return;
}
n -= 1;
return repeat(f, n);
})
}; |
@ericelliott Let's be nice, ok? Making statements in offensive manner doesn't make them any better or any more true. |
You are missing something. Here are the steps that initialize the local binding for named function expressions: 14.1.20 Runtime Semantics: Evaluation
Note steps 7 and 11 which don't exist for inferred function names.
Dude, drop the attitude. You're being extremely foul and nasty and are making this an extremely hostile discussion. |
I'm sorry, I'm not trying to be hostile, I'm just trying to understand how you could possibly think that method assignment function expressions should be treated any differently than any other function expression. RE: "bullshit function wrapper" <--- I am not the one who named it that. RE: "definitely does not say" <--- this isn't attitude, AFAIK, it's a simple statement of fact. Back to business:
In the context of an object literal, is BindingIdentifier not the same thing as the left hand identifier in the literal? If you read it differently, could you please explain why you read it differently? again, referring to specific lines in the spec that I can read in order to try to get on the same page with you? |
Please forgive my lack of communication skills and walk me through the logic that leads you to believe this:
...because we're reading the same specification, and I'm coming to a very different understanding of it. |
You seem to have gotten offended because I called it a "bullshit function wrapper". Not sure why.
Not going to have this philosophical argument about programming semantics and layers of abstraction and what justifies following semantics and what doesn't.
The steps I quoted are purely for named function expressions, their semantics do not matter since we are not talking about named function expressions. I only quoted them because you're confusing the semantics of named function expressions with that of function name inference. Function name inference in ES6 does not create bindings. I assume you obviously know what a binding is but if not it's something that creates a "declaration".
Your ultimate mistake is assuming that var bar = {
foo: function () {}
}; It does not. If you bring up your previous comment of:
No, you were when you implied that function name inference created bindings, it does not. Plain and simple. You keep on asking us to quote steps when you can't quote something that doesn't exist :) Feel free to quote the steps that lead you to the impression that you have and I'll gladly point you in the right direction. To go back over the methods that you have quoted so far: No mention of creating a binding.
This has nothing to do with bindings and is simply setting the scope to the current environment record which is completely irrelevant to magically creating bindings based on function name inference. |
I think we're getting lost in irrelevant details. Here's my logic. Please point out my errors. I assert:
Evidence: prop names are set by concise methods, SetFunctionName is called with prop name as value:
Evidence: SetFunctionName does lead to local binding at method evaluation time This works in every major browser & Node: var a = {
bar: function foo() {
console.log(typeof foo);
}
}; Output:
But Babel does this: const repeater = {
name: "Repeater",
types: [
{ f: 'function' },
{ n: 'number' }
],
repeat (f, n) {
if (typeof f === 'function') {
f();
} else {
throw new Error('repeat: A Function is required.');
}
if (!n) {
return;
}
n -= 1;
return repeat(f, n);
}
};
repeater.repeat(function () {console.log('foo');}, 10);
Again, I ask, what am I missing? Last time, you answered:
This doesn't explain anything to me, because as I read the spec, concise methods don't have to infer anything. The prop name is the function name, and the function name is available in the function's environment. |
This is where you're wrong. 9.2.11 SetFunctionName (F, name, prefix)
All As I mentioned before in this comment, the reason that a var a = {
bar: function foo() {
console.log(typeof foo);
}
}; is because of steps 7 and 11, not because of step 10 in: 14.1.20 Runtime Semantics: Evaluation
|
So your argument is that this evaluation signature:
... does not apply at all because const a = {
foo() {
}
} ... is not interpreted as a BindingIdentifier... and the value of the Hence: We're on the same page so far? I assert that in practice in all current ES5 engines, a function's
And in ES6, the spirit of the And that the spirit of the concise method is to provide the same semantics for methods, complete with a I also assert that if that's not how you interpret the spec, you're ignoring the context of the origins of the function I also assert that ignoring that context will astonish users of Babel. I've run out of time today to dig around deeper inside the guts of the spec to discover whether or not I've missed anything that would help my argument, but rest assured, I will return better armed to make my case -- and if we decide that the spec is indeed written as you suggest it is, I'll make my best effort to get the spec changed. I know it's probably far too late for ES6, but maybe we can fix it for ES7 so user's don't get frustrated as I have been, and several others I know of (including Kyle Simpson who authored an O'Reilly book on the subject of the ES6 spec). Thank you for your patience. |
Your interpretation of the spec is simply wrong, I'm not sure what you are talking about with respect to "context of the origins of the name property". You can try this in latest Chrome or iojs 2.0 if you want, as shorthand method syntax has moved out of V8 staging:
|
Well damn. I'm still quite confident that this will be a source of bugs and frustration for developers who are trying to build apps with it. I'm not the first to notice this issue, and I won't be the last. It may be time to move this to ES Discuss so we can think about sorting it out in ES7. Thanks for putting up with me. I really appreciate it. |
I've been bitten by this but when I figured out why I said, oh well that actually makes sense. |
Yeah, makes sense after my hand-held tour through the ES6 spec, at least. ); But I believe this post is still relevant: Can somebody explain to me why we need that function wrapper? |
@ericelliott This blog post does a decent job of explaining this whole issue, including your spec complaints and addressing your question: http://blog.getify.com/es6-concise-methods-lexical-or-not/ By adding the infered function name to the method function expression, it adds the name binding to the function (as established above). But that could break the code since it might be relying on an outer definition of that particular identifier. The wrapper function avoids that issue by wrapping the function in a named function that is defined in a separate scope. That way the function still has the proper outer scoping, which also having a name. |
Thanks. =) I guess I didn't see much point in the The problem is, I always want the name inside the function, just to avoid the confusion of finding that it's inconsistently missing in the concise method, even though the name is available in every other form that looks like a function expression. I might just disable concise methods in my ESLint config for this reason. Which totally sucks, 'cause I love the idea of them. ); |
To me, method syntax implies that the function is a really attached to one particular object in a more OO style, so I'd expect a recursive call to be done via What are the "every other form" you mean? |
Yeah, that "disables TCO" is a bummer. ); const repeat = function (f, n) {
if (typeof f === 'function') {
f();
} else {
throw new Error('repeat: A Function is required.');
}
if (!n) {
return;
}
n -= 1;
return repeat(f, n);
};
repeat(function () {console.log('foo');}, 10); // Works great. Yay! The same holds for let, var... in these forms, And that's the assumption that led me to read the spec wrong. I was looking at sigh I will admit defeat, and simply add concise methods to my "bad parts" collection. Or maybe I can come up with a crazy lint rule that will look for references to the method name inside the method and flag it as an error. That way we could catch it at build time and still use concise methods for everything else. =) |
use the optional transformer |
Thanks! That catches the error, but doesn't explain why it's undefined or how to fix it. I still think it's worth having a dedicated rule. =) |
Now there's a custom ESLint rule for this issue. 👍 |
FYI: that eslint rule appears to completely misunderstand the issue with lexical name bindings and inferences. I would avoid it until the misunderstanding and incorrect logic are corrected. |
See this gist for details.
The un-optimized function body gets moved as-is into a parameter that's passed into a wrapper function so that the function can be wrapped with a named function declaration...
I wonder if other ES6 things are broken by name wrappers, too?
Expected:
Tail calls should still be optimized.
The text was updated successfully, but these errors were encountered: