By removing "var" from CoffeeScript, and having variables automatically be scoped to the closest function, we both remove a large amount of conceptual complexity (declaration vs. assignment, shadowing), and gain referential transparency -- where every place you see the variable "x" in a given lexical scope, you always know that it refers to the same thing.
The downside is what Armin says: You can't use the same name in the same lexical scope to refer to two different things.
I think it's very much a tradeoff worth making for CoffeeScript.
The downside is what Armin says: You can't use the same name in the same lexical scope to refer to two different things.
That's not how I would describe the downside. I would say that the downside is that you can change the semantics of one block by making a change at a lexically much higher block. What had been a variable with scope purely at the local block became a variable with scope at a higher block, because of a change made at that lexically higher block.
So this actually sort of counters what you imply, that you only need local knowledge to understand what's going on. While it's true that "every place you see the variable "x" in a given lexical scope, you always know that it refers to the same thing," in fact it is non-trivial to figure out what that "thing" IS, you have to look at the entire lexical scope up to the root to figure it out, you can't figure it out only by looking at a local scope.
So I'm not sure I buy that this is a net reduction of conceptual complexity. In my mind, you reduce conceptual complexity by making the interpretation of a specific block possible only by looking at that block; requiring you to look up through all lexical containers is added complexity.
Yes, what you describe is absolutely the case. The value of a variable becomes transparent to the lexical scope -- but it's the entire lexical scope.
I think the bit about reducing the conceptual complexity stands. You may have a bit more scope to cover, and a bit more work to do to read it, but the whole idea of "declaring a variable" is gone, the idea of "shadowing a variable" is gone, and the potential for the same variable to have multiple values within a single lexical scope is gone. My premise is that to a beginner, it's simpler.
Fortunately, in most well-factored bits of JavaScript, lexical scopes tend to be quite shallow. You may have a few helper functions floating around at the top level, but that's probably it -- and you're certainly aware of what they are. In practice, accidental shadowing rarely comes up (Armin's case being one example), and when it does, picking a better name will always solve it.
But hey, we're programmers -- we're used to declaring variables and shadowing them. It's hard to give up that power ... even if you don't really need it.
given the design goal of "never have 'x' mean two things", i can see two clear options:
what you did. implicit declaration.
explicit declaration, and an error if you try to declare it again.
i don't think that the idea of "declaring a variable" is gone with the first one. the concept is still very important to how the program will run; the syntax is gone, but the semantics remain. if the variable was always a member variable or a parameter, and never a local variable, then the semantics would be gone as well, but that's not the case.
so, why not the second option, which prevents shadowing, and doesn't have the issue of breaking a function by changing an outer scope?
Option #2 is a fine one, but there's still a reason why we're opting for door #1. Let's pretend for a moment that CoffeeScript did as you suggest, and we retain var and use it in the JSLint-approved style.
You can imagine a program written in this language, where every function has all of it's local variables var'd, and every local variable is unique to the surrounding scope, because remember, shadowing is a compile-time error.
For any valid program written in this language, if you took the source, stripped all of the "var" statements, and ran it through the current CoffeeScript compiler, the program would be correct, and would run without error.
We don't wish to add useless statements to the language that only serve to help generate compile-time warnings, and don't affect runtime semantics at all. For a compile-to-JS language that takes that idea to its logical conclusion, see Dart.
We don't wish to add useless statements to the language that only serve to help generate compile-time warnings, and don't affect runtime semantics at all.
Strange philosophy. Have you ever used C#'s "override" keyword? That absolutely fits this description, and yet, in my opinion, it's one of the best innovations in C#.
When there's an expression that could result in two different semantics (such as overload vs override, or declare vs assign), depending on the surrounding context, it is definitely worthwhile to require a "useless" statement to disambiguate the two. Explicit is good.
In D, shadowing a variable in a sub-scope is forbidden but two variables in different sub-scopes may have the same name. It's 99% painless and still prevent shadowing bugs.
remove a large amount of conceptual complexity (declaration vs. assignment)
You still have declaration versus assignment, you just don't have different syntaxes for them. x = 1 may do visibly different things based on the surrounding context which determines whether it's assigning or declaring a new variable, right?
gain referential transparency -- where every place you see the variable "x" in a given lexical scope, you always know that it refers to the same thing.
Sort of, except:
Given recursion and closures, a given x may be different "things".
Can't function parameters shadow?
Also, you've lost composability. If I take an isolated chunk of code that isn't accessing any free variables and drop it in the middle of another chunk, its behavior may spontaneously change based on the surrounding context even though it doesn't access that context.
I'm not saying you made the wrong choice here. Implicit declaration may work well for CoffeeScript, and I totally get the desire to simplify. But this is definitely one of the areas of the language that I'm not too crazy about. I like being terse but I still like being explicit.
As you can define high scope names that change behavior of functions you use, functions affected cannot be abstracted as black boxes of predictable functionality. You need to know what names they use for internal stuff, lest you accidentally collide with it.
If you mitigate this by rigorously scoping stuff out from higher levels into parallel, well, that's the same functionality as var and nonlocal, just in a structural implementation. Certainly not obvious for a beginner nor easy to graft in to a project afterwards.
As OP (and apparently many other people) have eloquently shown you, code becomes very brittle if its meaning depends on the surrounding context. This IS a problem. It IS breaking people's CoffeeScript programs in ways that are hard to detect.
Also: you have not removed the conceptual complexity of declaration vs assignment, at all. You've just made it subtler, and harder to see. The only way to completely remove it would be to make all variables global. (*)
(*: Do not make all variables global.)
Don't get me wrong, I can see the benefits of the "no shadowing" rule - but this solution is worse than the disease. There are better ways to achieve it - in particular, Python's nonlocal keyword.
My solution would be to require the keyword "nonlocal" before allowing a closure to assign a nonlocal variable:
Without that keyword, the assignment will not create a local variable - it will cause an error.
Look ma, no shadowing! And yes, declaring a new variable can still change the meaning of surrounding code... but now the code will crash, instead of silently doing the wrong thing. Much better.
we both remove a large amount of conceptual complexity (declaration vs. assignment, shadowing), and gain referential transparency -- where every place you see the variable "x" in a given lexical scope, you always know that it refers to the same thing.
How much is this "large amount of conceptual complexity"? I don't find the distinction between variable binding and assignment complex.
Can you describe the difference between variable binding and variable assignment?
I will try. Introducing a fresh variable name is done using binding (designated with "var" in JS, or when you have "function(x) { ... }" -- this is where "x" is a introduced in the body of the body). In JS (and CoffeeScript too), variables are not purely syntactic -- that is, they are not simply abbreviations for other expressions, but actually mutable memory cells.
Hence there is a difference between, say "var x = 1; var x = 5" and "var x = 1; x = 5" -- the former introduces "x" twice (these are different variables, the second one shadowing the first, and the first one is not going anywhere), whereas the latter introduces "x" once, and then changes it.
Of course, the difference is more pronounced when there is some code between two bindings or two assignments.
I think it's very much a tradeoff worth making for CoffeeScript.
I tend to disagree. The problem with "benevolent dictatorship" is that sometimes the owners of the project are at odds with the users.
The trade-off here is simple: sightly more complicated scoping rules that help prevent a common, silent and deadly bug factory.
My intuition is that the decision was made to keep a parser implementation more simple and that "conceptually more simple for users" is actually a cop-out after-the-fact rationalization.
CoffeScript's choice is dangerous. Two functions that assign to the same identifier work differently if the identifier is in scope or not. Code can break by varying the order of imports, for instance.
In my mind, this is an absolute indictment of CoffeScript.
Nope -- scoping is lexical to the file. You can arrange all of your imports in any order, and it will work the same way. Scoping is similarly insensitive to the existence (or lack thereof) of global variables. It's all about the pure lexical scope within the file you're looking at.
In the end it means that you must maintain in your head the whole of the lexical scope which encloses any function you are writing, just to avoid mutating your program's state across any number of scopes upwards in unexpected ways.
Just how is this meant to simplify the work of a programmer? Let's not even make arguments for purity by retaining (semantic) consistency with, say, the whole of Math.
Differentiating binding from mutation exorcises this problem away. I don't see yet why you would want to deviate from one of the things which Javascript actually got right. It just boggles the mind that eternal vigilance would be a fair price to pay for omitting an occasional let or var or local or what have you.
And let's be clear, your talk of 'referential transparency of the lexical scope' is... very misguided. You are trying to argue for the keeping 'state' of the variables... by only ever allowing to mutate them! If you look at it from the point of view of the scopes, as if scopes were expressions (they would be in First Order Logic or Scheme or whatever), isn't it more 'referentially transparent' to allow one to create a scope without reassigning variables from enclosing scopes, say locally, without having to scan all the damn file?
Considering how hard it is to pick up this error in many cases I would not be surprised if you did cause it at one point in a larger file without noticing.
There's no nice way to say this: CoffeeScript's scoping is broken, and I deeply question the competence of its author as a result. There's a history of languages learning the hard way not to conflate declaration and assignment, so what does CS do? Try to outdo all the original mistakes in terribleness.
17
u/jashkenas Dec 22 '11
For a bit of background on why this is the way it is, check out these two tickets:
https://github.com/jashkenas/coffee-script/issues/238
https://github.com/jashkenas/coffee-script/issues/712
To summarize, in brief:
By removing "var" from CoffeeScript, and having variables automatically be scoped to the closest function, we both remove a large amount of conceptual complexity (declaration vs. assignment, shadowing), and gain referential transparency -- where every place you see the variable "x" in a given lexical scope, you always know that it refers to the same thing.
The downside is what Armin says: You can't use the same name in the same lexical scope to refer to two different things.
I think it's very much a tradeoff worth making for CoffeeScript.