jashkenas / coffeescript

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

CS3 Discussion: Assignment scope #4985

Open aleclarson opened 6 years ago

aleclarson commented 6 years ago

I know assignment scope has been discussed at length for CS2 (#4951), but I don't see a discussion in the context of CS3. So here it is.

 

Block scope =

I'm strongly in favor of changing = from var to let and using nearest block scope instead of function scope, as was proposed by @jashkenas here: https://github.com/coffeescript6/discuss/issues/58#issuecomment-265220724

 

Shadowing avoidance

There was also talk of adding a := operator for explicit avoidance of shadowing.

Another option is the let keyword (which invites inclusion of const).

Just today, I thought of :foo = 1 as another option.

 

Concerns

@jashkenas said in https://github.com/coffeescript6/discuss/issues/58#issuecomment-265193752:

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.

..which rules out the := operator, but maybe not let or :foo =?

If = is changed to block scope, the shadowing problem can be fixed without having two mental models of variable scope.

In the same comment, @jashkenas said:

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.

..but I assume this discussion is still "up in the air" for CS3.

Let me know if I missed any other ideas or concerns.

GeoffreyBooth commented 6 years ago

There was some side discussion of this in https://github.com/jashkenas/coffeescript/issues/4847#issuecomment-356124744, but this issue could perhaps serve as a better canonical place to track it.

Here’s how I remember the consensus so far:

aminland commented 6 years ago

What about preserving the existing behaviour, while also allowing you to explicitly define a variable at the block level without hoisting it to the top (i.e. enable the use of var/let/const keywords and have it output directly as written)?

GeoffreyBooth commented 6 years ago

@aminland See the quote from above:

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.

Any solution that preserves the existing behavior while also allowing some other way to declare a variable means you now have two ways to declare variables. The consensus from the earlier issues was that preserving the simplicity of there being only one way to declare variables is our top priority. So really the choice is only between “do nothing” and “change the current output to be block scope instead of function scope.”

aminland commented 6 years ago

If those are the only options then let's please not break backwards compatibility... Block scoping wouldn't actually enable you to do anything you can't currently accomplish with function scoping...

aleclarson commented 6 years ago

preserving the simplicity of there being only one way to declare variables is our top priority

Backwards compatibility is probably the top priority, since we don't want to fracture an already dwindling community. Also, block scoping shouldn't be written off as too complex. The Javascript community seems to be thriving with the addition of block scoping. There is clear demand for such a feature in Coffeescript. I would argue the greater risk is not matching Javascript on that flexibility.

With that said, I think the best direction is probably adding the let keyword. Familiarity is important in reducing the mental barrier of switching between Javascript and Coffeescript.

sa-0001 commented 6 years ago

i think block scoping is most correct and i would definitely never thoughtlessly define a variable within a for loop (for example), instead defining its initial value outside the loop if that's the intent. but while i know i would be able to migrate my code if all variable declarations became let by default, i also know i am in the minority - for everyone else it just 'breaks things randomly' (from their perspective), or they simply cannot upgrade.

so, unless there is a convenient way to use optional flags for such feature ({"blockScoping":true}, --blockScoping) then i think allowing use of the let keyword may be the only real possibility.

GeoffreyBooth commented 6 years ago

For the record, if block scoping were to be added to CoffeeScript in addition to the function scoping we already have, it would be via a new operator like :=, not the let keyword. Adding any new keyword would be a breaking change, whereas := would not be. Please review the top post and the discussions it links to before commenting.

sa-0001 commented 6 years ago

Adding any new keyword would be a breaking change, whereas := would not be

let is a reserved keyword since no later than v1.2 - see also your own comment: #58 "neither let/const nor := would break backward compatibility"

For the record, if block scoping were to be added to CoffeeScript in addition to the function scoping we already have, it would be via a new operator like :=, not the let keyword. [...] Please review the top post and the discussions it links to before commenting.

For the record, it is clear from reading said comments that this is your opinion on the matter, and far from a universally-accepted truth. While I did not actually mean let specifically but a new keyword/operator in general, and in fact I may prefer :=, let's err on the side of not telling new participants to take a hike.

GeoffreyBooth commented 6 years ago

You are correct, let is reserved. Regardless, in the prior threads there was a consensus that if we were going to support both types of scoping, it would be via :=. Later on, a consensus formed that having two types of scoping is too complicated for CoffeeScript and we would rather keep the current scoping rather than have both. Hence the consideration for a potential future 3.0.0 breaking change where the one and only supported scoping becomes block scoping rather than function scoping.

aleclarson commented 6 years ago

Furthermore, it seems a new consensus has been established in regard to "block scoping by default" being too drastic of a breaking change, which has a strong possibility of fracturing the community.

I think the argument between let or := can be logically put to rest for the following reason. The := operator works better in expressions where the value is being assigned and returned simultaneously.

# Assign `y` with block scoping, then add its value to `x`
x + y := 1

# Looks like a syntax error
x + let y = 1

But I'm warming up to the idea of a package-specific flag to enable block scoping by default. Of course, you could argue this is toxic for open-source projects, since it would not be obvious whether the flag is being used without checking the configuration. But that should be their choice, instead of the language gate-keeping a useful feature.

sa-0001 commented 6 years ago

I wish CS could change to block-scoping without causing further abandonment. I wish someone could make a sophisticated tool to help migrate code for this change. Or, I wish there were an elegant way to opt-in to new features per file/module. Unfortunately, adding a keyword or operator is simply so much easier than all of the above that it remains tempting.

Of course, "leave it like it is" may be good enough for people like me who simply prefer CS, not because of one particular feature or another.

Some (admittedly distasteful) ideas:

GeoffreyBooth commented 6 years ago

I agree, it’s an annoying problem. It’s problematic to increase complexity when one of our prime selling points is simplicity.

We could mimic ES5’s 'use strict' and add in-file configuration, e.g.:

'use block scope'

if yes
  x = 'block scoped!'

This is probably better than an extra compiler flag, for the reasons mentioned above (not knowing whether a file is written to be block or function scoped without looking elsewhere in the project, etc.). But is it really better than :=? It certainly doesn’t seem simple, and I haven’t seen JavaScript follow this pattern since 'use strict'. It also feels like it opens a crack in the door for all sorts of other compiler flags, which further degrades our simplicity.

zeekay commented 6 years ago

I'm a pretty big fan of block scoping as a default and think it would be a lot more intuitive for new users. I'd happily update all of my projects to properly support it assuming block scope became a default. I think mirroring modern JS here would have a lot of advantages long-term and be worth the intermediate pain.

I suspect most of my projects would require little if any effort to support a switch to block scope. An automatic upgrade tool could presumably hoist all variables to the top of function scope, no?

aleclarson commented 6 years ago

I agree with @zeekay. Block scoping by default is the way forward, and the upgrade path looks smooth. The JS community is evidence that the majority prefer block scoping. Anyone who disagrees can keep using CoffeeScript 2.

carlsmith commented 6 years ago

Meh... In CoffeeScript, expressing a function is very quick and easy, so there is no need for the language to infer tighter scopes to keep things local. On the few occasions where block scope would improve anything, we can just wrap that code in its own lexical scope with a few extra characters.

Lexical scope, based around functions (with associated concepts, like closure) is a beautiful idea that integrates concepts in a really elegant way. It all works so well in CoffeeScript that I personally think things are correct as they are.

vlad0337187 commented 6 years ago

I wote for :=. I think, that let, const and other trash should be avoided. Language must be clean.

And I hope, that this error at least will be fixed: https://github.com/jashkenas/coffeescript/issues/4723 Because incapsulation in CoffeeScript sucks.

I use it just for clean syntax, also often use JS to avoid problems.

Inve1951 commented 6 years ago

that's not a bug, that's expected behaviour how else would you make the value available outside the function without returning it

mrmowgli commented 6 years ago

My two cents after a year of straight ES coding: I found that because of linters like eslint I had to go back through and hand tune all the vars to let and const from cs.

If we had linting working well in CS, I could convince people to allow CS checkins. As it stands now though, I am stuck.

I personally think we should never worry about such things in CS and generate let and const appropriately. I see mistakes in ES all the time with the use of let and const, but var is all kinds of taboo at the moment.

GeoffreyBooth commented 6 years ago

I personally think we should never worry about such things in CS and generate let and const appropriately.

I did make an experimental branch that simply always output let wherever we currently output var, and it worked, with all the tests passing; though it was considered a bad idea because using let but always corresponding to function scope was felt to be misleading.

If it’s possible to reliably track variable references and assignments, which I think it might be, then we could output let and const right now, though they would still be relative to function scope (because to switch to block scope would be a breaking change):

If we know a variable isn’t being referenced, I wonder if we need the declaration line (var a, b, c;); but I’m assuming it’s there for a reason.

But the fact that our let and const output would always still be relative to function scopes, even if occasionally we can declare the variables within a block scope that’s inside a function scope, makes the fact that we’re outputting let and const less successful. Without it being constrained to block scopes, it wouldn’t correspond to idiomatic ES2015 output very well, and might still be misleading.

ghost commented 3 years ago

Late chime in but I do miss const. I would love to adopt CoffeeScript if it offered const as a storage modifier. Enums schemas regexes and identifiers are always const for me & have been since const was made available

I also agree with switching var to let in the compiler

dyoder commented 2 years ago

I'd like to point out that there is, in fact, already two ways to declare variables in CoffeeScript, so it's just a question of whether it's worth making it simpler. Specifically, you can introduce new block variables with do:

do ({ foo, bar } = {}) ->
  # ...

Are we the only ones doing this? 😀

Granted, technically, this is just function scope, but the point is that, in cases where you want to be sure of the scope, you can already do that. The choice already exists: the question is whether a block-assignment operator would be simpler and more elegant. Which I would think is self-evidently the case. 😊

Inve1951 commented 2 years ago

I think this is worth exploration. As far as I'm aware, do (a) => output could already be changed to using let without a breaking change, avoiding the IIFE in favor of true block variables.

// current
((a) => {/* ... */})(a);
// could be
{let a = a; /* ... */}

Fat arrow is probably what you want in most cases and as far as I can recall losing this from using a do -> IIFE has done nothing for me except having to come back and change the arrow when the need arose. Can anyone think of a case where you'd want to intentionally lose this?

edemaine commented 2 years ago

@Inve1951 Neat point about changing the output of do (...) =>. And if the block doesn't use this, it could also work for do (...) ->. While there may not be much use for losing this via ->, it seems natural to preserve the semantics in this case.

Back to whether to make something more convenient than do, the main use-case I have for block scoping is for loops. I can't tell you how many times I've written code like this:

for item in loop
  do (item) ->
    callbacks.push -> ... item ...
  #or
  callbacks.push do (item) -> -> ... item ...

I'd much rather write something like

for let item in loop
  callbacks.push -> ... item ...
#or
for const item in loop
  callbacks.push -> ... item ...

I can't think of how to write this with :=, but maybe I'm missing something. Given support for this, I think it would also be natural to allow (but not require) declaring variables as block scope via let x = ... or const x = ... (or just one, probably let, if we want to avoid complexity). This would also make it far easier to add TypeScript support to CoffeeScript; in particular, it would enable using : as the type operator just like in TypeScript, and I think it would make for more natural placement of existing jsdoc typing comments.

As none of this breaks backward compatibility, I think it could be added to CS 2.

Inve1951 commented 2 years ago

@edemaine I share the pain with loops and callbacks. Here's a proposal in the scope of CS3: Change the for item in items output to use let block-scoped variables unless an item identifier is already declared, e.g. via item = null just prior the loop.

Example:

for item in items
  console.log item

New output:

for (let item, i = 0, len = items.length; i < len; i++) {
  item = items[i];
  console.log(item);
}

Example 2:

item = null
for item in items
  console.log item

New output:

var item;

item = null;

for (let i = 0, len = items.length; i < len; i++) {
  item = items[i];
  console.log(item);
}

This also leaves a clear upgrade path for code relying on the current behavior - Simply declare item beforehand.

Aside from this use case (loops) I'm still not convinced we need or even want block-scoped variables (except for aesthetics).


There's one pain with do (a) -> that could maybe get alleviated: It gives you reference errors when a is not declared. This could be fixed with a typeof check in the output akin to a ? undefined for the parameter / RHS of assignment, but isn't free or particularly readable. Though, again, the only times I ran into this was with loops when I simply wanted to re-use an identifier from outer scope and that outer scope code changed, no longer using that identifier. Above proposal would not fix this.