Space Cat, Prince Among Thieves

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


Comment by: Rudi Angela on

Rudi Angela GravatarShortly after discovering Coffeescript I stumbled upon LiveScript, which is derived from Coffeescript, but enhanced with support for functional programming. I've been using LiveScript ever since. I just checked your example and this is what LiveScript compiles it to.
var y, test;
y = 0;
test = function(x){
var y;
y = 10;
return x + y;
};
test(5);

Comment by: Fatih Kadir Akin on

Fatih Kadir Akin GravatarYou can fix it using literals to make the variable local.

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

alert test 5
alert y

Comment by: alexG on

alexG Gravatar`var y`

That's javascript though, not CS :D Bit of a hack!

Comment by: Marcel on

Marcel GravatarThis also works:

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

alert test 5
alert y

Comment by: Joker_vD on

Joker_vD GravatarWell, to me it seems that CS what lacks is, in the first place, variable declarations. The "variable is created at the moment of the first assignment to it" stuff is stupid, because OF COURSE you can't have shadowing: how do you sintactically tell an assignment to an already existing variable from creating and initializing a new variable?

Either you mark declarations with "var" or whatever (Basic, JS), or automatically shadow variables in the outer scopes — but then again, how do you un-shadow them? PHP's "global" is mighty annoying... and do you really need global variables? You do need variables from outer scopes, however: "x = -5; if (x < 0) { x = 0 } alert x" better print "0".

Comment by: Greg on

Greg GravatarYou are wrong about “local until defined elsewhere globally.” It's not “elsewhere,” it's “in the same file.” If you feel the need to declare the same variable twice in the same file, don't. Start a new file.

Comment by: Christian Oestreich on

Christian Oestreich GravatarYour statement about using the variable globally only holds true if you compile using --bare causing the wrapper function(){...}.call() to be removed. By leaving that call in place and letting coffee compile normally the y variable would be confined to the file or module in which you defined it and wouldn't be "global" unless all your code was in the same file.

http://coffeescript.org/#lexical-scope

I would strongly advise people to adopt the revealing module pattern in coffeescript if you want to compile using --bare. Almost all of the scripts I write these days use that pattern with or without using --bare to ensure scope compliance.

The issue you raise is valid in some cases. I think regardless of coffee v. javascript, people too often make all code global and introduce these kinds of potential bugs.

Comment by: ckknight on

ckknight GravatarCoffeeScript's scoping is one of the more terrible things about it. It's one thing that I fixed in my compile-to-JS language, GorillaScript: http://ckknight.github.io/gorillascript

Comment by: Eugene on

Eugene GravatarGreg gets the point :)
If your module is too big you cannot track your variables something is wrong with the module independently of the language

Comment by: kayan on

kayan GravatarI kind of like Marcel's solution. It's not as hacky as escaping to JS, and placing the local variables at the beginning of the function definition is neat. It does, however, generate a useless "check for null" line of JS - but generating useless extra JS is one of a problems with coffee script in general.

I'd be much happier with a JS compiler that can understand pure JS, but will insert semicolons on line breaks in a logical way. The whole semicolon thing is the only part of JS that actually still bugs me. I wouldn't mind it also optionally let you use @ and -> for this and function, and added var to undefined variable definition - so long as it still let you do these things in the native JS way.

Comment by: Daz Fuller on

Daz Fuller GravatarIt might just be me, but what is happening with 'y' there is exactly what I thought would be happening. Reading the code and the outcome was not a surprise to me

Comment by: Nathan Van der Auwera on

Nathan Van der Auwera GravatarThis is discussed before. At first I had the same reaction as the OP had. But, think about it: how many times do you actually declare variables in global scope? In such an example as you show here (variables called x,y, ...) but who in their right mind would ever declare a `y` in global scope? Normally variables are declared inside a class. Inside a scope. I personally tend to avoid global scope, so it is not an issue for me.

So while this may seem a real problem, not being able to control the scope of a variable explicitly, in practice: no problem at all.

Comment by: Alexander Ewering on

Alexander Ewering GravatarHah! I had EXACTLY this problem today at work - had a function "lang" in the global (window) scope, and used a local variable "lang" in a function. Suddenly, the global function "lang" was totally broken ("undefined is not a function") even when calling it BEFORE the first declaration of the local "lang" variable.

Madness!

Comment by: Dude on

Dude GravatarMr, you actually don't quite understand javascript or coffeescript. Please check actually-YOU-dont-understand-lexical-scope.md

Comment by: Jesse G. Donat on

Jesse G. Donat GravatarActually, I do! I've read your link a while ago, and here is a quote from the author of the link, who's book I have now also read!

Long story short, I am now and have always been correct.

Comment by: maninalift on

maninalift Gravatargo LiveScript, besides solving this problem, there are a host of small improvements over CS which lead to cleaner code and just more fun.

Comment by: Shiggity on

Shiggity GravatarWhat do you suggest then, not using it? Rails "prefers" CoffeeScript and they use it in all their examples.

Comment by: spion on

spion GravatarThe problem is neither scoping, nor shadowing.

The underlying problem is conflating variable declaration and assignment.

Whenever a language does this, it has to rely on hacks and workarounds to minimize the confusion and problems that it spawns.

For example, python has `global` and `nonlocal`, coffee-script has this issue described here, and JavaScript has the issue where it makes a global if the variable doesn't exist

CofeeScript could still forbid shadowing but add `var` as a keyword to declare new variables.

And if you try to declare a variable with the same name it would give you a *compile-time* error:

"unable to use this name for a new variable, its already in use."

Or even when you try to assign to a non-existing variable

"unable to assign to an undeclared variable."

thereby reducing the original problem to finding new names when the compiler complains :)

And you know what? "use strict" in JS gives you exactly this (but with shadowing allowed)

Comment by: mp on

mp GravatarHi ! The fact is coffeescript forces you to write javascript the right way , ie no global variables anywhere. you need to use namespaces like

ns.x=5

on the "global" level if you want to do things properly. Coffeescript wants you to do that. It promotes a certain way of coding. If you dont like it fortunatly there are several alternatives.

Comment by: wat on

wat Gravatar"The underlying problem is conflating variable declaration and assignment.

Whenever a language does this, it has to rely on hacks and workarounds to minimize the confusion and problems that it spawns."

In Haskell, "variable declaration" is "conflated" for assignment. ML (created several decades ago) is very similar. And they don't have any hacks or workarounds caused by this. /thread. tl;dr rest of your response. Whatever issue you guys are arguing about is probably because of implicit mutable state (in ML, there is mutable state but you have to explicitly declare variables as having mutable values)

Comment by: Ben on

Ben GravatarDeclaring the intention to introduce a new variable into the current scope could be done with a very short comment (which could be in effect until the next blank line):

#nv
counter = 0

(This is similar to a comment like "// intentional fall-through" in a Java switch, but much shorter.)

Then you could just write a CoffeeScript checker that generates an error when you misspell a variable name or accidentally clobber a higher-level variable. That's what I plan to do if I adopt CoffeeScript.

Comment by: Ciantic on

Ciantic GravatarOh man, this really sucks, too bad the CoffeeScript creator can't see this.

Local Scope should be always the master for the sake of developer. Requirement to remember the names of all variables in outer scopes is distraction, and very error prone.

If one needs to access or set a variable in non-local scope, there should be keywords for that like "this.y = something" where this is a clear word meaning property variable, or something like global y; y=5; if it's non trivial case.

Comment by: Vic on

Vic GravatarFor what it's worth, I agree that this is not an issue.

I totally agree that if you're using the same variable name in nested scopes in the same file, you're not writing very readable or maintainable code to begin with. I would humbly suggest that "lang" (as mentioned in one of the comments above) is a particularly bad variable name (as is "x" and "y" for that matter). What does that mean? If it means a different thing in each scope, it should be named semantically so a casual reader can tell them apart. If it means the same thing in both scopes then that implies that it's global, so no problem.

Comment by: Hal Noyes on

Hal Noyes GravatarSimple solution - don't use globals, except for ONE global object as a namespace where you put all non-class variables and functions. Thus they are always qualified and can't be trashed by same-named locals.

Comment by: spion on

spion Gravatarwat, let me be more precise with my terminology then:

CoffeeScript's equals operator conflates variable initialization and variable reassignment. It doesn't allow the programmer to express their intent. "I want a new name" vs "I want to reassign this old name to a new value" are two vastly different things.

And now to refute your point: Haskell has no notion of "reassignment", so there is nothing to conflate.

Comment by: Alan on

Alan GravatarThis only really matters in nested scopes. How many scopes deep are you nesting anyway? In Python, this is rarely a problem. Although, it's also true that Python doesn't give you full anonymous functions (unlike CoffeeScript), discouraging you from nesting all that much anyway. In Python, the only time you need to use nonlocal is to actually assign to variables from the outer scope. But I would argue that this should be avoided to begin with.

Extreme levels of nesting come about when writing complicated function decorators or when navigating through callback hell. The former case is rare enough to not be that much of a problem. In the latter case, I think maintainable code uses mechanisms like promises to flatten the nesting, anyway.

In conclusion, scoping is not "broken". It just doesn't work well when you have layers and layers of inner scopes and try to reuse names. This might be inconvenient, if, say, you need to cut-and-paste in some closure. But realistically, I don't think it's a big deal. At all.

Comment by: Jeff on

Jeff GravatarThis also works to get around the issue:

y = 0;

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

alert test 5
alert y

Compiles to:

var test, y;

y = 0;

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

alert(test(5));

alert(y);

Comment by: Turbohz on

Turbohz GravatarOr even better, with do notation:

y = 0;

test = do(y=10)->(x)->x+y

alert test 5
alert y

Comment by: Matt on

Matt GravatarWhy not just using "@y" inside your complex function? This way you target only the function scope.

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

alert test 5
alert y

Comment by: dennis r on

dennis r GravatarAs your function is an object you can simply bind a variable to it:

y= 0
test= ( x ) ->
test.y= x

test 10

log y
log test.y

Comment by: dennis r on

dennis r GravatarOh, my log..

log= ()-> for arg in arguments
console.log arg

y= 0

test= ( x ) ->
test.y= x

test 10
log y
log test.y

Comment by: peec on

peec GravatarI find it funny that people need to write workarounds for something so basic, that's what happens when you go ahead using a layer like Coffeescript just because it's a bit shorter to write. What you get is a codebase that, in the end is full of ugly hacks such as "`var y`" - which is indeed just "var y;" in Javascript - that is one character less.

I feel Coffeescript is more like a language people use because they either are too fixated on "Syntactic sugar" or just doesn't know how to write plain Javascript - and if that is the case they shouldn't be writing Coffescript because you still need to know what is actually happening to debug.

Elegance over readability is not always good.

Comment by: Foxhoundn on

Foxhoundn GravatarI suppose the creator of CoffeeScript counted that the developers using it know the difference between an object and a function.

y = 0
test = (x) ->
@y = 10
x + @y;
alert test 5

Voila......

Email address will never be publicly visible.