Why I hate implicit return in CoffeeScript

Posted on Fri 26 October 2012 in Coding

CoffeeScript is “a little language that compiles into JavaScript.” If you haven’t seen or tried it, go check it out! I’ve been using it in a project for a while now, and its obvious advantage over pure JavaScript is that it is very concise and has high expressive power. For example, comprehensions takes away a whole class of tedious programming problems. Instead of writing this:

var names = [];
for (var i = 0; i < persons.length; i++) {
    names.push(persons[i].name);
}

You write this:

names = (p.name for p in persons)

Neat! Another great feature is the lambda-like anonymous function syntax. Instead of:

array.sort(function(a, b) {
    return a.index - b.index;
});

You write:

array.sort((a, b) ->  return a.index - b.index)

But it can get even more concise! Parentheses are optional and so is the return keyword. Thus, a shorter version is:

array.sort (a, b) -> a.index - b.index

The reason this works is that everything in CoffeeScript is an expression. But while it allows you to write more expressive code, it can also have very surprising side effects. And in general, I don’t like when a programming language surprises me! No, not in general. Never!

I will show two examples where implicit return can be very surprising:

1) Returning the value of a for loop

Consider the following contrived and pointless, but ultimately pretty innocent JavaScript code:

var dostuff = function(i) { };

var x = function(n) {
    for (var i = 0; i < n; i++) {
        dostuff(i);
    }
};

var start = new Date().getTime();
x(1000000000);
var elapsed = new Date().getTime() - start;
console.log("Time in ms = " + elapsed);

This piece of code does absolutely nothing, but it does nothing one billion times! Node.js version 0.6.7 runs it in just under 7 seconds on one of my VMs. What would the corresponding CoffeeScript code look like? A naïve translation would probably result in something like this:

dostuff = (i) ->

x = (n) ->
    for i in [0...n]
        dostuff i

start = new Date().getTime()
x 1000000000
elapsed = new Date().getTime() - start
console.log "Time in ms = " + elapsed

However, when this code is run using CoffeeScript 1.3.3 on the same VM as before, the process eats more and more memory until it hits a 1 Gb memory limit and dies:

FATAL ERROR: JS Allocation failed - process out of memory

So what happened? Well, the for loop is not really a for loop, but rather a comprehension over the numbers from 0 to n (exclusive, since we use ... rather than ..). It’s equivalent to writing dostuff i for i in [0...n], which reveals its expression nature a bit better. There’s one more thing into play: in the absence of a return statement, a function returns its final value. And in this case, the final value is the result of the comprehension. The x function in the compiled JavaScript code looks like this:

x = function(n) {
    var i, _i, _results;
    _results = [];
    for (i = _i = 0; 0 <= n ? _i < n : _i > n; i = 0 <= n ? ++_i : --_i) {
        _results.push(dostuff(i));
    }
    return _results;
};

From this, the out of memory error is pretty obvious - pushing one billion item onto an array is bound to fail!

The solution? Just add an empty return statement to the end of the x function. The CoffeeScript compiler detects that the result of the comprehension isn’t used and thus won’t generate the array-pushing code.

I tend to forget to put empty return statements whenever I don’t want a function to return anything, which means that there is a lot of unnecessary array juggling going on under the hood. Most of the time this isn’t a problem, of course, since the created arrays will be small. But not only does it feel wrong, it doesn’t help performance! And while the solution is simple, littering otherwise concise code with empty return statements is a bit annoying!

2) Returning unexpected values to the caller

I ran into a very peculiar problem when writing unit tests for an AngularJS controller written in CoffeeScript (AngularJS is a JavaScript MVC framework). I used the $controller service to create a controller instance, and I expected to get the instance back as stated by the (rather miserable) documentation. But I got back something else entirely! What was going on?

After a bit of detective work, it turned out that what I got back was the result of an initialization function called by the controller function as its last statement. And what happens with the return value of the last statement in a function? That’s right, it becomes the return value of the function!

What the $controller documentation doesn’t say is that if the controller function returns a proper object, that object is returned instead of the controller instance! Had I written the controller in JavaScript, the absence of an explicit return statement would cause the function return value to be undefined, which in turn would cause $controller to return the controller instance.

Lessons learned

Unless you’re in complete control of the call chain, make sure that you understand what a CoffeeScript function actually returns and what the implications of that are. Also, beware of using comprehensions as the last statement of a function!

CoffeeScript gives you great power, but with great power comes great responsibility!