jashkenas / coffeescript

Unfancy JavaScript
https://coffeescript.org/
MIT License
16.52k stars 1.98k forks source link

CS2 Discussion: Features: Block assignment operator #4951

Closed coffeescriptbot closed 6 years ago

coffeescriptbot commented 6 years ago

From @GeoffreyBooth on 2016-12-05 07:21

Splitting off from coffeescript6/discuss#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 =:

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 coffeescript6/discuss#35. Thanks.

coffeescriptbot commented 6 years ago

From @DomVinyard on 2016-12-05 13:42

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.

coffeescriptbot commented 6 years ago

From @edemaine on 2016-12-05 13:47

@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).

coffeescriptbot commented 6 years ago

From @edemaine on 2016-12-05 13:52

@DomVinyard There are lots of reasons to support let in some form, discussed on coffeescript6/discuss#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).

coffeescriptbot commented 6 years ago

From @rattrayalex on 2016-12-05 16:30

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.

coffeescriptbot commented 6 years ago

From @GeoffreyBooth on 2016-12-05 17:03

@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).

coffeescriptbot commented 6 years ago

From @GeoffreyBooth on 2016-12-05 17:05

@DomVinyard and @rattrayalex, the discussion in coffeescript6/discuss#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.

coffeescriptbot commented 6 years ago

From @jashkenas on 2016-12-05 18:58

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.

coffeescriptbot commented 6 years ago

From @vendethiel on 2016-12-05 18:59

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

coffeescriptbot commented 6 years ago

From @connec on 2016-12-05 19:33

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

coffeescriptbot commented 6 years ago

From @edemaine on 2016-12-05 19:53

@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.

coffeescriptbot commented 6 years ago

From @YamiOdymel on 2016-12-05 20:05

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.

coffeescriptbot commented 6 years ago

From @edemaine on 2016-12-05 21:05

@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 :=.

coffeescriptbot commented 6 years ago

From @GeoffreyBooth on 2016-12-05 21:26

@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.

coffeescriptbot commented 6 years ago

From @rattrayalex on 2016-12-06 04:14

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.

coffeescriptbot commented 6 years ago

From @rattrayalex on 2016-12-06 04:30

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.

coffeescriptbot commented 6 years ago

From @GeoffreyBooth on 2016-12-06 05:41

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.

coffeescriptbot commented 6 years ago

From @rattrayalex on 2016-12-06 07:29

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

?

coffeescriptbot commented 6 years ago

From @triskweline on 2016-12-06 08:39

@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.

coffeescriptbot commented 6 years ago

From @connec on 2016-12-06 09:31

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`.
coffeescriptbot commented 6 years ago

From @rattrayalex on 2016-12-06 09:56

Hmm. That is somewhat concerning, though the only "surprising" cases seem like antipatterns, and would be likely to give a curious programmer a moment of pause at the very least...

@jashkenas thoughts?

coffeescriptbot commented 6 years ago

From @connec on 2016-12-06 12:45

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.

coffeescriptbot commented 6 years ago

From @GeoffreyBooth on 2016-12-06 15:13

@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.

coffeescriptbot commented 6 years ago

From @edemaine on 2016-12-06 15:35

@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).

coffeescriptbot commented 6 years ago

From @jashkenas on 2016-12-06 16:17

I'm in fairly complete agreement with @rattrayalex in this thread. Especially this comment: https://github.com/coffeescript6/discuss/issues/58#issuecomment-265056144

My thoughts:

coffeescriptbot commented 6 years ago

From @DomVinyard on 2016-12-06 16:37

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

coffeescriptbot commented 6 years ago

From @GeoffreyBooth on 2016-12-06 16:39

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.

coffeescriptbot commented 6 years ago

From @jashkenas on 2016-12-06 16:47

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.

coffeescriptbot commented 6 years ago

From @GeoffreyBooth on 2016-12-06 17:28

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.

coffeescriptbot commented 6 years ago

From @jashkenas on 2016-12-06 17:50

@GeoffreyBooth — What you're suggesting is pretty similar to how I'd imagine the switch to full let working:

Right now, CoffeeScript declares a variable in the closest possible function scope to its assignment, with var.

If we switch, CoffeeScript should declare a variable in the closest possible block scope, with let.

If a variable is assigned (not just "used" — we have never, and should not, be doing this based on use, only assignment) in a parent block scope it can simply be let in that higher parent scope. It doesn't need to switch over to var.

My hypothetical version of your above would be:

message = null

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

alert message # "Error at 123456789!"

if error
  console.log time # undefined
let message;
message = null;

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

alert(message);

if (error) {
  console.log(time);
}
coffeescriptbot commented 6 years ago

From @GeoffreyBooth on 2016-12-06 18:04

@jashkenas Yes, that’s how a switch to “only block scoping” would work. My point was that I think there’s a middle ground, where we can kind of “automatically” block scope in the cases where people most want block scoping (temporary variables used only within their block scope) without getting rid of automatically function-scoped variables.

I think getting rid of automatically function-scoped variables is too drastic of a breaking change. Many of the CoffeeScript examples and tutorials across the Web would no longer work.

coffeescriptbot commented 6 years ago

From @edemaine on 2016-12-06 19:08

Oh, this is a neat idea, @GeoffreyBooth . An example in your new proposal:

unless quiet
  if good
    message = 'Error'
  else
    message = 'OK'
  alert message

compiles to

if(!quiet) {
  let message;
  if(good) {
    message = "Error";
  } else {
    message = "OK";
  }
  alert(message);
}

But we get the benefits for looping:

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

compiles to

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

This is a difference from CS1: it correctly outputs 1 through 5 wheras CS1 would output 6 repeatedly.

But we need to be a bit careful, as

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

becomes

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

which outputs 6 ten times. In other words, we still need to be careful that CS hates shadowing, so identically named variables tend to become identical, so you have to be careful when using variables of the same name. (So this doesn't address the "accidental reassignment" issue that let was helping us avoid.) But I do feel like this is a net improvement from CS1, we "support let" at some real level (not just replacing var with function-level let). Given a restriction to only one form of assignment in CS, this is probably my preferred one.

coffeescriptbot commented 6 years ago

From @jashkenas on 2016-12-06 19:13

My point was that I think there’s a middle ground, where we can kind of “automatically” block scope in the cases where people most want block scoping (temporary variables used only within their block scope) without getting rid of automatically function-scoped variables.

There isn't a middle ground. In your example, alert(message) would be undefined. We can't track mentions as determining scope — only assignments. Otherwise you clobber any potential message deriving from surrounding scopes or the global environment.

in the cases where people most want block scoping (temporary variables used only within their block scope)

If the temporary variables are truly only used (assigned and referenced) within a block scope, then it makes no difference. let and var are identical for that case.

I think getting rid of automatically function-scoped variables is too drastic of a breaking change.

I probably agree with you. The smartest course of action is probably to leave things working as they currently are — even though switching to let would be a slight overall improvement.

coffeescriptbot commented 6 years ago

From @edemaine on 2016-12-06 19:18

@jashkenas I think the "middle ground" proposal was the following: if x is assigned within a function (and not in any parent function), then put the let x declaration in the smallest scope that contains all references to x (reads and writes) within the function. This is not quite identical to var, specifically when variables get introduced in loops, as in the setTimeout example above. I think they are identical in all nonloop cases, though -- the difference arises from the magic that each loop iteration gets its own block in ES6.

coffeescriptbot commented 6 years ago

From @edemaine on 2016-12-06 19:24

Ah, here is an example where the variable is not introduced in the loop, but still the placement of let makes a difference:

for i in [1..5]
  j = i*2
  setTimeout (-> console.log j), i*100
for i in [1..5]
  k = i**2
  setTimeout (-> console.log k), i*100

would, under the "middle ground" proposal, compile to

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

In my opinion, this is much more intuitive behavior than CS1, which would output the same value in each loop iteration. The difference can happen whenever loops and closures are combined.

coffeescriptbot commented 6 years ago

From @GeoffreyBooth on 2016-12-06 19:25

@edemaine I think the block starts after the for. So for i in arr would put a let i; inside the for loop’s block.

coffeescriptbot commented 6 years ago

From @edemaine on 2016-12-06 19:30

@GeoffreyBooth Yep. It's particularly clear in CoffeeScript, as for loops already copy the loop variable within the loop.

for i in array
  setTimeout (-> console.log(i)), i*100

could become

let j, len;
for(j = 0, len = array.length; j < len; j++) {
  let i;
  i = array[j];
  setTimeout(function() { console.log(i) }, i*100);
}

whereas currently CS generates

var/let i, j, len;
for(j = 0, len = array.length; j < len; j++) {
  i = array[j];
  setTimeout(function() { console.log(i) }, i*100);
}

The only difference is the placement of let i, but the difference is critical.

coffeescriptbot commented 6 years ago

From @GeoffreyBooth on 2016-12-06 19:32

We can't track mentions as determining scope — only assignments. Otherwise you clobber any potential message deriving from surrounding scopes or the global environment.

@jashkenas I was proposing we track mentions of assigned variables to determine scope. Mentions wouldn’t trigger a declaration unless the variable is also assigned in our code. My example doesn’t declare error, even though it’s mentioned.

Getting back to the original := proposal, it seems there’s a consensus that we won’t be adding it? So I’ll close this issue.

I’ll open a new issue for my ”automatically block scope whenever possible” proposal. Maybe @jashkenas is correct and there really isn’t any way to make it workable. I haven’t thought it through enough to know for sure. But in my mind, the options are either that, or leaving things as is, because I don’t think scope is such a huge problem right now to justify the major breaking change that would entail from replacing function scope with block scope exclusively. So hopefully we can find a way to make the “automatically block scope whenever possible” work 😄

coffeescriptbot commented 6 years ago

From @jashkenas on 2016-12-06 19:39

So hopefully we can find a way to make the “automatically block scope whenever possible” work 😄

You won't. But good luck trying! ;)

But in my mind, the options are either that, or leaving things as is, because I don’t think scope is such a huge problem right now to justify the major breaking change

I think that I agree that leaving things as they are is the best option. @rattrayalex — do you agree as well?

coffeescriptbot commented 6 years ago

From @aleclarson on 2016-12-07 01:53

@jashkenas Without an explicit let keyword or := operator, it's not possible to override existing variables with a block-scoped variable of the same name. Is it considered bad practice to need such a thing?

x = 0
if y > 0
  let x = 1
  console.log x  # => 1
console.log x    # => 0

I agree with @GeoffreyBooth that changing = to block-scoping is a little extreme. I prefer using let or := to limit variable scope, rather than using foo = null to hoist variable scope. Having both function and block scoping isn't as crippling as you make it seem, since you only use block scoping when it's obvious to do so.

coffeescriptbot commented 6 years ago

From @rattrayalex on 2016-12-07 02:02

We haven't thought much as a group about a tool-assisted upgrade path. But I don't really expect such a thought experiment to be encouraging.

So I agree that we should keep scope as-is, function-level only.

In which case, compiling to anything other than var, ever, is a bit misleading.

I also agree this thread can be closed. Glad we thought this through!

On Wed, Dec 7, 2016, 07:23 Alec Larson notifications@github.com wrote:

@jashkenas https://github.com/jashkenas Without an explicit let keyword or := operator, it's not possible to override existing variables with a block-scoped variable of the same name. Is it considered bad practice to need such a thing?

x = 0if y > 0 let x = 1 console.log x # => 1console.log x # => 0

I agree with @GeoffreyBooth https://github.com/GeoffreyBooth that changing = to block-scoping is a little extreme. I prefer using let or := to limit variable scope, rather than using foo = null to hoist variable scope. Having both function and block scoping isn't as crippling as you make it seem, since you only use block scoping when it's obvious to do so.

— You are receiving this because you were mentioned.

Reply to this email directly, view it on GitHub https://github.com/coffeescript6/discuss/issues/58#issuecomment-265332131, or mute the thread https://github.com/notifications/unsubscribe-auth/AAq_Lie2CLhSB-ooCGmwo6_12-t3TzPBks5rFhGOgaJpZM4LD7Gz .

coffeescriptbot commented 6 years ago

From @GeoffreyBooth on 2016-12-07 04:32

I went back and reread the threads that preceded this one. I was looking for examples of problems that would be solved by giving people some way to declare block-scoped variables. The only one I found was from this comment:

window.myModule = do ->

  data = -> # ...

  # many lines of code

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

  data: data
  func: func

This is the “shadowing variables” problem, a.k.a. accidentally clobbering variables declared in an outer/ancestor scope. This has been a primary complaint against CoffeeScript since the beginning. Here’s a great essay arguing why it is bad.

Automatically block-scoping variables that aren’t used in their ancestor scopes wouldn’t solve this problem. In fact, I can’t think of any problems it would solve, other than creating more idiomatic ES2015 output (which is a worthy goal in and of itself, but only if there are no breaking changes) and maybe providing some very incremental performance improvements. I can still write up a proposal for it if people want, if anyone can think of a genuine problem it would solve for them.

I think CoffeeScript’s place in the marketplace is threatened enough already that we can’t afford a split like Python 2/3, where many people never upgrade because the breaking changes are too drastic. Linters and tutorials and Stack Overflow examples and all the other components of the ecosystem aren’t maintained enough anymore with enough vigor to all be updated to reflect major breaking changes to core features. If we add block scoping, it needs to be in a backward-compatible way.

So I think our only options are these:

  1. Do nothing. The shadowed/accidentally-clobbered variables problem remains.
  2. Add := as per the original proposal at the top of this thread, and finally give people block-scoped variables without breaking backward compatibility. (It could even go in 1.x.) I very much acknowledge that this introduces complexity into a language that prides itself on simplicity, and that alone might be reason why := isn’t worth adding.
coffeescriptbot commented 6 years ago

From @edemaine on 2016-12-07 14:26

I do think your intermediate proposal of automatic let scoping helps substantially with closures in loops: often you won't need do anymore, and when you do, a simple assignment would suffice. I'm not sure whether this improvement is worth breaking some old code that relies on the current shadowy behavior though. (Maybe worth running some tests.)

And it certainly doesn't address the issue of programmers taking control of scopes. I agree that this lack of functionality is the main complaint against CS I've seen. (In fact, I was trying to convince a friend to switch from Python to CS, as I've done with other friends, but he couldn't get past this issue.) So I remain in favor of :=.

coffeescriptbot commented 6 years ago

From @JavascriptIsMagic on 2016-12-07 18:43

I personally have wished I could use const in some form in coffeescript so I can program in a pure functional style. The best I can do is pretend to, but I have had a few accidental scope collisions that where hard to track down in larger cs files, which has lead to me making lots of files and breaking up the code significantly more, which is a good thing I suppose.

However you implement it let and const keywords I like the best because it's closer to the way you would write a spoken language, and closer to javascript syntax, but I am also fine with something like :=.

Perhaps := for let and ::= for const, (though that might conflict with :: and .prototype)

I am seeing const more and more in modern javascript code even though let is available, and the reason is mostly so you can guarantee you won't have unintentional scope collision or you get an error, which is nice to catch really early in development/unit testing. Also using import Something from 'something' will make Something a const already in coffeescript.

Those that enjoy pure functional programming might be attracted to CS's syntax, but be deterred by the lack of const which javascript has.

Whatever the case I agree that changing the meaning of = would be a bad thing at this point.

coffeescriptbot commented 6 years ago

From @JavascriptIsMagic on 2016-12-07 19:09

I would also like to point out that let and const are reserved words already in coffeescript, so using them here should not break any backwards compatibility, right?

coffeescriptbot commented 6 years ago

From @GeoffreyBooth on 2016-12-09 07:50

@JavascriptIsMagic neither let/const nor := would break backward compatibility. Changing how = behaves would. Please see the note about bikeshedding in the original proposal at the top of this thread.

coffeescriptbot commented 6 years ago

From @JavascriptIsMagic on 2016-12-09 16:53

@JavascriptIsMagic neither let/const nor := would break backward compatibility. Changing how = behaves would. Please see the note about bikeshedding in the original proposal at the top of this thread.

Sorry about that, my thought process was that if both Javascript's const and let where to be implemented in Coffeescript that we would need 2 different operators or keywords. I am assuming we do not want to change = for backwards comparability reasons, and I was thinking of a way to better represent two additional ways of assignment, so var, let, and const ES features where represented in some way, whatever operators or keywords are ultimately picked, or what they look like.

edit: Strictly speaking let and const are not necessary as is, you can make a do -> to block your scope and be careful about scope bleed. However let and const are both expected features of Javascript that are implemented in browsers today.

So to match := the only other operator I can think of is :== for const (perhaps the resemblance to === is fine.) a ::= b already means var a.prototype = b

I am mostly advocating for const here in some form. Sorry if this is off-topic and belongs in a const as it's own feature thread.

coffeescriptbot commented 6 years ago

From @mitar on 2016-12-11 19:45

I also thinks that := is a bad idea. I would be OK of changing variable semantics to blocks by default, requiring somebody to declare a = null before if they want to declare it outside. I think that for thinks like const I would go with support for TypeScript or Flow annotations and we can declare types through that and maybe then if a type is saying that something is constant, output JavaScript is const instead of let or var. But that should be an optional type annotation on the variable. But there should be only one way to declare a variable.

coffeescriptbot commented 6 years ago

From @GeoffreyBooth on 2016-12-12 00:47

I don’t think we should be considering switching all variable assignment to block scoping. Just like automatic inferred block-scoped variables don’t solve the accidental-clobbering problem, neither would all-block-scoped variable assignment. Consider the example from above:

window.myModule = do ->
  data = -> # ...
  func = ->
    data = getSomeData()

Even if we did away with function scope completely and always declared variables via let in block scope, you still have the second data clobbering the first here, as the second one is still inside the first one’s block. If we’re not going to solve this problem, there’s no point in changing things.

coffeescriptbot commented 6 years ago

From @connec on 2016-12-12 11:44

If we’re not going to solve this problem, there’s no point in changing things.

I always felt the the main advantage of let was for improving the scope of loop variables:

for (let i = 0; i < 10; i++) {
  setTimeout(() => console.log(i))
}

The equivalent in CS would currently require a (relatively expensive, I expect) IIFE and some variable duplication:

for i in [0...10] then do (i) ->
  setTimeout -> console.log i

# Gets much worse if you have several parameters
for { id, name, params } in results then do (id, name, params) ->
  doSomethingAsync id, name, params

Perhaps we could have a small syntactical addition for this case exclusively, e.g for let

for let { id, name, params } in results
  doSomethingAsync id, name, params
coffeescriptbot commented 6 years ago

From @aleclarson on 2016-12-12 12:27

@connec Maybe loops should always use let? Then we could reuse variable names across loops sharing a function scope. Although, this would break any code accessing loop variables after the loop finishes.