CoffeeScript's Scoping is Madness

CoffeeScript, for those not in the know, is a programming language that compiles to JavaScript. Its mission is to "expose the good parts of JavaScript in a simple way". I've heard a lot of interest in it lately from various channels, and our deployment bot at work is written in it. I started toying with it and ended up getting weird issues I didn't understand, that is until I looked at the JavaScript it was generating.

It ends up CoffeeScript's scoping of variables is basically nonexistent. As soon as you use a variable named x for example in an outer scope, x becomes global across all the inner scopes. This to me is a huge issue. Defining a variable in your code should never redefine the definition of inner functions. This is madness. This is CoffeeScript

Look at the following examples:

CoffeeScript test = (x) -> y = 10 x + y; alert test 5 JavaScript var test; test = function(x) { var y; y = 10; return x + y; }; alert(test(5));

All fine and good, but lets make one small change and define a variable named y in the outer scope, outside the function.

CoffeeScript y = 0; test = (x) -> y = 10 x + y; alert test 5 JavaScript var test, y; y = 0; test = function(x) { y = 10; return x + y; }; alert(test(5));

The Problem

Notice the pink highlighting indicating the scope of the y variable. By defining y in the global scope all subsequent y 's become global, forever. You cannot have a variable named y in your code, no matter how many levels of scope or closure deep, which is not the global instance. The fact that you have a global of that name defined overwrites all subsequent definitions. In most languages with closures (including JavaScript) the inner y and the outer y could and would be separate. CoffeeScript though does not allow Shadowing or defining a variable named the same thing in a deeper scope.

To contrast, in JavaScript the var keyword defines the scope of a variable. Leave it out and it scopes downward until it hits a var definition of that variable or the global level. This means that deeper scopes inherits from a level above unless it defines its own scope of the variable. CoffeeScript on the other hand offers you no method to scope a variable. Scoping is entirely automatic, and the lowest level use of a variable name is the single instance of it.

This is an extremely dangerous behavior. You need to be aware of every variable named blah within the current scope as well as every single deeper scope before defining a blah variable within your code, or else latter definitions will be trampled by your new earlier definition.

To better explain the problem, lets say you have four functions all using a generic i variable. Now if we use an i for a loop outside of your functions suddenly the definition of all four of your functions changes. They all reference the global i rather than the four local i 's they were a moment ago.

This means the only "safe" way to write CoffeeScript is to assume all variables are global, because essentially they are.

There are proposed solutions, such as a Go-lang style := for inner scoping, but these have not been accepted as of this writing. The creator of CoffeeScript, Jeremy Ashkenas, when questioned about it by Armin Ronacher on Twitter replied:

Not gonna happen ;) Forbidding shadowing altogether is a huge win, and a huge conceptual simplification.

His sentiment is not just bad but plain wrong, especially for larger applications. It makes work more complex, not simpler. The fact that the entire meaning of large swaths of code can change with one variable of function definition (remember they are in the same scope in JavaScript) means a world of unexpected consequences.

Finally

CoffeeScript's noble goal of "expos[ing] the good parts of JavaScript in a simple way" was completely missed. Instead of improving JavaScript's scoping which can be kind of daunting to learn, they actually made it much worse. If you want to improve JavaScript make every variable local until explicitly defined global, as is the behavior in the vast majority of languages.

JavaScripts "global unless defined as local" behavior is weird, but manageable. CoffeeScript's "local until defined elsewhere globally" behavior on the other hand is simply difficult to predict, and makes it easy to accidentally change the entire meaning of code even when you entirely understand the behavior.

It is a bad behavior. Period.

Further Reading

I'd like to also suggest reading Armin Ronacher's The Problem with Implicit Scoping in CoffeeScript. It eloquently describes the problem from a slightly different angle.

Update