While building Doppio, I spent some time looking at exactly what Javascript was being generated by the Coffeescript compiler. This post documents a couple of things I learnt about its handling of for loops, which might be obvious to anyone who’s been using Coffeescript for a while, but should be useful to Javascript developers looking to pick up Coffeescript quickly.
Here’s the typical Array iteration loop in Javascript:
for (var i = 0; i < arr.length; i++)
// code here...
One might think that the literal equivalent in Coffeescript would be
for i in [0...arr.length]
# code here
But this is what it translates to:
for (i = 0, _ref = arr.length; 0 <= _ref ? i < _ref : i > _ref; 0 <= _ref ? i++ : i--)
// code here
That seems like an awful amount of code for array iteration. The problem here is that Coffeescript’s ranges are bidirectional: they can either count up or down, depending on whether the initial index is greater or smaller than the endpoint. However, the compiler does not know whether arr is truly an array, and so cannot determine at compile time if arr.length is positive or negative. Thus, on each loop iteration, Coffeescript decides how to compare and change the loop index based by first comparing the start and endpoints.
All this is certainly undesirable if you want performant code. Fortunately, the idiomatic Coffeescript expression compiles optimally:
for element in arr
# code here
for (_j = 0, _len2 = arr.length; _j < _len2; _j++)
// code here
(If you need access to the array index as well, you can obtain it using the syntax for element, index in arr.)
On the other hand, if you really want to count from 0 to some n > 0 efficiently, use the by modifier:
for i in [0..n] by 1
# code here
for (i = _i = 0; _i <= n; i = _i += 1)
// code here
Of course, if the loop endpoints are explicit compile-time constants, the compiler will generate a single-direction loop for you. That is, for i in [0..10] will compile to an increment-only loop. But if either of the endpoints are variables, the compiler will not do any static analysis to try and figure out if a single-direction loop is possible.
All’s well and good thus far, but what happens if we want to iterate in reverse? One might think by would do the trick: if for element in array by 2 steps forwards through the array in increments of two, shouldn’t -1 step backwards? While it technically does, the comparison operator is not changed accordingly, giving us an infinite loop:
for i in arr by -1
# code here
for (_k = 0, _len3 = arr.length, _step2 = -1; _k < _len3; _k += _step2)
// code here
The correct – albeit less elegant – way to do it is
for i in [arr.length-1..0] by -1
# code here
Sometimes we want to do the same thing an n number of times, where we don’t need to know the value of the current index. Coffeescript has a (currently undocumented) shorthand for this purpose:
twoDArray = ([0...10] for [0...10])
Which is equivalent to
twoDArray = ([0...10] for i in [0...10])
Unfortunately, this shorthand syntax does not work with the by keyword modifier – trying to use it will generate a syntax error.
The last example was a comprehension, another great feature of Coffeescript. They provide an elegant way to write your to write your loops as a (hopefully side-effect-free) one-liner, with the results aggregated into an array. However, beware of binding pitfalls – the loop keywords have lower precedence than the assignment operators (= and :). So we have the following:
x = i for i in [0...10] # x = 9 at end of loop
x = (i for i in [0...10]) # x = [0, 1, 2, ...]
y = x: i for i in [0...10] # y = { x: 9 } at end of loop
y = (x: i for i in [0...10]) # y = [{x: 0}, {x: 1}, ... ]