Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CS3 Discussion: Assignment scope #4985

Open
aleclarson opened this issue Feb 19, 2018 · 26 comments
Open

CS3 Discussion: Assignment scope #4985

aleclarson opened this issue Feb 19, 2018 · 26 comments
Labels

Comments

@aleclarson
Copy link

aleclarson commented Feb 19, 2018

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: coffeescript6/discuss#58 (comment)

 

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 coffeescript6/discuss#58 (comment):

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
Copy link
Collaborator

There was some side discussion of this in #4847 (comment), but this issue could perhaps serve as a better canonical place to track it.

Here’s how I remember the consensus so far:

  • Nothing related to scope was changed for CS2, because preserving backward compatibility was a top priority.
  • If scope is changed in a theoretical CS3, no syntax would change. The only difference would be that instead of a var foo, bar, baz; line at the top of a functional scope, the compiler would output a let foo, bar, baz; line at the top of the block scope.
  • Changing how scope works is a potentially massive breaking change that would prevent many, if not most, apps from upgrading. This has the potential to cause a split akin to Python 2 and 3, where both versions need to be maintained. The benefits from switching to block scope are probably less than the cost of a possible schism and the confusion caused by countless CoffeeScript examples across the Internet that assume function-based scope.

@aminland
Copy link

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
Copy link
Collaborator

@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
Copy link

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
Copy link
Author

aleclarson commented Feb 27, 2018

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
Copy link

sa-0001 commented Mar 8, 2018

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
Copy link
Collaborator

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
Copy link

sa-0001 commented Mar 8, 2018

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
Copy link
Collaborator

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
Copy link
Author

aleclarson commented Mar 8, 2018

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
Copy link

sa-0001 commented Mar 8, 2018

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:

  • What do they call the compiler flags at the top of some python files (ex. specifying utf-8 encoding)?
  • Could a use statement at the top of the file specify something other than 'strict' (ie. compiler options)?
  • Specifying the CS version per file/module, however ugly it may be, allows never having to worry about breaking changes (but also having to context-switch).
  • Some techniques are more useful if it's your code, some more useful if it's third-party.

@GeoffreyBooth
Copy link
Collaborator

GeoffreyBooth commented Mar 8, 2018

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
Copy link

zeekay commented Mar 15, 2018

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
Copy link
Author

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
Copy link
Contributor

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
Copy link

vlad0337187 commented Jul 19, 2018

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: #4723
Because incapsulation in CoffeeScript sucks.

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

@Inve1951
Copy link
Contributor

Inve1951 commented Jul 19, 2018

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

@mrmowgli
Copy link
Contributor

mrmowgli commented Jul 19, 2018 via email

@GeoffreyBooth
Copy link
Collaborator

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 see that a variable is assigned exactly once within a function scope, and only referenced within a block scope, the line where it’s assigned could become a const declaration.
  • Otherwise we could add our traditional let a, b, c; declaration line at the highest relevant block scope, which at worst would be equivalent to that function scope.

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
Copy link

ghost commented Jul 19, 2021

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
Copy link

dyoder commented Oct 27, 2022

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
Copy link
Contributor

Inve1951 commented Oct 27, 2022

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
Copy link
Contributor

edemaine commented Oct 27, 2022

@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
Copy link
Contributor

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

@cosmicexplorer
Copy link
Contributor

I have introduced a non-breaking change which makes CoffeeScript aware of block scope: #5475. I believe this is a necessary prerequisite for this feature request. Please take a look if interested!

@cosmicexplorer
Copy link
Contributor

I have also introduced something related but different: a distinct declaration, which is currently just using var but can likely be extended to block scope: #5477. It also allows for type annotations with JSDoc, and I have a script that runs tsc against the output to demonstrate it's actually a full type system as well. Not the same thing as this issue, but I suspect interesting to anyone following this thread.

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

No branches or pull requests