The worst thing about CoffeeScript
walpurgisriot.github.io
walpurgisriot.github.io
Let's extend your example:
>>> a = 1
>>> def foo():
print a
>>> foo()
1
So Python does take from the outer scope? >>> a = 1
>>> def foo():
a = 2
print a
>>> foo()
2
Or not? >>> a = 1
>>> def foo():
print a
a = 2
>>> foo()
UnboundLocalError: local variable 'a' referenced before assignment
Or it just doesn't work at all?Next time, pick a better example. The answer to "What does Python do with a variable from outside a function's scope?" is "It depends."
At least CoffeeScript is consistent.
It can lead to some really unexpected bugs. There is a reason most languages don't do things this way.
http://lucumr.pocoo.org/2011/12/22/implicit-scoping-in-coffe...
setter = (arg) -> store = arg
store = 'initial'
is quite different from store = 'initial'
setter = (arg) -> store = arg
This is a practical problem since it means that the most commonly used JS encapsulation feature - closures - become harder to write. Suddenly assignments that have no direct influence on each other cannot be blindly reordered (which is a pain), and in particular closures that have a bunch of helper functions defined initially that need to ensure that local variables are set before the helpers, even if those local variables are themselves functions.It's just a mess. Frankly, I think javascript does this part a lot better than coffeescript, especially now that "use strict" exists and you can get some feedback when your scoping assumptions turn out wrong, rather than a silent failure and unexpected behavior.
No, the issue is that he's used to having scoping work properly.
Coffeescript behavior corrects a wrong (shadowing) with an even worse wrong. Even worse, as a default it can affect variables outside of the function, so it breaks locality.
Fortunately, CoffeeScript is open-source, and can be both easily changed, and easily forked to suit your fancy. There are very nice versions (LiveScript and Coco, for example), that change this behavior by introducing two different types of assignment — if that sort of thing is your cup of tea.
... and if anyone has a specific proposal about how they'd like to see this feature changed in order to improve it, feel free to open a ticket for discussion — it could very well happen. For example, making this:
topLevelFunction = ->
path = "a/b/c"
... many lines of code go here ...
path = require('path')
... into a compile-time error, with some sort of "Subsequent variable shadows `path`" warning, that would be something worth talking about.As to the ticket; A quick search would have revealed there are many tickets on the topic of shadowing: https://github.com/jashkenas/coffee-script/search?q=shadowin...
And this ticket covers this specifically https://github.com/jashkenas/coffee-script/issues/2697 and was closed.
LiveScript is interesting, but seems to have Too Many Features for my tastes.
People have to distinguish between the simple issues that trip up beginners, and the fundamental issues that plague you for life.
Whether it causes any issues in practice for some particular programmer or not (and 10K lines is not much even for ONE modern webapp, and you write them broken down in multiple libraries), the thing is the behavior is logically flawed in itself.
Like Javascript needing "var" to NOT make a global variable (instead of defaulting on local vars) is.
Coffescript's behavior on this scoping issue is in the list of bad items that includes goto, global variables and nullity.
If you have a team that is big enough and a project that lives long enough that someone has to do changes in some old code, this thing is bound to happen sooner or later.
angular.module("blabla", []).directive "bla", ->
template: """ <div>Whatever<div ng-transclude></div></div></div> """ scope: true transclude: true restrict: "E" compile: (element, attributes) -> $input = element.find "input" for attr, value of attributes.$attr $input.attr value, attributes[attr] element[0].removeAttribute value
(scope, element, attributes) ->
$input = element.find "input"
# do something with $input
# now $input is global to all directive and that sucksThat saves me quite a bit of time in the long run.
One downside of coffeescript's loop comprehensions is that they generate fairly complex code whenever you use them in non-trivial ways, thus undermining coffeescript's key feature: easily readable (and debuggable) javascript.
This has two assignments, but only one var in the compiled JS:
a = 1
-> a = 2
This has two vars: -> a = 1
-> a = 2One variable:
a = 1
-> a = 2
Two variables: -> a = 2
a = 1 a = 1;
def foo(): b = a; return b
foo() is a
>> True
This gets hairy when referring to mutable types: a = []
def foo(): b = a; return b
foo().append(1)
a
>> [3]
Compare to immutable types: c = ()
def bar(): d = c; return d
bar() + (1,)
c
>> ()I was simply illustrating how using a mutable data structure defined in an outer scope can bite you.
So you would do:
a = 1
def contrived():
global a # I added this
a = 2
return a
contrived() # 2
a # 2
The "nonlocal" keyword would give the same result if the outer scope is the top level of the module. If the entire above code is itself enclosed in a function like so: def enclose():
a = 1
def contrived():
nonlocal a
a = 2
return a
contrived() # 2
a # 2
You need "nonlocal" to refer to the "a" that's a local variable in enclose(). I.e. "global" gets you the top level, "nonlocal" gets you the next enclosing level.The keyword is only necessary when you're assigning to an out-of-scope variable. If you remove the assignment statement "a = 2" from the function, the program will correctly read and return the value of a from the enclosing scope.
In other words, by default the set of names considered to be local variables in a function is the set of names that are the target of an assignment statement. Usually the default behavior is what you want, but if you wish to override it, use the "global" or "nonlocal" keyword.
From a design standpoint, I've learned over the years that global / nonlocal keywords are a "smell" that frequently indicates poor architecture, usually of the sort that the oft-cited "global variables are evil" doctrine is intended to protect against. Code that extensively uses these keywords should usually be refactored into a class, with the formerly global names instead being attributes (member variables).
my $a = 1;
my $contrived = sub {
my $a = 2; # new lexical variable $a
$a = 3; # if the my above wasn't there, outer $a would be 3
...
}
say $a; # 1
Explicit declaration of the scope of a variable at initialisation is awesome. 'global' and 'nonlocal' feel weird/less consistent/like a workaround. People are used to creating variables 'on the fly' in Python / CoffeeScript - both break (for a certain value of 'break') certain assumptions in certain circumstances.I've never heard anyone ever complain about perl's lexical 'my'.. It even catches - with a warning - multiple my declarations that will clobber each other. You don't see that in other languages that often.
Explicit declaration of the scope of a variable at initialization leads to unintended global namespace pollution when omitting the declaration is legal and causes the variable to be placed in the global scope, which can be difficult to diagnose because it is usually completely silent until unrelated code happens to interact by using the same name.
From a language design standpoint, this means that you should either require a declaration which determines scope (C or Java), or have local be the default for variables whose scope is undeclared (Python).
> I've never heard anyone ever complain about perl's lexical 'my'
That's because programmers who care about good syntax in their languages don't use Perl.
Ah, but it's not legal with 'use strict;' (which everyone uses)
> That's because programmers who care about good syntax in their languages don't use Perl.
And Lisp has too many parentheses. I clearly just like my code to be illegible.
def outer():
a, b = 1, 2
def inner():
nonlocal a
a, b = 3, 4
print('inner a: ', a)
print('inner b: ', b)
inner()
print('outer a: ', a)
print('outer b: ', b)
outer()
# inner a: 3
# inner b: 4
# outer a: 3
# outer b: 2
I find Python's scoping rules to be quite sane.I personally would like to apologize to everyone for dissing CoffeeScript in the past, here (thus my low karma) and elsewhere.
I'm working with a new customer now, three weeks already. and they're using CoffeeScript. and I had to get over myself and start using it. and it's amazing. I've been so stubbornly blind to be against it without actually trying it out yet cursing and flaming against it. it's so much faster to work with it, it reduces the boilerplate code, it simplifies everything.
and, yes, I still think you need to be able to know and use JavaScript very good before taking on CoffeeScript -- otherwise topics like the one we're discussing here arise.
Of course he's doing something wrong. That's the whole point of TFA. That the language, due to a bad design decision, permits this case of doing something wrong, when it shouldn't.
Like Javascript making variables global by default (unless you use var). If this is an issue to you, you're doing something wrong (namely, you're forgeting to add "var"). But the language does an even bigger mistake by permitting this to happen.
Big thanks to jashkenas, because ... CS is awesome!
If you want to complain about CoffeeScript, there are some much more fundamental issues. For example:
- I can write `foo.bar baz, bat` but if I call it with no arguments I have to use parens. `for.bar` will just return the function. I understand why it's the case, but it sucks.
- Syntax is so flexible that sometimes it still compiles when you make a mistake.
Interestingly, if you write "do foo.bar" you don't need to use parens and it will work as expected. I think this is a bit more idiomatic.