Skip to content
This repository has been archived by the owner on Feb 19, 2018. It is now read-only.

CS2 Discussion: Features: Block assignment operator #58

Closed
GeoffreyBooth opened this issue Dec 5, 2016 · 65 comments
Closed

CS2 Discussion: Features: Block assignment operator #58

GeoffreyBooth opened this issue Dec 5, 2016 · 65 comments

Comments

@GeoffreyBooth
Copy link
Collaborator

GeoffreyBooth commented Dec 5, 2016

Splitting off from #35, this proposal is for just one new operator: :=, the block assignment operator. So the great advantage of let/const is its block-scoping, i.e.:

let a = 1;
if (true) {
  let b = 2;
}
console.log(b); // undefined

This is a genuinely new feature added in ES2015, and a great improvement over var, which explains why let and const have become popular features. This block scoping that let and const each offer is probably something that CoffeeScript should have, separate from the feature of const that means “throw an error on reassignment.”

The new operator would behave similarly to =:

  • a = 1 means, “declare a at the top of this function scope using var, and assign a here with the value of 1.”
  • a := 1 would mean, “declare a at the top of this block scope using let, and assign a here with the value of 1.”

let has the same issue as var, in that its declaration is hoisted to its entire scope (what MDN refers to as the temporal dead zone), so let declarations should be grouped together at the top of their block scope similar to how var declarations are currently grouped at the top of their function scope.

What about const? Well, there’s really no reason we need it. It protects against reassignment of variables, and that’s all it does. Per this comment, it probably has no performance benefit in runtimes; and since const reassignments can be caught by transpilers (whether CoffeeScript or Babel) such reassignments are in practice usually caught at compile time, not run time. If we decide we want to provide a way to output const, for full expressibility of ES2015 and for this protection against reassignment, that could be a separate feature added later.

So this CoffeeScript:

a = 1
b := 2

if (yes)
  b := 3

would compile into this JavaScript:

var a;
let b;

a = 1;
b = 2;

if (true) {
  let b;

  b = 3;
}

Note about bikeshedding: Please refrain from feedback that says how you prefer the let keyword to :=, or that you prefer some other sequence of characters to :=. Ultimately the keyword or operator chosen is a personal preference decision, and will be made by @jashkenas and whoever implements this. There was plenty of discussion about this on #35. Thanks.

@DomVinyard
Copy link

DomVinyard commented Dec 5, 2016

Strongly dislike :=, it's way too ambiguous as a visual metaphor. If you were introducing entirely new semantics, why not stick with the word let (for the sake of a couple extra keystrokes), however my vote would be to not expose two types of assignment.

Everything is var, or (probably is preferable) everything is let. Not both.

@edemaine
Copy link

edemaine commented Dec 5, 2016

@GeoffreyBooth You've probably thought about this, but why are you hoisting the lets to the top of the block instead of just leaving them at the assignment? Your example could alternatively be compiled to

var a;

a = 1;
let b = 2;

if (true) {
  let b = 3;
}

The difference is in treatment of the temporal deadzone. Your compilation removes the deadzone, so use of a variable never causes an exception, while this compilation causes e.g. f(b); b := 3 to throw a ReferenceError. I don't know for sure which is better, but the exception might be preferable. (Can't imagine why using b before assignment could be useful...)

This is a larger deviation from current = behavior, but also seems easier to implement (no hoisting). Eh, I guess we still might need to figure out which block the := is in to prevent multiple assignments to the same variable (which is illegal in ES6, so if we want to avoid outputting illegal ES6, need to detect in CS6).

@edemaine
Copy link

edemaine commented Dec 5, 2016

@DomVinyard There are lots of reasons to support let in some form, discussed on #35 in particular. Namely, let enables the programmer, when desired, to control where your variables are accessible, preventing accidental leakage and re-assignment. See also It’s a Mad, Mad, Mad, Mad World: Scoping in CoffeeScript and JavaScript (for example).

@rattrayalex
Copy link
Contributor

Speaking as the person who originally proposed the := operator, I actually agree with @DomVinyard on this one.

I'll try to write up the reasons soon.

@GeoffreyBooth
Copy link
Collaborator Author

@edemaine Yes, the declaration is hoisted away from the assignment to follow the pattern established by = and var. See http://coffeescript.org/#lexical-scope. I feel like both should behave the same way for consistency (though obviously the let declarations would be at the top of the block scope, not the top of the function scope).

@GeoffreyBooth
Copy link
Collaborator Author

@DomVinyard and @rattrayalex, the discussion in #35 was overwhelmed by bikeshedding of people arguing whether let or := were better. If you want to continue that argument, can you please open a dedicated thread for it? I think it would be more productive to improving this proposal if we could just take it as a given that the implementation will be :=. Thanks.

@jashkenas
Copy link

I agree with @rattrayalex and @DomVinyard — CoffeeScript should try to be as minimalistic and boiled down to the essence as we can make it.

JS now has three types of variable assignment (four, if you count named function declarations). CoffeeScript should have one.

That said, CS2 breaking compatibility might be an excellent opportunity for us to switch over from var to let, wholesale — if you guys think it's truly a better choice.

@vendethiel
Copy link

👍 for let. but we need to change some hoisting code.

@connec
Copy link

connec commented Dec 5, 2016

@vendethiel or make a break in those situations. I tried to outline something along those lines in this comment.

@edemaine
Copy link

edemaine commented Dec 5, 2016

@jashkenas All hoisted variables can probably be switched from var to let; that's the experiment of GeoffreyBooth's let branch (which still needs a bit of debugging). The point of the := operator is to enable creating variables localized to a given scope, i.e., hoisted only to the containing block. I know this idea has been raised before (it could even be added to CS1, by renaming variables), and you're famous for rejecting it. I'd like to think the outcome will be different this time because of ES6's introduction of let, so JS now supports variable scopes that are not function-wide. I believe CS should embrace this JS feature, given its many uses and ways it can help a programmer be more safe. I see your point about it increasing the complexity of CS, but I also worry about being too simplifying to the point of losing useful features.

@YamiOdymel
Copy link

I think we should have a shorthand for let since we don't use var but = in CoffeeScript.

But as a Golang Developer, := sounds more like var to me.

@edemaine
Copy link

edemaine commented Dec 5, 2016

@YamiOdymel Reading a little about Golang, it seems Go's var is semantically equivalent to JavaScript's let (the scope is the containing block). So the semantics proposed for CS := exactly match Go's semantics for :=.

@GeoffreyBooth
Copy link
Collaborator Author

@jashkenas I guess when you mean we should only use let, do you mean only use let and only block scope? My let branch simply outputs let wherever var is output now, which effectively means that these are function-scoped lets, which we can certainly do if your goal is just to banish the var keyword from our output. We could even do that and still add := for block-scoped let output.

I would be very hesitant to get rid of function-scoped variables (i.e. what we have now). For example, consider this code:

if error
  message = 'Damn!'
else
  message = 'Woohoo!'

alert message

which currently becomes:

var message;

if (error) {
  message = 'Damn!';
} else {
  message = 'Woohoo!';
}

alert(message); // 'Damn!' or 'Woohoo!'

If we have only block-scoping, it would be output as:

if (error) {
  let message;

  message = 'Damn!';
} else {
  let message;

  message = 'Woohoo!';
}

alert(message); // Undefined

This can be refactored to still work with only block scoping, by adding message = undefined or similar on the first line, but I think this illustrates how drastic of a breaking change banishing function scope would be. I think we need both scopes, if we’re adding block scoping at all, hence the two operators.

@rattrayalex
Copy link
Contributor

I agree with @jashkenas that there should be one, and only one, way to declare variables in coffeescript. It's coffeescript.

The example you gave is illustrative; it should be written as:

message = if error 
  "Damn!" 
else 
  "Woohoo!"

which translates as intended.

A more complex example would require intentional hoisted declaration:

a = b = null
if error
  doSomething()
  doSomethingElse()
  a = "Damn!"
  b = "What a bummer..."
else 
  a = "Woohoo!"
  b = "I'm so happy!"

This is the "function scoping" you're looking for; not :=. It's much more clear, intentional, and simple.

let should be hoisted only to the top of a block scope to avoid temporal dead zones, not to the top of a function. That's just a misleadingly-declared var, and removes the value of let.

The biggest problem with this proposal, of course, is backwards incompatibility and an upgrade path. Fortunately, it should be fairly doable to use the coffeescript compiler to write an upgrade tool that checks for any differences between function-hoisted and block-hoisted variables and either warns the developer of each instance or directly inserts varname = null in the original source, allowing them to audit thereafter.

@rattrayalex
Copy link
Contributor

To expand upon why an operator like := would be harmful for this feature...

Imagine you have some code like this:

bar = -> 
  for i in arr
    x := i * 2
    if i > 3
      x = 3
      foo(x)

If you then later decide you don't need x := i * 2 anymore, your code will look like this:

bar = -> 
  for i in arr
    if i > 3
      x = 3
      foo(x)

and x is, all of a sudden, a function-level variable leaking outside the for block. In a world where this kind of bug introduction is possible, you'll constantly need to check every assignment for whether it is first declared with := or not, and whenever you remove a :=, you'll need to check for all possible assignments to the variable.

(In case you're wondering why I proposed := for const given the above, const is not vulnerable to the above bug; you can't reassign later. That said, I support the decision not to include const in coffeescript 😄 )

More generally, if we give users the option between function-level and block-level scoping, they're going to have to think about it all the damn time. And since block-level scoping is a best-practice, but not strictly necessary in most cases, developers will constantly find themselves saying, "bah, is it worth adding this extra operator here?". Inconsistency is likely to result.

On the other hand, with a consistent rule of "all variables have block-level scope and can be reassigned", there is a simple calculation, and the handful of times that variables should belong to a function scope, they can be easily declared as noted above, with varName = null.

@GeoffreyBooth
Copy link
Collaborator Author

I think removing function scoping is a huge breaking change with little benefit. It’s probably better to do nothing than to redefine = to be always block scoped.

@rattrayalex
Copy link
Contributor

Do we all agree that the two best options are:

  1. keep function scope only, either with var or let
  2. move to block scope only, with let

?

@triskweline
Copy link

@jashkenas:

JS now has three types of variable assignment (four, if you count named function declarations).
CoffeeScript should have one.

The distinction between let and var is meaningful: It lets us not accidentally re-assign variables.

We should make CoffeeScript as simple as possible, but not simpler. I believe this is too simple. Please reconsider.

@connec
Copy link

connec commented Dec 6, 2016

I want moving wholesale to let and block scope to be a good plan, but compared to finding a function (-> or => or class), the lack of explicit braces makes it much harder to identify where a scope begins.

->
  # Currently it is clear this is all in a single scope, but if we move to block scope...

  if true
    a = 1 # Is this block-scoped? Yes.

  while true
    a = 1 # Is this block-scoped? Yes.

  if a = 1
    a # Is this block-scoped? No (unless we special-case assigns in `if` condition).

  for a in array
    a # Is this block-scoped? Yes.

  while a = array.shift()
    a # Is this block-scoped? No (unless we special-case assigns in `while` condition).

  obj =
    foo: a = 1 # Is this block-scoped? No (unless we generate a block).

  f \
    a = 1 # Is this block-scoped? No.

Even if we add a let or := operator this wouldn't entirely go away, especially given CS' "everything is an expression" policy.

if a := 1
  a # Where is `a` bound? `if (let a ...)` is invalid JS.

a for a := in array # Where is `a` bound? `for (let a ...)` is _valid_ JS.
                    # Also not sure what this should look like with `:=` which is important
                    # given it's one of the most useful cases of `let`.

@rattrayalex
Copy link
Contributor

rattrayalex commented Dec 6, 2016 via email

@connec
Copy link

connec commented Dec 6, 2016

I agree they are largely anti-patterns (assignment in expressions in particular), I guess the issue that distinguishing a block from a continuing expression, or determining when the block starts (for vs. if) might not always be trivial.

@GeoffreyBooth
Copy link
Collaborator Author

GeoffreyBooth commented Dec 6, 2016

@rattrayalex yalex No, I don't think it's down to just those two choices. The third choice is to keep = as it is and add :=, and therefore have both block and function scoping.

@edemaine
Copy link

edemaine commented Dec 6, 2016

@connec The original proposal makes the choices for := pretty clear: the let goes in the nearest containing block. So if a := 1 compiles to let a; if a = 1, and a for a := in array (or however we figure out how to write it) compiles to for(let a in array) a. Also while a := array.shift() ... compiles to while(let a = array.shift()) ... (I believe while loop iterations get their own block, just like for loops). In general, we would follow the blocks defined by ES6.

Also, strong preference for keeping = like it is. I think many underestimate the huge amount of code this would break -- also much harder for beginners to learn (cf. Python, which follows CS = within a function).

@jashkenas
Copy link

I'm in fairly complete agreement with @rattrayalex in this thread. Especially this comment: #58 (comment)

My thoughts:

  • For starters, this isn't really a giant deal. We've lived with function scope for many years in JavaScript, and although it's not ideal — once you're used to it — it's not really an annoyance in day-to-day life, ever. It becomes normal.

  • Using let, but auto-declaring it at the top of functions is pointless. Or to be more colorful, it's an abomination — a perversion of let's whole point.

  • There should only be one way to declare variables in CoffeeScript. Not having to think about two different mental models of variable scope at the same time is precisely and specifically the raison d'etre of CoffeeScript in the first place.

  • If y'all think that block scoping is inherently superior to function scoping, (I too feel that way, but only lukewarmly), then the time to make the breaking change is now. CS2 is the only big breaking change we've had in 6 years — so do it now, or don't do it at all.

@DomVinyard
Copy link

There should only be one way to declare variables in CoffeeScript. Not having to think about two different mental models of variable scope at the same time is precisely and specifically the raison d'etre of CoffeeScript in the first place.

Amen.

If y'all think that block scoping is inherently superior to function scoping, then the time to make the breaking change is now.

+1

@GeoffreyBooth
Copy link
Collaborator Author

GeoffreyBooth commented Dec 6, 2016

There is perhaps another option, that might please everyone: detect whether a variable is used only in its block, and if it is, declare it with let in that block; or else declare it with var at the top of its function scope like we do now.

if error
  time = Date.now()
  message = "Error at #{time}!"
else
  message = 'Woohoo!'

alert message

Becomes:

var message;

if (error) {
  let time;

  time = Date.now();
  message = `Error at ${time}!`;
} else {
  message = 'Woohoo!';
}

alert(message);

There is still only one way to declare variables in CoffeeScript, but we get the benefits of block and function scoping. This way, people who use variables like i and write lots of code in single files—the people most vocal about the shadowed variable problem—get their block scoped variables, without the need for a new operator or removing function scope.

@jashkenas
Copy link

jashkenas commented Dec 6, 2016

There is perhaps another option, that might please everyone: detect whether a variable is used only in its block, and if it is, declare it with let in that block; or else declare it with var at the top of its function scope like we do now.

Nope. Not quite.

Think it through and you'll see why that doesn't work. If a variable is only used within an inner block, then it makes no difference if it's declared with a var or a let — both produce an identical result. And as soon as you mention it outside of the block, it becomes a var.

This proposal is just a complicated implementation of var scope.

Edit: I didn't think through @GeoffreyBooth's full proposal — see below.

@GeoffreyBooth
Copy link
Collaborator Author

And as soon as you mention it outside of the block, it becomes a var.

I meant only when it’s used in a parent block scope does it become a var. In other words:

if error
  time = Date.now()
  message = "Error at #{time}!"

alert message # "Error at 123456789!"

if error
  console.log time # undefined
var message;

if (error) {
  let time;

  time = Date.now();
  message = `Error at ${time}!`;
}

alert(message);

if (error) {
  console.log(time);
}

time is used in its block scope, but not in any ancestor block scopes; so it gets defined with let. message is used both in its block scope and in its parent block scope, so it gets defined with var.

@connec
Copy link

connec commented Dec 12, 2016

I think the idea of choosing variable scope based on usage has been discarded, e.g.

i for i in array
i # is this trying to bring `i` above into the function scope, or is it referencing 
  # some other `i` (perhaps even a global)?

Always using let in loops would sit well with me generally. You'd just have to be explicit when you want to access the variable outside the loop:

i = null
i for i in array
i # all `i`s are the same reference

The problem then is that we have two types of implicit scope, compounding the uncertainty about what variable are declared where.

i for i in array
i # was `i` above already used somewhere in this scope, and so is available? Or is it
  # unassigned here?

Having an explicit syntactic modifier would eliminate the guesswork:

i = null
i for let i in array
i # `i is null`, since the `i` in the loop was specified with `let`

That said, if one block scoping thing gets in, there will probably be a push for others, and my preference would be to have "all function scope" rather than adding scope switches that can be used anywhere. Just in this particular case I think it makes sense to add a syntax for what is a fairly common ask from people regarding loops (and even a source of bugs). Particularly when the only supported solution (wrap it in a closure), is likely to be quite expensive in comparison.citation needed

@DomVinyard
Copy link

The general consensus for this particular issue seems to be trending towards "leave things as they are" with a view to either block-scoping by default at some point in the future, or scope based on usage at some point.

As for now, no action?

@GeoffreyBooth
Copy link
Collaborator Author

I think compared with changing how scope works in the language overall, no action. We need function scope.

But I still think we should discuss the merits of this proposal—the original one at the top of this thread. It doesn’t break backward compatibility, it finally adds block scoping to CoffeeScript, and it solves the “clobbering variable” problem. It does introduce a second way of declaring a variable.

I think we’ve determined that there’s no silver bullet solution to have two types of scopes (function and block) in CoffeeScript without two ways to declare variables. (At least, not for the reasons that people want both scopes.) So I guess the question is whether we value the simplicity of a single way to declare variables over the power of having two types of scopes. Part of that consideration should include how popular let and const have become in the JS world.

@edemaine
Copy link

edemaine commented Dec 12, 2016

@GeoffreyBooth If it's alright with you, I'd like to try writing a new issue proposing changing the normal = scope rules to "put let x in the tightest block containing all references to x" (your intermediate proposal). This is very close to present behavior --- the current behavior uses "function" instead of "block". Amusingly, the current description of = seems consistent with this alternate behavior --- it's just that the notion of "scope" has changed:

The CoffeeScript compiler takes care to make sure that all of your variables are properly declared within lexical scope — you never need to write var yourself. [...]
Notice how all of the variable declarations have been pushed up to the top of the closest scope, the first time they appear.

I think this proposal is somewhat independent from the := proposal (this issue), as we could do both, or one, or neither. So perhaps should be considered separately?

@GeoffreyBooth
Copy link
Collaborator Author

Sure @edemaine, please create a new issue and link to this one. As far as I can tell though it doesn’t give people what they really want. They want block scope because they want to use it to protect them from themselves: not accidentally clobbering parent-scope variables, etc. We can have the compiler automatically declare variables as block scoped (or even as const), but that doesn’t solve the problem of protecting oneself.

It’s perhaps better than doing nothing, as our output would be more idiomatic ES and might be very slightly more performant, if fewer variables get declared overall; but those are the only gains I can see. Please feel free to prove me wrong 😄

@edemaine
Copy link

edemaine commented Dec 12, 2016

OK, the intermediate proposal is now in #62. Hopefully that explains why it's useful, as well as the limitations you describe. It does not give the programmer control to prevent accidental lexical scoping problems (just like current CS). One alternative approach for that, though, is in another new (but modest) proposal, #61.

@DomVinyard
Copy link

So I guess the question is whether we value the simplicity of a single way to declare variables over the power of having two types of scopes

This is fundamental. It might be worth polling people directly on. To me, a single way to declare variables is one of CS's most powerful features. It should not be discarded lightly.

Not sure how many others feel like that, perhaps i'm an outlier.

@triskweline
Copy link

To me, a single way to declare variables is one of CS's most powerful features

To me it's a footgun that has cost me countless hours to debug. I might also be an outlier, but this issue must affect any program with long or nested closures.

Note that some form of const would also solve the problem of accidentally reassigning variables at runtime. If we had const, I could help the compiler save me from myself:

window.myModule = do ->
  const data = -> # ...
  const func = ->
    data = getSomeData() # throws compiler error

If we cannot agree on how to scope, can we maybe agree on having const? After all "prevent reassignment" and variable scoping are orthogonal features. We can have constant variables with Coffeescript's original scoping.

@mitar
Copy link

mitar commented Dec 13, 2016

I think that static-typing features like const should be part of the general type-checking optional addition on top of core CoffeeScript language.

@JavascriptIsMagic
Copy link

@DomVinyard coffeescript has at least 4+ ways to declare variables: a = undefined, class a, do (a) -> and a for a in [undefined] all can be used to declare variables (and this leaves out destructuring like {a} = or [a] = or functions ({a}) -> and then there is a?.b = which will only set a.b if a exists).


At the time CS1 was written "it was just javascript" which meant it used var semantics. Since then new features of javascript have been implemented and can be used in some cases without.

I see CS6 as playing catch up with javascript at this point to address the problem of people migrating their code bases away from coffeescript because it "lacks ES features"


( And looking at the progress so far in the last few months CS6 is amazing! I can't tell you how happy I am to see coffeescript being updated! I write in CS6 now in a new project at work, been using the new 2 branch since async/await, I just wish work and life commitments where not all sucking my time so much at the moment, I'd actually dig into the code and actually do something useful here... )


If you where to ask me, I would say that coffeescript is "unfancy javascript", that is to say a "syntax lite" language that should eventually implement all of javascript's feature set whenever possible.

My concern is that leaving javascript features out of coffeescript is not good for the language in the long term because it deters people from writing in the language, and tempts people to switch away from it.


@henning-koch I've shot myself in the foot so many times that now whenever I declare a variable, I do a "find" in my editor for that variable name, and scan the file to make sure I haven't used it already, and I also try to keep my names as unique as possible because I can't be too careful. Sometimes I use do (a) -> just to declare a variable that has a duplicate name.... Making modifications to code is a pain too because I am worried about if I declare something, is there code some place far below that uses the same name?

const is a nice feature of javascript that I think should be added in addition to the normal var function scoping, and let like block scoping, but it's a separate thing, although it does solve the clobbering issue.

const is nice for code maintainability, if you are fixing a bug and add a const in the middle of a large block of code (you may not have written yourself), the compiler should tell you if you are using only = some place deeper in your file that would accidentally clobber it otherwise.

I won't argue for things like all variables should be const in an immutable compiler flag, nor do I really care that much about what the syntax will look like in the end, as long as the feature itself makes it in some usable form.

@mrmowgli
Copy link

Personally I would be perfectly happy allowing the const/let keywords in CS6, and making sure it was optional only. I don't find the current version of CS causing me those kinds of issues, but I can see the value. There are many times when I really want a pass through, where CS syntax would let me use ES2015+ constructs like get /set or const and just pass those through.

In other words a lot of the modifiers aren't currently supported, but I think they will end up getting added. I personally don't need special symbols but it would be nice to be able to pass through a const or a get now and then.

That being said, for now it's medium priority and also covered in several other threads, like classes. Take the time to read up on the issues, and potentially start looking at the code for for coffeescript and put together a pull request :)

@connec
Copy link

connec commented Dec 13, 2016

This is fundamental. It might be worth polling people directly on. To me, a single way to declare variables is one of CS's most powerful features. It should not be discarded lightly.

I agree with this statement.

coffeescript has at least 4+ ways to declare variables

Whilst there are 4+ syntaxes to declare variables, the variable is declared in the same way every time (with some additional acrobatics for do): the variable is declared at the closest enclosing function scope, and assigned where the expression occurs.

@GeoffreyBooth
Copy link
Collaborator Author

GeoffreyBooth commented Nov 25, 2017

Closing as the consensus for CoffeeScript 2 was “no action.”

It seems like there might be a consensus in a theoretical CoffeeScript 3 that we shift from the current function scoped variables to always block scoped variables. That should get its own thread though.

@coffeescriptbot
Copy link
Collaborator

Migrated to jashkenas/coffeescript#4951

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests