CoffeeScript's Scoping is Madness

CoffeeScript 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.

After a coworkers departure, I inherited maintenance on our deployment bot, and it was written in CoffeeScript. While trying to make some small modifications, I ended up getting unexpected issues I didn't understand. Things I did not touch were suddenly breaking. I did not understand until I investigated the generated JavaScript.

It ends up CoffeeScripts approach to scoping of variables is to not scope variables. There is no shadowing. There is no var, let nor const equivalent, there is simply no way to explicitly define scope.

Variables in CoffeeScript are scoped to their outermost definition, regardless of whether the name was already in use elsewhere. When you use a variable of a name, its scope now includes all inner scopes, trampling any existing definitions of the variable in those scopes.

Let's examine the following examples:

CoffeeScript
test = (x) ->
y = 10
x + y;

alert test 5
Generated JavaScript
var test;

test = function(x) {
var y;
y = 10;
return x + y;
};

alert(test(5));

Now let's make one small change and define a variable y in the outer scope, outside the function.

CoffeeScript
y = 0;
test = (x) ->
y = 10
x + y;

alert test 5
Generated JavaScript
var test, y;

y = 0;

test = function(x) {
y = 10;
return x + y;
};

alert(test(5));

The Problem

Notice the highlighting indicating the scope of the y variable. By defining y in the global scope all subsequent y's are also now that global. You may no longer have a variable named y in your code, at any deeper scope. The outer y is the only y.

Creating a global of a name given overrides all subsequent definitions.

In JavaScript var, let, and const keywords define 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 inner scopes inherits from outer scopes 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 left to the lowest level use of a variable name.

This behavior is dangerous and makes code very difficult to maintain. The problem is twofold. Firstly, when writing a function, you need to be aware of all shallower scopes variable definitions, as not to trample them. Secondly, when altering code at any scope but the outermost, you need to be aware of all deeper scopes variables as to not trample any of them.

The only "safe" way to write CoffeeScript is to assume all variable names are global. In practice they are.

There have been 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.

This is not true for applications of any scale larger than toy. It makes development more complex, having to check every inner scope before using a new variable name. This is not simpler. The entire behaviour of all inner scopes can change with a single bad variable defintion.

Finally

CoffeeScripts goal of "expos[ing] the good parts of JavaScript in a simple way" was missed. Instead of improving JavaScript's scoping which can be kind of daunting to learn, they actually made it 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

I've a fight on Reddit here.

I've irked Reginald Braithwaite, the author of CoffeeScript Ristretto, so badly he's giving away 100 copies of his book for free!

The author has upgraded me to Not Wrong™!