Lexical scope and order #1121

Closed
TrevorBurnham opened this Issue Feb 8, 2011 · 19 comments

Projects

None yet

7 participants

@TrevorBurnham
Collaborator

I was a little surprised to discover recently that

incrX = -> x = x + 1
x = 0
decrX = -> x = x - 1

gives x local scope in incrX but not decrX—in my mind, it's more intuitive for the compiler to take the whole file into account when determining scope, rather than requiring outer variables to be "declared" before inner ones. That way, shadowing would only be possible with argument names.

Is this behavior desirable? Is there a previous issue on it?

@michaelficarra
Collaborator

Well, I would say it is consistent and not surprising to me. But is it the way it should work? I'd be willing to hear some good arguments against how it works currently, but it appears correct to me. I don't think the whole file should be taken into account with regards to scope. I don't want to see a function defined at the top of a huge file and not know if it's local to that function until I've read the whole file. With the way it is now, you only need to remember the variables you've seen since the top of the file.

@TrevorBurnham
Collaborator

I'd make two arguments for order to be made irrelevant to scope:

  1. Shadowing is bad; this is what we tell people every time they ask for explicit var in CoffeeScript. It's best for different variables to have different names. You have to be paying fairly close attention to realize that there are two variables named x in the simple example above. With order-irrelevant scoping, shadowing would only be possible with argument names, where the dangers are well-understood.
  2. The rules are simpler that way. I like to picture scope as being a series of levels; a variable simply lives in the outermost level in which an assignment is made to it. Adding order into the mix makes scope a bit harder to explain.

It's not a big deal, but I'd call order-irrelevant scoping a minor improvement to the language.

@aeosynth
Contributor
aeosynth commented Feb 8, 2011

isn't this a feature?

Lexical Scoping and Variable Safety

The CoffeeScript compiler takes care to make sure that all of your variables are properly declared within lexical scope — you never need to write var yourself.

edit: better link/quote

@satyr
Collaborator
satyr commented Feb 8, 2011
@TrevorBurnham
Collaborator

Alright, I thought this was worthy of discussion, and it's now been discussed. Closing the issue.

@strmpnk
strmpnk commented Apr 15, 2011

Looking through this issue, I'm not sure it's been discussed very thoroughly. I recently had it bite me on some pretty obvious code otherwise. The issue for me is that, while there are reasons to automatically close over known variables, JavaScript only allows variables to exist in a function body top to bottom, making it a pretty inconsistent choice to work how coffee currently does (regardless of Ruby since it does something completely different here anyway).

foobar ->

  nested ->
    x = x + 1

  other = ->
     object.value = x

  x = 0
  obj = {nested: nested, other: other}

Now this is obviously a little contrived but it illustrates the problem. I might need to put all my initialization at the top of the function. but the object literal is then stuck at the bottom while the x is at the top, creating spaghetti code. Now I'm sure one could say "use classes" but I have cases where I really need to avoid that.

I'd really like to avoid splitting my code up like this just because coffee doesn't do a pass on the entire enclosing function block before deciding what variables are available for closure... I could likely avoid this if there were a format for hoisted functions rather than var= form, but that's been shot down before, so perhaps this issue can be reviewed again.

@TrevorBurnham
Collaborator

Well, it sounds like the core team is pretty set on the way it currently works, and the linearity argument is a good one (makes things easier on the compiler, makes the REPL consistent, and avoids cases where, after scrolling down a hundred lines, your assumptions about a variable's scope are invalidated). But your point about spaghetti code is a good one, too.

I think moving all initialization code to the top is good style anyway. So the best way to write your example would be

foobar ->

  x = 0
  obj = {}

  obj.nested ->
    x = x + 1

  obj.other = ->
     object.value = x

  obj

which could, of course, be shortened to

foobar ->

  x = 0
  obj =

    nested: ->
      x = x + 1

    other: ->
       object.value = x
@strmpnk
strmpnk commented Apr 15, 2011

I understand people are set one way or another but I don't think that should be a reservation for discussing issues.

Right, though my earlier case (sorry, code I can't share) isn't very easy to reorganize like that... and even in that case you have the problem of two variables across two functions (contrived again but much less code to read):

foobar ->
  a =
    x: -> b = b.x()
    y: -> 42
  b =
    x: -> 42
    y: -> a = a.y()

In this case I have some concurrency primitives I've written for some of my projects on node.js which do need to assign to closed state and have multiple functions that can deal with this state. What I end up with is a little ugly but it works. I just assign a dummy value to one like:

b = null
a = ... # Now b won't be treated local.
b = ...

It's a problem we see in many languages like Ruby too, for those cases where you need to establish the scope before a call... but in some cases, a smarter compiler could do variable declaration as a final pass rather than in lexical order since JS doesn't work that way either.

I'll be able to write this code either way but it seems like these little details could be improved, which makes a difference over time.

@jashkenas
Owner

Establishing scope before a call is totally the way go to -- it's much like declaring the variable, which you would need to do anyway in a language that had variable declaration.

@TrevorBurnham
Collaborator

True, though I sorely wish there were a better convention for saying "I declare x, y, and z to be in this scope" than "x = y = z = null," since that syntax misleadingly suggests that the particular initial value is important. I like the consistency of always using = for this purpose, mind you, but some special syntax like "x = y = z = *" would help to clarify some code, as well as yielding slightly more byte-efficient output.

On Apr 15, 2011, at 1:33 PM, jashkenasreply@reply.github.com wrote:

Establishing scope before a call is totally the way go to -- it's much like declaring the variable, which you would need to do anyway in a language that had variable declaration.

Reply to this email directly or view it on GitHub:
#1121 (comment)

@jashkenas
Owner

I strongly disagree -- the whole concept of "I declare this variable to be in this scope" is the concept we're trying to avoid. And the initial value is important -- what better value for a variable without a value than null?

@strmpnk
strmpnk commented Apr 15, 2011

@jashkenas I'm sure "in a language that had variable declaration" is JavaScript and my primary concern is that the relationship between CoffeeScript and JavaScript make this more of a leaky abstraction than a clean separation of concerns.

Also, I don't see how it's declaration to promote the existence of a variable. We are just saying, similar to JavaScripts semantics, that variables are function scoped. As an example, this code will find the local variable even though it doesn't exist in terms of lexical order:

x
x = 42

You could consider this a bug in CoffeeScript or a leaky abstraction. Finally, I'd love to ask what we are losing if we add support for hoisted vars. I'm having a hard time seeing how this creates problems for existing idiomatic CoffeeScript code or even any existing code at all except for code that probably already had a bug to begin with.

@jashkenas
Owner

I'd consider that a bug in JavaScript. The fact that you can hoist a variable declaration, or even (worse) an entire function definition, is the implementation details of JavaScript leaking into the public API. There's no legitimate use case for code that makes use of either type of hoisting -- and I know we've debated this point before. Any code that expresses itself through hoisting can be expressed more clearly without the hoist. I don't think CoffeeScript should make it any easier to abuse.

In your example above, if we could statically determine that x doesn't exist on the global object, it would be great to throw a compile error.

@strmpnk
strmpnk commented Apr 15, 2011

JavaScript is your backend so in that sense a bug in that produces a bug in CoffeeScript. Not sure any righteous attitude here helps this at all. As far as my code concerning x, if wouldn't matter if it were global or not as the var generated shadows anything that might have been there, so that would be an invalid error to raise. The real error might be: reference to x before first assignment or l-value.

If you feel you can't find any use-case then feel free to end the discussion here because I'm not going to argue with you on style. I only argue this point because I feel like discipline is better than restriction in this case.

@jashkenas
Owner

Great -- let's add a compiler error for variables being used before they're declared ... in cases where a subsequent declaration would be sure to shadow. Reopening the ticket.

@jashkenas jashkenas reopened this Apr 15, 2011
@michaelficarra
Collaborator

@jashkenas: instead of reopening this issue, maybe a new issue should be opened for that? At this point, we seem to have strayed far enough away from this issue's original intent to warrant a new one.

@RussellSprouts

Here's some stuff that may be of interest.
In Lua programming it is recommended to always use local variables, but variables are always global by default. Here are some arguments why:
http://lua-users.org/wiki/LocalByDefault

Basically, what I'm worried about in my code is defining a variable again deeply nested and having a hard to find bug. Since CoffeeScript has no way to define globals besides window.whatever, I think allowing a var statement so that a variable can be shadowed in a good idea. The default can be the current behavior.

@strmpnk
strmpnk commented Jun 22, 2011

Not sure this has anything to do with local vs global. It's really just the lifetime that a declaration is effective for. I personally believe that sticking to function as your scope operator in JavaScript works really well and slicing it thinner than that prevents people from really understanding function as a scope operator. CoffeeScript takes a different point of view and that's that. I'm not sure there is anything to argue since none of the facts have changed. What this is open for is the possible addition of compiler warnings or errors to assist discovering this discrepancy.

@michaelficarra
Collaborator

Closing in favor of #1555.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment