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 let and const operators #35

Closed
GeoffreyBooth opened this issue Sep 12, 2016 · 38 comments
Closed

Comments

@GeoffreyBooth
Copy link
Collaborator

GeoffreyBooth commented Sep 12, 2016

Building off of #1 and this comment, 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 dramatic improvement over var, and a big reason why let and const have become popular features. This block scoping is probably something that CoffeeScript should have, separate from the feature of const that means “throw an error on reassignment.”

We could have both, via := and :== operators (or whatever two operators people think are best):

  • a := 1 would mean, “declare a at the top of this block using let, and on this line assign a with the value of 1.”
  • a :== 1 would mean, “declare and assign a on this line using const. If it gets reassigned later, throw an error.”

We don’t necessarily need the second operator, if we don’t care to give the “throw an error on reassignment” feature.

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. const must be declared and assigned on the same line, so it can’t get moved up.

So this CoffeeScript:

a = 1
b := 2
c :== 3

if (yes)
  b := 4
  c :== 5

would compile into this JavaScript:

var a;
let b;

a = 1;
b = 2;
const c = 3;

if (true) {
  let b;

  b = 4;
  const c = 5;
}
@kirly-af
Copy link
Contributor

Hi Geoffrey, thanks for starting this discussion.
Personally, I rarely used felt the need for let in CS. As you mentioned, the first issue it solves is, variable hoisting out of declaration block:

if true
  foo = 42
console.log foo # 42, expected undefined

To solve the hoisting issue we could just transpile to let by default. The behavior would be more natural in my opinion.

The second issue it solves is shadowing:

foo = 42
unless bar?
  foo = 21
console.log foo # 21, expected 42

In this case, we need a way to shadow the inner block declarations. Maybe we could opt for something like the suggested := but I dislike both the operator and the fact it's not explicit enough.

I'd prefer something like the non-standard JS let expressions:

foo = 42
if true
  let (foo = 'a', bar = 'a') console.log foo, bar # a b
console.log foo #  42

There is also a non standard let block:

foo = 42
if true
  let (foo = 'foo', bar = 'bar')
     console.log foo
     console.log bar
console.log foo

As far as const is concerned, we probably could just use uppercase characters to declare constants like ruby does.

@GeoffreyBooth
Copy link
Collaborator Author

@kirly-af two quick notes: the let block was a standard that was proposed but then retracted, so I don’t think it’s something we should adopt; and ECMAScript const is very different from constants in other languages. const only prevents reassignment of a variable symbol; it doesn’t prevent the value of the variable from changing. Try in your Chrome DevTools console:

const obj = {};
obj.a = 42;
obj.a = 43;

This is a big reason why const is used all over the place in ESNext code.

@kirly-af
Copy link
Contributor

Hi Geoffrey

I'm aware let blocks/expressions are non standard ES features. I'm not sure we should care about that, we are free to adopt any construct in CoffeeScript syntax. Anyway it was just a sintax suggestion, my point was to have explicit redeclaration (ie. let on its own would be fine as well) vs := operator.

Concerning const, I'm aware of it's behavior. However it's definitely not our concern here. Correct me if I'm wrong but this discussion only focuses on declaration syntax, const behavior could even be delayed to runtime (as you said throw on reassignment). Again my point was uppercase vs :==.

Let me know if some part of what I say is not clear, I don't mind elaborate more.

@GeoffreyBooth
Copy link
Collaborator Author

The uppercase issue has been discussed already, if not here than in #31 or #1. There were a lot of objections, because in most people’s ES2015 code most variables are consts. And an uppercase name implies that the variable is immutable, like a constant in other languages, when it isn’t in JavaScript.

CoffeeScript doesn’t have declarators such as var or function (though it does have class). I think it would be preferable to think of a new operator for let/const rather than introduce more anti-pattern declarators.

@kirly-af
Copy link
Contributor

I agree let/const keywords should be avoided if possible.

My main concern is that ES2015 block variables will be completely unintuitive:

if true
  foo = 'a'
  bar := 'b'
console.log foo # 'a'
console.log bar # RefError thrown

For this reason, I think we should transpile all declarations to const (when possible) or to let instead of var everywhere. This way, the first console.log would throw as well. Of course, that would be a breaking change, but I think this is preferable.

Still, I really think, we should not allow shadowing in a language like CS. Python doesn't have block-level (understand if/while/for/switch) variables, like CS, and it's just fine. Even though I see how useful that feature is, I think it's probably just going to be error-prone.

@GeoffreyBooth
Copy link
Collaborator Author

I think redefining what = does is only possible if it isn’t a breaking change. Like if we can somehow know definitively that we can declare a variable with let or const instead of var and it won’t break anyone’s code, then sure, let’s use let or const. But if the behavior of = changes and such a change breaks existing code and forces refactoring, I think that that is unacceptable. This is such a core feature, anything that breaks = would require refactoring projects from top to bottom. No one would ever upgrade CoffeeScript.

@kirly-af
Copy link
Contributor

I get your point, and you're completely right. Though I see more this change as a fix. Consider the example I used earlier:

if true
  foo = 42
console.log foo # 42

To me this is something that shouldn't be work. At least if we use let instead of var, foo will be undefined at the console.log level. Of course the best would be to make it invalid (ie. compiler error).
If this behavior is intended the refactoring would be limited to first initialize the variable before the if block.

@GeoffreyBooth
Copy link
Collaborator Author

Take it up with JavaScript. Until let and const, foo in your example could only be 42.

I don’t think code blocks like this are terribly uncommon:

if error?
  message = 'Error!'
else
  message = 'Success!'

alert message

Obviously there are more concise ways to write this in CoffeeScript, but imagine the messages were much longer and you can see why someone would write it out across multiple lines. I think code like this is common across many projects, and we can’t cause blocks like this to break.

@kirly-af
Copy link
Contributor

Humm... yeah, I never did such thing but it's reasonable to think a significant number of users do this (I'm not making any judgement, just I never thought about doing such thing).

Still, I'd be in favor to keep it simple, as it is.

@lydell
Copy link

lydell commented Sep 17, 2016

Just throwing a thing in here:

$ coffee -e 'console.log foo
foo = 5'
undefined

$ node -e 'console.log(foo)
var foo = 5'
undefined

$ node -e 'console.log(foo)
let foo = 5'
[eval]:1
console.log(foo)
            ^

ReferenceError: foo is not defined

$ node -e 'console.log(foo)
const foo = 5'
[eval]:1
console.log(foo)
            ^

ReferenceError: foo is not defined

@GeoffreyBooth
Copy link
Collaborator Author

Almost that exact example is discussed in the MDN article on let, under the heading Temporal dead zone and errors with let.

This goes back to my point about #31. I feel like we can either keep things as they are, or somehow allow let and const. It would make CoffeeScript too inconsistent to allow only const, which would behave so differently than var: not just the unreassignability that people expect to get from const, but also the block-scoping and the temporal dead zone (i.e. the lack of hoisting its declaration to the top of its scope).

@GeoffreyBooth
Copy link
Collaborator Author

I just did a little experiment where I changed the compiler to output let wherever it previously output var. It’s on this branch.

Amazingly, all but two tests still pass. The two that fail involve the REPL:

testRepl "variables in scope are preserved", (input, output) ->
  input.emitLine 'a = 1'
  input.emitLine 'do -> a = 2'
  input.emitLine 'a'
  eq '2', output.lastWrite()

testRepl "existential assignment of previously declared variable", (input, output) ->
  input.emitLine 'a = null'
  input.emitLine 'a ?= 42'
  eq '42', output.lastWrite()

So I guess the REPL would need to be updated to work with let. But everything else seems to work.

These two tests would pass as non-REPL CoffeeScript:

./bin/coffee -be 'a = 1
do -> a = 2
console.log a'
2

As does the other test. The above compiles to (in --bare mode):

let a;

a = 1;

(function() {
  return a = 2;
})();

console.log(a);

Even the example I posted above, where I thought var would be necessary:

if error?
  message = 'Error!'
else
  message = 'Success!'

alert message

becomes:

let message;

if (typeof error !== "undefined" && error !== null) {
  message = 'Error!';
} else {
  message = 'Success!';
}

alert(message);

Since we’re hoisting the let declaration lines all the way to the top of the function scope, the same scope that var had, I guess we can swap out var for let everywhere we’re currently using var? Is there an edge case where we couldn’t?

The bigger question is whether we should do this. I’m not sure let offers any advantages over var when used this way; presumably if let was used deliberately by a human, to define a variable within a block scope, the JS runtime might be a very tiny bit faster by not keeping unnecessary extra variables around within a larger function scope. But if we also automatically output const for variables we know never get reassigned, assuming such a determination could be made, then presumably we could get some additional runtime performance benefit from const. If nothing else, our output would look much more modern.

@triskweline
Copy link

Thanks @GeoffreyBooth for leading this effort.

I wanted to post a counter to the "I rarely used felt the need for let in CS" above.

Over at Unpoly Coffeescript's lack of let has repeatedly caused us to accidentally introduce errors to the codebase.

We're heavy users of the revealing module pattern. This very useful pattern lets you write class-like constructs without the constant housekeeping of keeping this bound to the correct object.

It looks like this:

window.myModule = do ->

  privateState = 'foo'

  privateFunction = -> # ...

  publicFunction1 = -> # ...

  publicFunction2 = -> # ...

  # return public API
  publicFunction1: publicFunction1
  publicFunction2: publicFunction2

# Usage of the module
myModule.publicFunction1()
myModule.publicFunction2()

What has repeatedly happened is that someone accidentally overrode a function by declaring a variable with the same name. This is caused by CS hoisting all assignments to the topmost scope possible:

window.myModule = do ->

  data = -> # ...

  # many lines emitted

  func = ->
    # author wants to store stuff in a temporary variable called "data",
    # but accidently overrides the "data" function above.
    data = getSomeData()

  data: data
  func: func

These kinds of errors are hard to debug, because the data function changes its meaning at runtime (after the first call of func).

I'm hoping some kind of variable scoping can eventually make its way into Coffeescript.

@itsMapleLeaf
Copy link

itsMapleLeaf commented Dec 1, 2016

I personally think this is a bigger issue than its priority makes it seem. Variable scoping is a problem CS has had since ever, due to the simple fact that foo = 'bar' can be seen as both a declaration and a reassignment, and without searching over the entire file, there's no clear way to tell which. The example provided by @henning-koch shows precisely how major of an issue this is. A variable reassignment can introduce really nasty bugs without proper scope declarations.

Since both let and const are already reserved, maybe it'd be correct to go ahead and implement let x = 1 and const y = 2 style statements? Not even in the context of ES2015+, just as a sanity-saver for CoffeeScript as a whole. Without let or const, a declaration could behave like the current var does in 1.x.

let foo = 1 # let declaration
const bar = 2 # const declaration


if true
  baz = 3 # no `let` or `const` of baz exists in scope, assign as a `var`, declared at head of function

  let foo = 6 # local foo assignment
  bar = 42 # compile-time error?

  console.log foo #=> 6

console.log foo #=> 1
console.log baz #=> 3

And maybe for simplicity's sake, leave const out of the picture entirely.

@aleclarson
Copy link

aleclarson commented Dec 2, 2016

I like @just-nobody's idea of...

  • add only let
  • keep var as the default

...for 4 reasons...

  • backwards compatibility
  • let is clearer than a new assignment operator
  • avoid temporal dead zones by default
  • lack of const spam

As an extra measure to avoid temporal dead zones, let should be hoisted to the top of its block.

console.log foo # undefined, because `var` is used
foo = 1

if error?
  console.log foo # undefined, because `let` is hoisted
  let foo = 2

...compiles to...

var foo;
console.log(foo);
foo = 1;

if (error != null) {
  let foo;
  console.log(foo);
  foo = 2;
}

@GeoffreyBooth
Copy link
Collaborator Author

If we add support for let and/or const, it will almost certainly be via an operator, not a keyword like let or const. That has been discussed elsewhere, that allowing a keyword for this goes against CoffeeScript’s core design. We don’t have var; it would be antipattern to have let.

As for allowing only let and not const, we’d need a strong justification for this. const, whatever else you may feel about it, is popular and certainly not condemned as one of JavaScript’s “bad parts” that we should prohibit. I think there’s a case to be made for excluding both, justified by sparing people from the mental effort of tracking different types of variables; but if we add one, we should probably add the other.

@aleclarson
Copy link

aleclarson commented Dec 2, 2016

I think the utility of let outweighs the burden of const, and this should not be a "both or neither" discussion. const merely protects developers from themselves (and may provide perf benefits when JS engines optimize). Since ES6 convention is to use const by default, I would only want const in CS if it can be made default (but that breaks backwards-compatibility). Otherwise, there will be const keywords (or :== operators) everywhere (assuming the same convention is used).

But I contend let is useful enough for CS to support (with or without const). And it can be added without breaking backwards-compatibility. When it comes to syntax, I think let is clearer than :=, but I could live with either.

@itsMapleLeaf
Copy link

itsMapleLeaf commented Dec 2, 2016

That has been discussed elsewhere, that allowing a keyword for this goes against CoffeeScript’s core design. We don’t have var; it would be antipattern to have let.

but if we add one, we should probably add the other.

I don't really agree with this. I like CS a lot because of its expressiveness and readability, how you can write statements like eatLunch() if hungry? and time is lunchTime. From one perspective, let would just be another language feature to add onto this expressiveness, just like let is in ES6, and the reasoning for excluding const is that the issue of variable scoping is (arguably) greater than limiting reassignment, since one is much more likely to cause issues than the other.

I understand the reasoning, but I really think the benefits of having a let keyword outweigh the cons, and for the most part keeps backwards compatibility. I wouldn't argue against a new operator a la := or similar, but that's not nearly as readable to me as let I think. Should get more opinions on this, perhaps.

Also, referencing this to show just how major of an issue this is. An issue created six years ago, spawning an entire fork of the language, no less.

@edemaine
Copy link

edemaine commented Dec 3, 2016

Another context where let seems especially helpful is in for loops that generate callbacks. It'd be cool to be able to say:

for let i in [1..5]
  setTimeout (-> console.log(i)), i*100

and have that compile to something like

for (let i = 1; i <= 5; i++)
  setTimeout(function(){ console.log(i) }, i*100)

whereas currently we need to write

for i in [1..5]
  do (i) ->
    setTimeout (-> console.log(i)), i*100

I'm not sure how to express this with an operator like :=. for i :in [1..5] looks pretty bad...

Anyway, supporting let also seems like a nice opportunity for CS to provide more control over scoping when needed. const support seems helpful mostly for attaining speed. As an optional feature, it seems like it could be handy...

@connec
Copy link

connec commented Dec 4, 2016

for i in [1..5]
  do (i) ->
    setTimeout (-> console.log(i)), i*100

Not particularly relevant to the thread, but for this case, in the meanwhile, you can use then to inline the IIFE:

for i in [1..5] then do (i) ->
  setTimeout (-> console.log(i)), i*100

More relevant to the thread:

I also think @jashkenas' comments in jashkenas/coffeescript#712 still stand regarding lexical scoping being a positive feature of CoffeeScript. Issues with variable shadowing tend to arise frequently when you're doing too much in one lexical context (e.g. writing your whole app/library in a single file, or naively joining files without a safety wrapper).

Orthogonally, I think a wholesale change to let as the default for variables could be done in a more-or-less backwards compatible way (excepting @lydell's observation regarding names no longer being hoisted, but that would be odd behaviour to depend on). The main issues I can think of are:

  1. # Assignments in if / else
    if condition
      val = 'something'
    else
      val = 'something else'
    doSomethingWith val
    
    # This can be rephrased in CS to
    val = if condition
      'something'
    else
      'something else'
    doSomethingWith val
    
    # The same pattern could be used to deal with any assignments in blocks
    val = switch
      when true then 'something'
      else           'something else'
    
    # OR the user could exploit lexical scoping
    val = null
    if condition
      val = 'something'
    else
      val = 'something else'
  2. # Use result of last loop iteration
    break for item, i in items when item.id == id
    console.log "Item #{id}: #{item} at #{i}"
    
    # We could make the compiler output something like
    let item$, i$
    for (let i = 0; i < items.length; i++) {
      let item = items[i]
      item$ = item
      i$ = i
      if (...) {
        break
      }
    }
    let item = item$, i = i$
    console.log(...)
    
    # OR we could leave it up to the user to take advantage of lexical scoping
    item = i = null
    break for item, i in items when item.id == id
    console.log "Item #{id}: #{item} at #{i}"

Another point is that I can imagine this being quite complicated to implement. The current rules of scope aligning exactly with function boundaries seems fairly heavily exploited by the compiler.

Actually, I think it'd be pretty cool to compile to const in all cases, with some shenanigans to handle assignments in loops. I expect this would be a bit too far, though.

console.log item, i for item, i in items
console.log item while item = items.shift()
for (let i$ = 0; i$ < items.length; i$++) {
  const i = i$, item = items[i$]
  console.log(item, i)
}
let item$
while (item$ = items.shift()) {
  const item = item$
  console.log(item)
}

@GeoffreyBooth
Copy link
Collaborator Author

If I can try to keep us on track, I think that if we implement let/const, it would follow this proposal and not #31. If you only had to choose between the two, would anyone disagree? The sooner we can settle on a single proposal, the sooner someone can start trying to implement it.

I don’t think we need any more arguments for why we should support let/const in the abstract. At this point, what would be useful is specific feedback to this proposal to make it better, unless people think it’s ready to be implemented as is. In your feedback, though, please look beyond whether it’s better to use a let keyword or := or some other symbol. That’s just a personal preference decision, and @jashkenas should just pick whatever he feels meshes best with his overall design and that should be that. Please just assume for now that we’re going with :=, and :== if we use two symbols, unless @jashkenas decides otherwise.

So aside from those two things, does anyone have any other feedback on the proposal at the top of this thread?

@itsMapleLeaf
Copy link

Yeah, I read @jashkenas ' comments on the issue I linked, and he does have a good point that scope declarations introduce unneeded complexity for end users, and that scope management should ultimately be the responsibility of the programmer, not the language. Regardless, +1 to this proposal, for bringing more parallels from ES6.

@edemaine
Copy link

edemaine commented Dec 4, 2016

I agree with this proposal over #31 (but I'm new here, so don't know how much my vote counts). I definitely have more use for let over const (though I see the value to both), so wouldn't want just const. (And I definitely don't want to redefine =. Very happy with if condition then x = 1; y = 2 else x = 2; y = 1.)

Regarding automatic const: given that let and const have the same scoping, I could see a := operator being automatically converted to let or const according to whether the assignment happens exactly once in the same scope. But this won't satisfy the people who don't want to accidentally reassign (not realizing their variable was already used elsewhere in the same block). Perhaps we could do the automatic conversion, but still provide a :== operator that makes const explicit when desired. So still, if you don't want to care about this issue, you can use :=.

I see the appeal to an operator approach more and more; my only dissatisfaction is the inability to do something like for let as in my comment above. Maybe for i:= in [1..5]? Eh, yuck. Partly the issue is the (frankly rather strange) magic that ES6 makes each iteration of the for loop the scope for the let; I understand this is useful, but find it counterintuitive notationally, given that the let is in the initialization block. Maybe we should just stick to the simpler for i in [1..5] then j := i; setTimeout (-> console.log(j)), j*100 (or do).

@jcollum
Copy link

jcollum commented Dec 4, 2016

I like the idea of having both let and const. I want to be able to mark something as "this thing shall not be changed" vs. "this thing could change". I see a lot of people saying that const is very common but in ES6 code I've seen let is more common.

To me the :== syntax reads as "Is this thing equal and also a constant?". I'm saying I'd prefer to not have the == in there.

How about:

a = 5
z := "Fry" 
o ::= b: 6, c: 8

to

let a = 5; 
var z = "Fry"; 
const o = {b: 6, c: 8}

After spending time in React-Native land (and slowly ramping up on Redux) I see the appeal of not being able to reassign variables.

There's a preference for symbols over words in here though -- that doesn't seem very coffeescripty. The coffeescript solution seems to be to favor plain words instead of symbols when possible (see and vs && and is vs ===). Keeping that in mind I think this would be better:

var a = 5
z = "Fry" 
const o = b: 6, c: 8

to

var a = 5; 
let z = "Fry"; 
const o = {b: 6, c: 8}

I know people want to keep keywords that determine variable scoping out of the language but the alternative is either not doing it (which would become a big -- and valid -- criticism of coffeescript) or using symbols, which seems antithetical to the language itself.

@GeoffreyBooth
Copy link
Collaborator Author

Again, please just assume that the symbols will be := and :==. If that changes, it will be up to whoever actually submits the PR, and the contributors who help review it. I don’t think we need any more opinions on what people’s favorite symbols are. If you want to argue for some other operator, or for a keyword instead, then please actually implement this and submit a PR 😄

The main open questions in this proposal are:

  1. Whether we need or want both operators, or would rather get by with just one.
  2. Exactly what each operator would do.

Currently the proposal states:

a := 1 would mean, “declare and assign a with the value of 1, using let if a gets reassigned in this block and const if it never gets reassigned.”

a :== 1 would mean, “declare and assign a with the value of 1 using const. If it gets reassigned later, throw an error.”

If we have only one operator

If we have only one operator, it would be :=, and would either behave as defined above or simply always output let. People would have no way to force const, and so there would be no way to force the runtime to protect you against accidental reassignment.

Assuming we could reliably detect variable reassignment via static analysis, I think we would want to output const whenever possible, to take advantage of whatever speed improvements runtimes should presumably achieve by better handling of const. If ultimately we can’t safely determine when it’s safe to output const, there’s still an argument for having only := and having it only output let: that would give people the ability to use block-scoped variables, which is a currently much-desired feature.

The argument for having only one operator is that it fits in nicely with CoffeeScript’s current design pattern regarding var. Currently CoffeeScript declares all variables at the top of scope using a line like var a, b, c;, saving people the trouble of needing to separately keep track of declaration and assignment of variables. In CoffeeScript there is no declaration; the compiler just takes care of that for you automatically. Likewise the compiler could take care of choosing let or const for you automatically. (This would be tricky to implement, as const statements must be declared and assigned in the same statement; it’s not possible to put a line like const a, b, c; at the top of a scope and assign those variables later.)

The other argument in favor of := alone is simplicity. It’s complicated enough to have two ways to declare/assign variables, = and :=; why make it even more complicated with three ways? By adding := we solve the “block scope” problem, where people can finally declare variables that exist only within a limited scope like a for loop or an if block. All they lack compared to full ES2015 is const’s protection against your own mistakes if you accidentally reassign a variable you intended to be unreassignable (I won’t say “intended to be constant,” since that’s not what const does, as we think of it from most other programming languages).

If we have both operators

If we have both operators, should := simply always output let, or should it try to intelligently output const whenever possible and let otherwise? The latter is just a performance improvement for compilers with const optimizations, assuming they ever come; and a convenience for people to not have to think about needing to use :== unless they want to force the protection against their own mistakes.

The argument for having both operators is that it’s full compatibility with ES2015; people would be able to output var, let and const, and therefore take advantage of everything that ES2015 has to offer with regard to variable declaration and assignment. This is no small thing.

But it comes at the cost of users having three operators to understand and use correctly, and the added mental burden of needing to consider “should I ever need to reassign this variable?” when you declare one. Is the ability to protect yourself against accidental reassignment worth adding an additional operator into CoffeeScript? Should we leave that to linters or type checkers?

@jcollum
Copy link

jcollum commented Dec 4, 2016

After reading about it wouldn't const be the preferable "first stop" assignment? Since it would error if reassigned later via = it seems really safe, eliminating the above concerns about coffee using var to assign variables which might (unknowingly) be re-assigned later.

Since const is just "reserve this variable name in this scope" I think that that slack could be taken up by an immutability library (e.g. https://facebook.github.io/immutable-js/) if someone really wants a constant. const is really misleading the more I think about it.

It sounds like the only place let is advantageous is in loops, so I'd think coffee would just do that -- use let when creating loops. const everywhere else and var never?

Full compatibility with ES2015 is addressed with var a = 5; etc.

That all seems fine to me; I withdraw my earlier statement :-)

I'm saying I'm in favor of:

a = 5
b :=6

becoming:

const a = 5; 
let b = 6; 

I'm having a hard time seeing the right place to use var in post-ES2015 world.

@GeoffreyBooth
Copy link
Collaborator Author

@jcollum we can’t redefine = to const. That would be the breaking change to end all breaking changes. While we are making a few breaking changes in CoffeeScript 2, the goal is to make as few as possible. We want old projects to be able to use CoffeeScript 2 with few if any changes; and one of those “old projects” is the CoffeeScript compiler itself.

We can redefine = to output let, which is an experiment I tried in my let branch. It isn’t a breaking change as long as we put the let declarations at the top of the function scope, where the var declarations go now. In that case, yes, there is no more need for var.

Redefining = to output let, though, is somewhat pointless. It just makes our code look more modern. Maybe it’s worth doing for that reason, but I think it should probably be its own proposal.

@jcollum
Copy link

jcollum commented Dec 4, 2016

I see, sorry. In that case the only real use I can see for const is just for alignment with ES2015. I think it's an easy criticism to lob at coffee 2 otherwise: "Coffeescript? They don't even support all the variable assignment types!" Let people have const but make them work for it with :=.

Thanks for your patience here @GeoffreyBooth.

@itsMapleLeaf
Copy link

itsMapleLeaf commented Dec 4, 2016

I actually just came across another solution, which should probably go in its own issue, but I'll mention it here anyway.

MoonScript has a using keyword, which allows you to create a new scope only pulling in the variables from the parent scope that you need, along with being able to pass in nil to create an entirely new scope.

x = 5
dostuff = (using nil) ->
  x = 10

dostuff!
print x --> 5

Maybe we can pull something like that over into coffeescript? I like the idea of this more than a new assignment operator, honestly.

@GeoffreyBooth
Copy link
Collaborator Author

@just-nobody MoonScript compiles into Lua, not JavaScript. What JavaScript would your example compile to? I’m not aware of a way to introduce a new scope in JavaScript that doesn’t inherit the variables from its parent scope. (Though if you can prove me wrong about this, please start a new issue. I’d like to keep this issue focused on the proposal in the first comment. Thanks!)

@aleclarson
Copy link

aleclarson commented Dec 5, 2016

The following scenarios describe how the = and := operators could work most elegantly (IMO):

Click to expand

 

1: A variable that is never reassigned

Assuming the compiler can determine immutability, it's safe to default to const here. It's worth noting that the "reassignment safeguard" benefit of const is not provided in this context.

This scenario occurs within a function scope (or the global scope). Scenarios 3 and 5 deal with behavior within a block scope.

x = 0

# Output:
const x = 0;

2: A variable that is reassigned at least once

In this scenario, we keep let with its first assignment (instead of hoisting let x; like var x;). This is necessary, because const cannot be hoisted like let can.

This scenario occurs within a function scope (or the global scope). Scenarios 3 and 5 deal with behavior within a block scope.

x = 0
x += 1

# Output:
let x = 0;
x += 1;

3: Using = within an if statement

In this scenario, we cannot use const by default (due to block scoping concerns). We also hoist each let in order to avoid using var.

if true
  x = 0
  y = 0
  y += 1

# Output:
let x, y;
if (true) {
  x = 0;
  y = 0;
  y += 1;
}

4: Using := within an if statement

This scenario is almost identical to scenarios 1 and 2, but in the context of an if statement.

As seen below, const is used by default (for variables that are never reassigned).

if true
  x := 0
  y := 0
  y += 1

# Output:
if (true) {
  const x = 0;
  let y = 0;
  y += 1;
}

5: Using = within a loop

This scenario is identical to using = within an if statement, but for completeness' sake.

while true
  x = 0
  y = 0
  y += 1

# Output:
let x, y;
while (true) {
  x = 0;
  y = 0;
  y += 1;
}

6: Using := within a loop

This scenario is identical to using := within an if statement.

while true
  x := 0
  y := 0
  y += 1

# Output:
while (true) {
  const x = 0;
  let y = 0;
  y += 1;
}

 

Anything wrong with this solution?

If this solution is used, it's probably worth having a separate discussion about only :== (which forces the use of const, protecting developers from themselves).

@itsMapleLeaf
Copy link

@GeoffreyBooth #57

@GeoffreyBooth
Copy link
Collaborator Author

Over on this thread I asked @alangpierce how they implemented the “output const if possible” feature of decaffeinate. His response was illuminating. Basically, it is possible to detect if a variable can be assigned with const instead of let, though the case of destructuring is a tricky one; but we actually might not want to bother. If I can quote at length:

IMO there isn’t much value into automatically choosing let or const in generated JS code. I’d keep it simple and always output let for :=. At least from my experience, const is for programmer convenience; it makes it harder to make some types of mistakes, and it arguably makes code easier to read because it makes the intentions of variables more clear. I’m pretty sure babel won’t even transpile your code if you try to assign to a const variable, so the runtime behavior never comes up in practice; it’s all basically a compile-time check. (eslint also has a check for it, which I guess is useful for people who don’t use babel.) If CS2 had :==, I would expect the compile step to fail if there’s a reassignment, not just for const to appear in the JS code.

I haven’t measured it, but I doubt const is any better in terms of performance. I’ve worked a little with JS engines, and I think it’s relatively easy for a JS engine to look ahead and see if a let declaration is actually const in practice. I think modern JS implementations already do preprocessing and optimizations that are much more advanced than that.

FWIW, the decaffeinate projects all have a convention of using let rather than const everywhere (which is basically the “only :=” proposal), although in other code that I work on, I prefer const when possible. So my personal opinion is that either style is reasonable, and I certainly prefer let over var.

So I think perhaps we should split this proposal into two features: a “block assignment operator,” :=, that always outputs let in the current block scope; and a “const block assignment operator,” that is mostly the same thing but with const. We can and should implement the former first; I’m not sure we need the latter at all, since generally it hasn’t been CoffeeScript’s place to provide linting.

@edemaine
Copy link

edemaine commented Dec 5, 2016

@aleclarson That seems mostly right, but your rules 1 and 2 don't really make sense for =, only :=. Old assignments have to be hoisted so that they can be accessed in any scope. (Even outside if and for loops, you have to worry about temporal deadzone.)

@edemaine
Copy link

edemaine commented Dec 5, 2016

@GeoffreyBooth Agreed. I think the main argument for :== would be full ES6 expressability (some people seem to like const), and "you don't have to understand it if you don't use it" (but that doesn't really apply to reading others' code). And you gave the main argument against. I don't think it's a big deal either way, and perhaps excluding it is the simpler option for now (we can always add it later -- we can't really remove it later). Anyway, we can focus on := to let conversion (sans hoisting).

@aleclarson
Copy link

aleclarson commented Dec 5, 2016

@edemaine I should have clarified that scenarios 1 and 2 are only if used within the global scope or a function scope. As long as that condition is met, using let is no different than var.

If defaulting to const isn't worth it, I agree with @GeoffreyBooth that := should always output let, but I'm mostly opposed to :==.

The one nice thing about my solution above: never see a var statement again.
Whereas, if := always outputs let, then = keeps outputting var.
Unless we hoist let declarations made by = as to avoid using var.

func = (x) ->
  if x > 0
    y = 0
    z := 1

# Outputs:
let func;
func = function(x) {
  let y;
  if (x > 0) {
    let z;
    y = 0;
    z = 1;
  }
};

@GeoffreyBooth GeoffreyBooth changed the title Block assignment let/const operator Block assignment let and const operators Dec 5, 2016
@GeoffreyBooth
Copy link
Collaborator Author

I created #58 to propose just := by itself. I think if we go the route of adding let and/or const, we would probably build := first, always outputting as let; and then const would be its own effort, if we decide to support it at all. There could also be a “phase 2“ effort to automatically decide whether to output := as let or const, but that would be an even lower priority since doing so would have no practical benefit; it would only make our output look more like handwritten ES.

Anyone opposed to closing this proposal in favor of #58?

@coffeescriptbot
Copy link
Collaborator

Migrated to jashkenas/coffeescript#4931

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

10 participants