Coffeescript's `for` loops

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.

Efficient Iteration

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:

Coffeescript

for element in arr
    # code here

Javascript

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:

Coffeescript

for i in [0..n] by 1
    # code here

Javascript

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.

Reverse Iteration

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:

Coffeescript

for i in arr by -1
    # code here

Javascript

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

Shorthand

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.

Comprehensions and Binding

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}, ... ]

Related Posts

Jez Ng 17 May 2012
blog comments powered by Disqus