function hoisting like var
Ingvar von Schoultz
ingvar-v-s at comhem.se
Sat Jul 26 16:13:40 PDT 2008
Sorry about the length of this, but I'm trying to cover the
unclear things, and often I don't know which things are unclear.
Brendan Eich wrote:
> On Jul 26, 2008, at 2:07 PM, Ingvar von Schoultz wrote:
>
>> You can't get away from supporting this:
>>
>> {
>> function a(){}
>> var b = a;
>> }
>>
>> ES4 is planning to support function declarations locally
>> bound in blocks, so the above is valid ES4 code.
>>
>> What you see above is function b() hoisting like var.
>>
>> (I said b, not a.)
>
> What you said does not make sense. It's true that var b is hoisted to
> the top of the program or function body. But it is not initialized
> until control flows through the assignment b = a that is part of the
> var declaration. So there is no capture problem.
That's my point! There isn't any capture problem. That's exactly
what I'm showing here. And, more importantly, you can't insert a
capture problem while keeping the structure intact. The arrangement
is inherently well-behaved.
I'm trying to show that you can support functions that hoist like
var in a well-behaving way, and that this is /not/ complicated.
And the snippet is intended as proof.
But perhaps I should interpret what you said somewhat differently.
The snippet, as shown here, does not have a capture problem.
But if you "improve" the hoisting carelessly, there's a threat
of a capture problem lurking within it. (That's assuming that
I understand "capture problem" correctly -- this is a rare
case of Wikipedia not explaining a computing term.)
Somebody might want to "improve" the hoisting by making it so
that the function is assigned (is callable) from before we enter
the global scope.
Let's assume, for the argument, that the above code magically
starts to behave that way. Then it will work fine as long as it
stays the same. But later the programmer might decide to add a
local variable:
{
function a(){}
var b = a;
let c = 3;
}
c is now visible from inside a(), but exists only when we enter
the block. So in this situation a() must not be called before
we enter the block. The function must no longer be assigned
(callable) at the top of the global scope.
I would consider it extremely surprising semantics if I can call
a() above the block, but only before I add that local variable,
and this suddenly changes just because I add c. It's a huge change,
it's far away, and it's unrelated.
So for the sake of consistency and predictability we must assign
|undefined| whenever the code structure allows a capture problem
to be inserted as a side effect of doing something unrelated,
like adding |let c|.
That's why I said this, in the email where I first showed this
snippet:
,-------
| Assigning |undefined| is correct for any function whose
| assignment depends on sequential code. The above is such a
| sequential dependency, even though it may not look that way.
`-------
It may not look sequential, but that's just because I left out
a lot of details, in an attempt to keep it as brief as ever
possible, to minimize any misunderstandings.
As you can see, in my opinion, what I'm saying does make perfect
sense!
The claim that just because of this limitation my proposal
"doesn't work" is in my opinion quite mistaken. I consider
this functionality fully acceptable. It's simple, understandable
and predictable. Sure it's a limitation, but a minor one. It's
/far/ better and more useful and intuitive than having functions
not hoisting out at all.
The usual use case will be an if() or some such. Then the
programmer fully expects the function to be available only
afterward. Very useful. Using it in a bare block like the
above will be unusual, but if used, the rules are simple.
>> There is no far-too-complicated split-scope complexity. There
>> is no capturing of variables that haven't been declared yet.
>> It's simple, intuitive, well-defined and well-behaved.
>
> Thanks, I agree. But it is not what you proposed.
Sorry, I don't understand. What is not what I proposed, and
in which one of my proposals did I not propose it?
> Again, from
> Waldemar's original reply, but with your proposed {{}} interpolated
> and the elided code amended to say what the consequence is:
>
> // outer scope
> function c() ...;
>
> // inner scope
> {{
> if (foo) {
> const c = 37;
> }
> ... c in your proposal must be hoisted to the {{,
> so it can't be function c -- yet it can't be
> initialized to 37 if foo is "falsy" ...
> }}
Yes, c exists in the inner scope {{ }}. It exists there from
before you enter the scope and throughout. It shadows the outer
c() throughout. If foo is false, the constant c is never initialized.
> You could reply that const is new (sort of -- two browsers already
> implement it one way, another treats it as var) and therefore should
> always scope to { or {{, whichever is closer. But the point stands if
> you replace const with function or var and hoist to the {{.
Which point? Forgive me, I didn't see any point. I don't see any
problem or disadvantage with that uninitialized c. It's what the
programmer coded.
It's also how |var| works. In my opinion, |var| and |const| should
have exactly the same behavior, except for the constantness.
It looks perfectly fine to me. So please explain.
> Repeating
> the next counter-example, with {{}} changes again, to track your
> proposal since the original exchange with Waldemar:
>
> // outer scope
> function c() ...;
>
> // inner scope
> {{
> function f() {
> return c;
> }
> a = f();
> if (foo) {
> const c = 37;
> }
> b = f();
> ... just what do a and b hold here? Was f's captured
> variable rebound by the if statement? ...
> }}
c is a constant that is visible throughout the inner scope
{{ }}, visible from before you enter the scope and throughout.
It shadows the outer function c() throughout. It is initially
unassigned, or assigned |undefined|.
When you call |a = f()|, the function f() accesses this
unassigned c. I don't know if there's a consensus about what
should happen when you access an unassigned constant. It
seems to me that this should raise an error. But my only
reason for saying this is that if it doesn't, the "constant"
becomes a one-shot binary toggle, and a toggle isn't a
constant. So it's a detail of intuitive semantics and
consistency.
If instead it's specified that an unassigned constant returns
|undefined|, then a will contain |undefined| and the program
continues. Then |if (foo)| may or may not allow c to get its
only-once-permitted assignment.
I don't know why you mention rebinding. Maybe I've misunderstood
something. But as I see it, it's just an assignment, just as if
c were a var, except you can only assign once. You ask how f()
sees it (I think). Well, f() sees it just like it sees any var
in that scope. I'm not sure this answers your question, I hope
it does.
After that, b will receive whatever c contains, either |undefined|
or 37.
A different notion of constant is possible, where earlier assignment
is enforced, or maybe even hoisted to the beginning of the scope.
But I think the constant that I described is a /much/ better fit for
this language.
I hope the above description is sufficient.
What's this talk about rebinding? Waldemar asked about rebinding,
and unless I misunderstood, rebinding was at the basis of the
strange things that he thought that I was proposing. I have not
intended to propose any kind of rebinding. If there is any kind of
rebinding inherent in any one of my proposals, I'm not aware of it.
Maybe I'm misunderstanding something.
I can't say anything more than that about this rebinding for now,
but I can say this. Think about var declarations:
alert (a);
var a = 10;
alert (a);
This alerts undefined and 10. If you remove |var a = 10| it will
instead raise an error. That's because the name a is then unknown.
So when you use |var a| anywhere within the scope, the name a
becomes visible both before and after |var a|. |var| isn't
something executable (in my mind model at least), it just says
which scope the variable resides in. The result is this scoping.
And as such, it is valid throughout the scope.
This is a very good arrangement, and I think all declarations
should work this way, always, consistently (all declarations
where this makes sense).
In the following example, one var is redundant. You can omit either
one, the meaning remains identical:
function f()
{ var a = 10;
var a = 20;
}
Here's another example of redundant var:
if (x)
var a = 10;
else
var a = 20;
For consistency and simplicity, in my opinion all declarations
should also allow this redundancy. It's an odd arrangement,
but in fact it's useful.
Of course such redundant declarations must be consistent. So
the following would be a conflicting declaration error:
if (x)
var a = 10; // ERROR -- Conflicts with const.
else
const a = 20; // ERROR -- Conflicts with var.
I use redundant |var| everywhere in my code to signal localness.
I do this because if I leave out |var|, this is a clear warning
to myself that I'm accessing something external.
I would greatly prefer having a strict mode that required me
to use an |outer| declaration, as a much clearer warning that
I'm accessing something external:
var a = 10, b = 20;
function f()
{ outer a;
c = outer b;
}
This would be /much/ more useful than data types in very small
programs.
> And so on.
>
>> The above is the /exact/ functionality of function hoisting
>> like var, apart from using two names. You can refuse the
>> clearer syntax, but you can't refuse the above code and
>> functionality.
>
> I think I see the confusion now. Do you believe that in the var b =
> a; code you wrote, both the binding of the var named b *and* its
> initialization with the value of the function object denoted a are
> hoisted? Hoisted up to what point?
No, it starts out |undefined|, and that's a good thing, as detailed
above.
The assignment stays in place and occurs where it's written in the
code. I couldn't express this with exactness in that code snippet.
In my snippet's inner scope the function is assigned (is callable)
from before you enter the inner scope, since it's written as a
declaration rather than an assignment. Once again, this is exactly
as should be. That snippet is carefully crafted.
The main difference between a real hoisted function and my snippet
is that my snippet shows the function with two different names, one
illustrating the inner-scope assignment and the other illustrating
the outer-scope assignment, whereas a real hoisted function would
have only one name, bound only in the outer scope.
I could instead have written my snippet like this instead:
{
var a = function a(){}
}
Then it's only one name, hoisting out and bound in the outer
scope, so in that regard, this is more similar to a real hoisting
function.
However, this does not reflect the fact that the function should
be assigned (should be callable) from before you enter the inner
scope. Since I wanted to refute the claim about complexity, I
found it more relevant to show correct hoisting and assignment
than solving the triviality of having two names.
In fact showing it with two names might even help a little. It
may help make clear and specific how there are two scopes involved,
and how each one is dealt with by re-using existing functionality.
(Or rather, I get the impression that you can re-use existing
functionality, largely, and I've seen no specific counterargument
so far.)
So the one and only outer name of the hoisted function gets assigned
the callable function just before we enter the inner scope.
> Waldemar wrote a while back: "Keep in mind that function assignments
> hoist to the beginning of the scope in which the function is defined,
> so your proposal won't work."
>
> The word "assignment" where "definition" was perhaps more precise
> (function definitions replace extant properties of the same name in
> the variable object, they are not equivalent to assignment
> expressions) may have misled you. From the context and the long-
> standing spec and implementation behavior with functions not in
> blocks or any other sub-statement position, it was clear (I think)
> what was meant, but I can see how this could be confusing.
>
> Assignment expressions and initializers in var statements do not
> hoist or otherwise move in the flow of control.
I hope I've shown now that I understand all this quite clearly,
and intended exactly this behavior, and consider it necessary,
acceptable, and very useful.
I greatly appreciate your reply and questions. Detailed and
specific, clarifying to me what I need to explain and answer.
Perfect for my somewhat overly detailed mind. Thank you very
much.
I hope my long answer hasn't been too exhausting to read.
--
Ingvar von Schoultz
------- (My quirky use of capitals in code comes from my opinion that
reserved and predefined words should all start with lowercase, and
user-defined should all start with uppercase, because this will easily
and elegantly prevent a host of name-collision problems when things
like programming languages are upgraded with new labels.)
More information about the Es4-discuss
mailing list