-
Notifications
You must be signed in to change notification settings - Fork 2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Variable declaration vs assignment #712
Comments
It has been well documented and discussed on the tracker before. I am actually quite happy with how Coffee handles scoping at the moment. |
I just read the documentation again more carefully. Indeed, I was too quick to post an issue. My bad. However, let me elaborate on this issue a little bit. This is a part of the documentation that explains my point in fewer words.
Consider this code
JS has different syntax for variable declaration My preferred syntax would look something like this:
This approach is only slightly more verbose but makes programs more explicit and robust, imho. And again, in my own code I tend not to use destructive assignment that much. What do you think? P.S. Python uses |
How about referencing a variable from the global scope? If you simply do When you are writing in Coffee, you will not always have full control over what files you interface with. While it may be possible to throw compile time errors when referencing a non-existing global variable, we cannot reliably do so when you are working on a file that just happens to be a small part of an application which in turn might have been written in pure JavaScript. Coffee is also used on the server-side. In node.js it's common to have many callbacks, like so:
Here we are referencing Keep in mind Coffee is a dynamic language just as JavaScript is. There is no type checking, no If I have to point out one benefit of the existing scoping, it would be that it's consistent. Define once and use everywhere. |
Maybe I didn't express myself clear enough. I guess I should have used the word outer scope instead of global. I am not proposing to change the scoping rules. Quite the opposite, I propose to keep them exactly the same as in JavaScript, and therefore making Coffee more consistent with JavaScript (I am as big fan of consistency). One way to do it is to introduce the following syntax
Right now So your example with |
How about keeping current semantics and re-introducing JavaScript's var keyword to force declaration?
|
If I understand correctly what you mean by forced declaration, it doesn't really solve the issue. To give anouther example. Say you have a function somewhere at the bottom of the source file.
Here JavaScript copied scoping rules from Scheme but imho it got the defaults wrong. I realize that proposed |
Yes, you understood me correctly. AFAIK there is no way to shadow variables in CoffeeScript, which Using var was a proposal to allow that in backward-compiatible way. The more I think about your proposal the more I like it. It is a huge backward incompatibility, but hey, CoffeeScript is before 1.0 version Speaking of syntax,
Other way to word akva's proposal that would probably make Stan happier is: |
Note that [we do have a way](http://satyr.github.com/cup/#foo%20=%2042%0A((foo%29%20-%3E%20foo%20=%2043%29(%29%0Aputs%20foo) (+1 by the way) to shadow outer variables. See also: #238 |
Exactly. Destructive assignment is a dangerous operation and should stand out as a warning. And, to repeat myself, programs written in functional style won't have too many
Don't want to sound pedantic but just to make sure we're on the same page. By assignment I mean destructive assignment, or rebinding the earlier declared variable. In this sence you use
Yes, but shadowing is not the problem.
Totally agree. It took Python 3.0 to get the scoping right - http://www.python.org/dev/peps/pep-3104/ (though they had a different problem to start with). CoffeeScript has a chance to get it right from the very beginning. Especially considering that it's not hard to implement, since JavaScript already gets the scoping (almost) right. |
Sorry, folks, but I'm afraid I disagree completely with this line of reasoning -- let me explain why: Making assignment and declaration two different "things" is a huge mistake. It leads to the unexpected global problem in JavaScript, makes your code more verbose, is a huge source of confusion for beginners who don't understand well what the difference is, and is completely unnecessary in a language. As an existence proof, Ruby gets along just fine without it. However, if you're not used to having a language without declarations, it seems scary, for the reasons outlined above: "what if someone uses my variable at the top of the file?". In reality, it's not a problem. Only the local variables in the current file can possibly be in scope, and well-factored code has very few variables in the top-level scope -- and they're all things like namespaces and class names, nothing that risks a clash. And if they do clash, shadowing the variable is the wrong answer. It completely prevents you from making use of the original value for the remainder of the current scope. Shadowing doesn't fit well in languages with closures-by-default ... if you've closed over that variable, then you should always be able to refer to it. The real solution to this is to keep your top-level scopes clean, and be aware of what's in your lexical scope. If you're creating a variable that's actually a different thing, you should give it a different name. Closing as a |
I was going to write a long response, but instead decided to replace it with a short question. Here
loop variable |
I'm not saying that shadowing isn't technically possible -- just that it's bad style, and CoffeeScript shouldn't make it easy for you to accomplish. It would be far better to use a different name for that index. |
I totally agree with you that shadowing is a bad style. And instead of shadowing it's indeed better to just choose a different name. But I am not talking about deliberate shadowing. I am talking about situations when you are not aware that top-level variable exists and assign it accidentally, or when top-level variable is added later. Check this example from my comment. The body of fibonacci function is supposed to be a black-box, but it's not. And what's worse, even unit test won't spot it - when run in isolation fibonacci function will work just fine, but will break in the context of the whole module. In other words the current scoping rules break encapsulation. This example is particularly illustrative since |
Adopted this strategy on my coco branch and am quite happy with the decision. |
@satyr Looks great. Would you like to share some more details about your experience of using it? Honestly, I reckon Ruby scoping rules are just wrong. It seems that they may change in Ruby 2.0 |
During the change among CS sources, I've found:
|
@satyr Thanks
That's about what I expected.
It's true that these bugs are quite rare BUT they are extremely hard to find. Even unit tests won't help. I rather deal with the bugs that happen all the time but easy to find than with those that are quietly sitting around as a ticking bomb waiting to explode. |
I discovered the scoping problem on my own. |
So I've reached this thread also. In fact, it's very arguable whether the shadowing is a bad style. Behind and before this term "shadowing" stands the basic term of programming -- an abstraction. A helper procedure should be really a black box. It's the main principle of substitution the arguments for a formal parameters even in math functions. The same stands for the local variables. The abstraction barrier which separates the level of implementation of a procedure from the level of usage of the procedure should be the real barrier in a well-designed system. Which means -- it should not bother the level of usage with exact variable names used inside. A designer of the procedure also shouldn't worry about which variables to use as just helper local variables. In general, it can be a casual case -- to reuse a some 3rd-party procedure in own project. Exactly for this concept of a scope and in particular nested scopes (namespaces, modules, classes, etc) is invented. That the Ruby have chosen to declare local vars without any keyword and refer these variables as outer from closures ( The mentioned reason ("it's a completely lexical scope") in fact just breaks the concept of nested scopes. And moreover, it smells like a substitution of concepts. The lexical scoping is one when it's possible at parsing stage to determine in which scope a variable will be resolved in runtime. And this determination is made by the place of the variable's definition. I.e. we may have several nested definitions with the same name and it still will be the lexical scope. If you don't like a special keyword for the definition, you may consider instead Python's way (which was mentioned also above in this thread). Though, less ugly keyword than a = 10 b = 20 foo = -> outer a = 30 b = 40 alert a, b # 30, 20 It requires to capture manually needed closured vars thought. Or indeed, maybe nevertheless to return the definition keyword but not in the definition semantics but in semantics of localizing the scope. E.g. x = 10 foo = -> let x = "test" y = 30 # Syntax Error (undefined global/local var) foo() alert x # still 10 Which semantically, repeat, sounds even not as "define a variable", but as "assume for this scope name Or e.g. Lua's Anyway, current breaking of abstraction -- that is, breaking the black-box with all it's "offal" which belong to this black-box (and belong by the right) is very arguable. Repeat, a 3rd-party programmer should be able to use any names of local (to the black-box) variables. And at the same time another 3rd-party programmer should be able to reuse this function written by the first programmer -- and without reviewing the code of the function. It's the main principle of the abstraction which seems just broken in Coffee. Notice, that in Ruby (in contrast with Coffee) the picture is a bit different. There most frequently a user works with methods ( Once again, Ruby's semantics in this respect is not the same as in JS/Coffee -- in JS all functions are closures (so the complete port of the semantics cannot be used as a best argument in explanations). And moreover, Ruby's design is not perfect in this question; consider it. So Lua's way with Dmitry. |
It may help (or, as it were, end) this discussion to note that coffeescript now has the |
odf how will it help to restore breaking principle of an abstraction? Should all programmers starts their functions with So I still propose to consider Python's way but with Dmitry. |
Well, I'm hoping that eventually the behaviour of |
Yes, I'm ware about what However, it's not the generic case. I mean, if you suggest to start every function with that One more time. Currently Coffee isn't even consistent in chosen strategy. On one hand it says -- "no name shadowing" (i.e. there are no two frames in the environment with the same name binding), which by itself, as I wrote above, already breaks the main principle of the abstraction and nested scopes. On the other hand, it contradicts to the first chosen way, and nevertheless allows two frames with the same name bindings -- it's achieved via formal parameter names or with the same And one more time. Arguing that "this is like in Ruby" isn't completely correct. Since repeat again -- in Ruby methods doesn't capture local vars of the surrounding context and create local vars via assignment (that's for the example with two 3rd-party programmers which share the code -- in Ruby and in contrast with Coffee they can do this safely). And Ruby closures (blocks, lambdas, procs, etc) do captures vars, but with closures usually already the user himself works and know his vars (though, again repeat, even this is not safe in Ruby and can be considered like a design flow). In JavaScript as you know, all functions are closures. So the case with two 3rd-party programmers fails breaking the main principle of an abstraction. Dmitry. P.S.: Exact proposals: a = 10 b = 20 c = 30 d = 40 foo = (a) -> outer b, c a = 100 b = 200 c = 300 d = 400 foo() console.log a, b, c, d # 10, 200, 300, 40 That is, Another way: a = 10 b = 20 c = 30 d = 40 foo = (a) -> local b, c a = 100 b = 200 c = 300 d = 400 foo() console.log a, b, c, d # 10, 20, 30, 400 That is, only |
I don't see the point. Why would you have all those variables on the file level when the functions you're exporting are not supposed to use them? If you don't want those bindings to be visible everywhere within your file, don't put them there. In general, if you don't pollute your scopes with unnecessary bindings in the first place, you'll have no problems with broken abstraction. I think the way to look at this is to consider the context in which a function is defined as a genuine part of it, not just an environment it was thrown into by accident. |
First of all -- do you agree that Coffee isn't even consistent in its chosen way? That is, "no two bindings with the same name in the environment chain" vs. "allow nevertheless two bindings with the same name via formal parameter names and If "yes", what the difference do you see from defining a local variable via formal parameter name and just a local variable with the same name? Why would you have all those variables on the file level when the functions you're exporting are not supposed to use them? OK, let's take a simple example. We (you and me) work together and should support the same source. I wrote a helper function somewhere above:
You three month ago write your function 1000 lines below: createWidget = -> square = new Square 10 square.onResize = (e) -> console.log e square You just used a local variable name, probably you didn't even know about my square function since that block of code was in my responsibility. What will be with my code execution then? How will we find the bugs? We'll of course find the bug sooner or later (and moreover since we in one project, you can argue that you should know all the source and all the used identifiers above. By the way, why "should" you if to consider the principle of separation programmer responsibilities?). But we can complicate the example, when I e.g. may reuse (in the simplest way just to copy-paste) some peace of code from completely another project to mine. Should I review all the "imported" sources to find out used variable names? If yes -- why "yes" since it's a direct breaking of the abstraction? Dmitry. |
I don't find your example convincing, at all. If I were unaware of which functions you defined on the file level, what would stop me from simply re-using the name |
Seems you just ignored my questions I wanted you to answer before your reasoning about the scope theory. Well, OK. Let's assume that you just agree with them. For details, look at any general scope theory paper (and especially on environment frames concept) -- it will help us in discussion not to mix definitions and in particular the definition of the "same scope". E.g. http://bit.ly/esnkD6 So if you reuse However, if you use the same name in your own scope, it's completely your right as the author of this encapsulated abstraction. Dmitry. |
Just a small note. OTOH, e.g. Erlang warnings about shadowed variable: X = 10, Foo = fun(X) -> X + 1 end, % warning X is shadowed Foo(20) It's for that the shadowing can be dangerous. However, in contrast with Coffee/Ruby, Erlang's variables are immutable and just pattern matched (i.e. you can't assign to Dmitry. |
Hi Dmitry, I agree with almost everything you said it. But some problems/bugs you'll find testing your code, doing TDD or something like that. |
@cairesvs yes, I'm aware about TDD. Though, the question was specially to underline the design flow (it wasn't a real asking how we should find the bugs ;) But, thanks anyway for mentioning. Dmitry. |
Despite this issue's tendency to break out in flames -- strict lexical scoping is very much a core principle of CoffeeScript, and it's a great issue worth discussing. Dmitry raises a couple of specific criticisms: First, that the lack of shadowed variables "breaks the principle of abstraction", because 3rd party code can't be blindly copied-and-pasted into the middle of your source. Second, that CoffeeScript is inconsistent, because function parameters do give you a way to shadow variables. He then proposes a change: tagging either local variable with a I'd like to persuade y'all that strict lexical scope is a defining feature of CoffeeScript -- a massive improvement over the manual We all know that dynamic scope is bad, compared to lexical scope, because it makes it difficult to reason about the value of your variables. With dynamic scope, you can't determine the value of a variable by reading the surrounding source code, because the value depends entirely on the environment at the time the function is called. If variable shadowing is allowed and encouraged, you can't determine the value of a variable without tracking backwards in the source to the closest So it's a very deliberate choice for CoffeeScript to kill two birds with one stone -- simplifying the language by removing the "var" concept, and forbidding shadowed variables as the natural consequence. This brings us to Dmitry's second point: It's still possible to shadow with parameter names, because of the nature of JS functions. I think that Trevor has the right idea here, we should be more strict about shadowing instead of less. It would be great to entertain tickets that either make parameter shadowing a syntax error, or a compile time warning. If we ever go down the road of having a Finally, the arguments about strict lexical scope making CoffeeScript a one-programmer-per-project language are total baloney, in my opinion. Accidentally clobbering an outer variable in a nested function is certainly possible ... but accidentally shadowing an outer variable is just as likely, and can also break your code. If I try to use a top-level define'd variable, in a nested function, but you've shadowed it, I'm hosed. And having strict lexical scope makes it much easier for me to determine what exactly has gone wrong, as opposed to "hunt for the Strict lexical scope isn't going to change in CoffeeScript proper, but I encourage you add Re-Closing the ticket. |
But that's just the thing. People make errors all the time, and at present, there's no way for the compiler to tell whether someone accidentally re-used a name from the including scope. I support the idea of issuing compile time warnings on parameter shadowing, but I think it would be even more useful if the programmer could indicate that they would like to use a name locally and assume that it's not taken, so that the compiler could issue a warning if they're wrong. The I'll continue this on #960. |
odf: Sure, in theory. In practice, well structured JavaScript code doesn't litter the top-level scope with lots of global variables, and keeps function scope shallow. This problem literally never comes up. And in the rare cases it does -- it's the same as when you try to reuse a variable name inside of a deep if/else statement -- you think "oh, I need to use a different name for this" ... and do just that. |
Join the club--it's much easier to fork it than convince its creator. ;) |
Yep -- and that's great. The reason why the entire source code is annotated is to make it easy for folks to modify the language to suit their fancy. |
@jashkenas |
Absolutely not -- what I was trying to make clear above is that CoffeeScript's strict lexical scope is an important feature ... having to distinguish your variables with |
jashkenas: I agree with you, but as others have pointed out, even though it's a rare problem, it could lead to really hairy bugs, especially when cutting and pasting code around as Dmitry mentioned. I don't know if it would justify introducing a new language construct, but since we already have satyr: Agreed! I haven't gotten around to it yet, but I'll definitely fork and fiddle. :) |
@odf |
cairesvs: That's not at all what I'm saying. I'm saying that there's not a "problem" here -- there's a major CoffeeScript feature. Certainly, you have to be more aware of your variable names than you would otherwise ... but the bonus side is that with better naming, you can entirely avoid the concept of There's no problem to be solved, here. |
jashkenas |
cairesvs: The
prints |
OK, thanks everyone for the discussion, it cleared some things in the reasons of accepted initially design. Some notes below. The issues I mentioned still stands. One of which (not mentioned here): in case if a name appears before the function - there is "one scope", and there are several scopes of the same name if the name appears after the function: foo = -> x = 10 x = 20 foo() console.log x # 20, local x was changed bar = -> x = 10 bar() console.log x # 10 # global x is changed with the same code function Although, it can also be argued as "complete and strict lexical scope" (first However, if there is only a reference to
You mean, there is a global foo = -> console.log 'foo' myFunciton = -> let foo = 10 # I shadow it /* some (long?) code */ yourFunciton = -> foo() # you try to call global `foo` and fail Yes, it seems fair note, since you should analyze the code of But the difference is -- the code of inner function is much less to check and if you write another inner function you should know the code of the surrounding one -- and this is not the same as to know the whole code of the file. So the issue with "imported" (copy-pasted) source stands and isn't baloney -- the user will check all the variables of the imported functions, and he will check in all outer scopes all presented (borrowed) names -- of both -- vars and functions, and he's forced to do this. So, OK, my mission here was to mention the issues I see. Though, repeat, at the same time I also like the idea to define vars without any keyword. P.S.: notice though, that even Ruby since 1.9 introduced the local scope with shadowing for the block arguments: I.e. old semantics of 1.8 (which was considered as ugly design): x = 10 p x # 10 [1, 2, 3].each{|x|} p x # 3 ? Strict lexical scope? And is fixed in 1.9 (though it's only for parameters, assignment to outer vars modifies them): x = 10 y = 20 [1, 2, 3].each{|x| y = x; z = x } p x, y, z # 10, 3, error This seems corresponds to the current Coffee's way. Though, consider also (about what I also mentioned above) -- methods in Ruby aren't closures and doesn't capture outer local vars, so that imported method can easily be copy-pasted: # my code a = 10 # global "local" var def foo a = 20 end # imported (copy pasted) def bar a = 30 end foo p a # still 10 bar p a # still 10 OK, let's leave it as is then. As I said, personally I can program in the current Coffee's way. Though, probably another fork will be good, yeah ;) Dmitry. |
Note that Ruby 1.9 also got a syntax for pure shadowing: |
@satyr
Ha, I didn't know about it (since do not practice Ruby much today, and especially 1.9., though, I know its initial design). Thanks for the example -- which again shows that even in Ruby (from which the design of Coffee was borrowed) considered such an ability as needed ("needed" means not as "shadowing is good", "needed" means the ability to write this code by another programmer -- to keep the abstraction principle). P.S.: Just another similar example to make it even clear: x = y = 0 # local variables 1.upto(4) do |x;y| # x and y are local to block # x and y "shadow" the outer variables y = x + 1 # Use y as a scratch variable puts y*y # Prints 4, 9, 16, 25 end [x,y] # => [0,0]: block does not alter these Dmitry. |
Thanks for the link @satyr. Appealing to authority is the last resort :) |
(I realize the discussion is old and closed, but:) Most of the time you access outer variables (i.e. from surrounding scopes), you only read them, don't you? Python does something very slick to stop me from clobbering outer variables: Assignment implicitly makes a variable local. For instance:
Localness is determined by static analysis for the entire scope:
In those rare cases where you want to overwrite the value of an outer variable (instead of shadowing), Python has the IMO this gives you the best of both worlds:
Thoughts? |
It's very tempting. Let's look at a reduced example of code that uses this style: watch = (source, base) ->
prevStats = null
compileTimeout = null
compile = ->
clearTimeout compileTimeout
nonlocal compileTimeout = wait 25, ->
fs.stat source, (err, stats) ->
return if prevStats?.size is stats.size
nonlocal prevStats = stats
compileScript() I think that it's quite hard to make the case that the above is more readable or "better" than the original: watch = (source, base) ->
prevStats = null
compileTimeout = null
compile = ->
clearTimeout compileTimeout
compileTimeout = wait 25, ->
fs.stat source, (err, stats) ->
return if prevStats?.size is stats.size
prevStats = stats
compileScript() ... especially when the former breaks symmetry of access. It's just When |
No changes to CoffeeScript is needed for the following convention for declaring local variables: local = (fn) -> fn()
foo = 'a global!'
local (foo, bar) ->
foo = 'a local!'
console.log foo # a local!
console.log foo # a global! CoffeeScript could be updated to define this Here's a contrived example... {log, sin, cos, tan, PI} = Math
myCos = (theta) ->
local (cos) ->
cos = sin(PI/2 - theta)
console.log myCos(PI/3) # 0.5
console.log cos # [Function: cos] Also... watch = (source, base) ->
local (prevStats=null, compileTimeout=null) ->
compile = ->
clearTimeout compileTimeout
compileTimeout = wait 25, ->
fs.stat source, (err, stats) ->
return if prevStats?.size is stats.size
prevStats = stats
compileScript() The only gotcha is that you may accidentally call #correct: a space after `local`
local (foo) ->
foo = 'bar'
#incorrect: may run if `foo` was declared to be a function in the outer scope.
local(foo) ->
foo = 'bar' Finally, you may not like the extra indentation, so you might be tempted to do... watch = (source, base) -> local (prevStats, compileTimeout) ->
compile = ->
clearTimeout compileTimeout
compileTimeout = wait 25, ->
fs.stat source, (err, stats) ->
return if prevStats?.size is stats.size
prevStats = stats
compileScript() so maybe CoffeeScript could allow the following shorthand... watch = (source, base ; prevStats, compileTimeout) ->
compile = ->
clearTimeout compileTimeout
compileTimeout = wait 25, ->
fs.stat source, (err, stats) ->
return if prevStats?.size is stats.size
prevStats = stats
compileScript() |
Funny -- I ran into this problem when reading CoffeeScript's own source code. Global variables defined here: Which makes it hard to tell in a given function whether an assignment is local or global. It'd be great if assignments were always local, and there were an |
there is a trivial way around this issue, it needs no change to the language, works just like Common Lisp's let and even doesn't look bad ;) foo = 5
func = -> ((moo = 1, foo, boo = 5) ->
foo = moo + boo
)()
alert func() + ' ' + foo
the only thing pissing from the language to make this really beautiful is Smalltalk-style code blocks :) |
foo = 5
func = -> do (moo = 1, foo, boo = 5) ->
foo = moo + boo
alert func() + ' ' + foo |
Hi, Cant we write I think its better solution than |
Just go back to JavaScript: ES2015 modules, structuring/destructuring, arrow functions, classes... and |
I figured that general discussion of CoffeeScript belongs to issue tracker. So here it goes.
It seems like coffee-script does not distinguish variable declaration and assignment. This means that, unless you are very careful with naming local variables, it's easy to unintentionally assign to a global variable. Consider this js code:
Here variable foo inside a function is local to that function. It is not possible to reproduce this code in CoffeeScript. CoffeeScript 'equivalent' will override global variable foo.
This may lead to hard to find bugs. What's your thoughts on this?
Regards,
-- Vitali
The text was updated successfully, but these errors were encountered: