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

CS2 Discussion: Features: Block assignment operator #4951

Closed
coffeescriptbot opened this issue Feb 19, 2018 · 64 comments
Closed

CS2 Discussion: Features: Block assignment operator #4951

coffeescriptbot opened this issue Feb 19, 2018 · 64 comments

Comments

@coffeescriptbot
Copy link
Collaborator

coffeescriptbot commented Feb 19, 2018

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

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

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

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

So this CoffeeScript:

a = 1
b := 2

if (yes)
  b := 3

would compile into this JavaScript:

var a;
let b;

a = 1;
b = 2;

if (true) {
  let b;

  b = 3;
}

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

@coffeescriptbot
Copy link
Collaborator Author

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

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

coffeescriptbot commented Feb 19, 2018

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

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

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

coffeescriptbot commented Feb 19, 2018

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

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

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

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

@coffeescriptbot
Copy link
Collaborator Author

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

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

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

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

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

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

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

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

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

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

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

coffeescriptbot commented Feb 19, 2018

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

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

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

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

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

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

My thoughts:

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

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

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

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

@coffeescriptbot
Copy link
Collaborator Author

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

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

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

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

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

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

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

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 [email protected] 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
coffeescript6/discuss#58 (comment),
or mute the thread
https://github.com/notifications/unsubscribe-auth/AAq_Lie2CLhSB-ooCGmwo6_12-t3TzPBks5rFhGOgaJpZM4LD7Gz
.

@coffeescriptbot
Copy link
Collaborator Author

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

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

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

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

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

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

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

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

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

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.

@coffeescriptbot
Copy link
Collaborator Author

From @edemaine on 2016-12-12 13:24

@aleclarson If we automatically declare every variable at the nearest scope where it gets used (the intermediate proposal), then we won't break code that uses the for loop variable afterwards, but we will still make it easy to do closures within for loops (just don't use the same variable outside).

@coffeescriptbot
Copy link
Collaborator Author

From @connec on 2016-12-12 14:23

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

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

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

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

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

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

Having an explicit syntactic modifier would eliminate the guesswork:

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

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

@coffeescriptbot
Copy link
Collaborator Author

From @DomVinyard on 2016-12-12 16:15

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

As for now, no action?

@coffeescriptbot
Copy link
Collaborator Author

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

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

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

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

@coffeescriptbot
Copy link
Collaborator Author

From @edemaine on 2016-12-12 18:33

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

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

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

@coffeescriptbot
Copy link
Collaborator Author

From @GeoffreyBooth on 2016-12-12 18:39

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

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

@coffeescriptbot
Copy link
Collaborator Author

coffeescriptbot commented Feb 19, 2018

From @edemaine on 2016-12-12 19:45

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

@coffeescriptbot
Copy link
Collaborator Author

From @DomVinyard on 2016-12-13 10:01

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

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

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

@coffeescriptbot
Copy link
Collaborator Author

From @triskweline on 2016-12-13 11:17

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

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

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

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

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

@coffeescriptbot
Copy link
Collaborator Author

From @mitar on 2016-12-13 11:24

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

@coffeescriptbot
Copy link
Collaborator Author

From @JavascriptIsMagic on 2016-12-13 12:05

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


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

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


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


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

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


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

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

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

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

@coffeescriptbot
Copy link
Collaborator Author

From @mrmowgli on 2016-12-13 13:04

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

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

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

@coffeescriptbot
Copy link
Collaborator Author

From @connec on 2016-12-13 16:18

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

I agree with this statement.

coffeescript has at least 4+ ways to declare variables

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

@coffeescriptbot
Copy link
Collaborator Author

From @GeoffreyBooth on 2017-11-25 08:26

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

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

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

No branches or pull requests

1 participant