CoffeeScript's Scoping is Madness
- Comments:
- 36
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 scopped to their outermost definition, regardless of whether the name was already in use elsewhere. When you use a variable of a name, it's scope now includes all inner scopes, trampling any existing definitions of the variable in those scopes.
Let's examine the following examples:
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.
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!
- I've been upgraded to Not Wrong™ by the author!
Comment by: Rudi Angela on
var y, test;
y = 0;
test = function(x){
var y;
y = 10;
return x + y;
};
test(5);
Comment by: Fatih Kadir Akin on
y = 0;
test = (x) ->
`var y`
y = 10
x + y;
alert test 5
alert y
Comment by: Joker_vD on
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
Comment by: Christian Oestreich on
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
Comment by: Eugene on
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
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
Comment by: Nathan Van der Auwera on
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
Madness!
Comment by: Dude on
Comment by: Jesse G. Donat on
In all fairness, @donatj is not wrong: The simplification of CoffeeScript's scoping inference does have costs.
— Reg Braithwaite (@raganwald) July 25, 2013
Long story short, I am now and have always been correct.
Comment by: maninalift on
Comment by: Shiggity on
Comment by: spion on
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
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
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
#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
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
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
Comment by: spion on
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
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
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
y = 0;
test = do(y=10)->(x)->x+y
alert test 5
alert y
Comment by: Matt on
y = 0;
test = (x) ->
@y = 10
x + @y;
alert test 5
alert y
Comment by: dennis r on
y= 0
test= ( x ) ->
test.y= x
test 10
log y
log test.y
Comment by: dennis r on
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
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
y = 0
test = (x) ->
@y = 10
x + @y;
alert test 5
Voila......
Comment by: androi on
http://androidexample.com
/Global_Variable_Or
_Application_Context_Variable_-_Android_Example/
index.php?view=article_discription&aid=114&aaid=136
Comment by: lionon on
I wouldn't mind if shadowing would be forbidden in a way you get a compile error when trying to shadow a variable (to all these, it shouldn't happen arguments). But this implicit behavior is just mad and difficult to track bugs to come.
I remember the hugh discussions why any variable in Lua is implicit global by default, when not explicitly declared local. And they got good arguments why implicit locals are stupid. But I never got why implicit anything for variable declarations was a good idea anyway. What is about typing a few letters more compared to the joy of hours and hours for debugging?
Anyway, implicit non-shadowing as well as implicit locals is an absolute deal breaker for me.
PS: The only reason I accepted Lua for another project was being able to alter the global meta table to raise an exception when a implicit global is tried to be created.