Skip to content

Fix implicit loop closure, support do (x) -> #959

@TrevorBurnham

Description

@TrevorBurnham

This is an attempt to tie together a few recent issues and come to a consensus on the current feature of closing-over variables in a loop.

The purpose of the feature

Originally proposed in issue 423, the feature is nicely illustrated by this test case:

for i in [1, 2]
  setTimeout (-> console.log i), 0

If converted straightforwardly to JavaScript, this will generate the output

2
2

Why? Because the closure doesn't get invoked until after the loop has finished executing, by which point i is 2. The closures always references the same i.

But because of this feature, you get the output

1
2

Now the closure references the loop's indices (and other variables with "loop scope"—see issue 948) as they are when the closure is defined. This is implemented using a function called from each loop step:

var i, _fn, _i, _len, _ref;
_ref = [1, 2];
_fn = function(i) {
  setTimeout((function() {
    return console.log(i);
  }), 0);
};
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
  i = _ref[_i];
  _fn(i);
}

Inconsistencies resulting from this feature

Unfortunately, there are several problems with this implementation, most notably issue 954: The transformation doesn't work if there's a break, continue or return anywhere in the loop. So the code

for i in [1, 2]
  setTimeout (-> console.log i), 0
  return if false

gives you

2
2

This is highly unintuitive: Why would the addition of a control statement affect the code's output?

Another inconsistency, issue 950, is that loop indices are immutable when this transformation occurs (that is, when there's a closure and no control statements in the loop), but are mutable otherwise.

Yet another inconsistency, pointed out by satyr at issue 728, is that arguments won't work within the for loop if the transformation occurs: It will reference the arguments of _fn rather than the arguments of whatever function the for loop exists in.

A more sophisticated implementation could correct these problems, but it would not be simple or elegant, and it's certainly not going to happen in the next few days.

Philosophical problems with this feature

Even if it weren't for the inconsistencies pointed out above, would we want this feature? It's a pretty dramatic transformation on the CoffeeScript compiler's part, and makes an unintuitive leap from JavaScript's semantics.

While it may be the case that you usually want a closure in a for loop to reference surrounding variables as they were at the time the closure was declined, this isn't the way that closures behave anywhere else in the language.

One of my favorite things about JavaScript is that only functions have scope. It's a nice, easy, consistent rule, one that lets you easily determine scope in CoffeeScript by looking for -> and => (and class). There have been several attempts at giving loops special scope in CoffeeScript, but all have proven problematic. I think loops should be kept simple.

Alternatives

Let's look at that original example again. How do we say "let's preserve the value of i when the closure is invoked"? The simplest approach is to write our own explicit closure,

for i in [1, 2]
  ((i) ->
    setTimeout (-> console.log i), 0
  )(i)

Of course, the syntax here is terrible. There was a better syntax that was proposed at issue 788 and had fairly wide support, not to mention a working implementation courtesy of satyr. If a proposed variation on this syntax were added to master, then you could write

for i in [1, 2]
  do (i) ->
    setTimeout (-> console.log i), 0

where do (i) -> ... is shorthand for do (i = i) -> ..., which in turn is shorthand for the current ((i) -> ...)(i).

This, I believe, is the perfect solution: A simple, succinct way of explicitly stating which variables you want to capture. This is a much better fit with CoffeeScript's philosophy of letting you write syntactically elegant code with JavaScript's semantics.

Conclusion

The implicit loop closure that's been in CoffeeScript since issue 423 should be removed due to the unpleasant surprises listed above. Even a perfect implementation would not, in my view, be worth the increased distance between the CoffeeScript and the underlying JavaScript. And the do keyword, rejected as insufficiently practical, should be reconsidered as an elegant way of capturing variables.

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions