The Artillery Blog

Game Development and Engineering

Working with CoffeeScript: Common Gotchas

We've been using CoffeeScript at Artillery for well over two years and we're pretty satisfied with it. The language adds useful features to JavaScript like concise function declarations, the existential operator (?), expressive loop syntax, and traditional-style classes, which have made development faster and easier. We're usually coding with the underlying JavaScript in mind, and swapping between CoffeeScript in the editor and JavaScript in the browser hasn't been a problem at all.

Unfortunately, sometimes the CoffeeScript compiler does things we don't expect. Here are some of the surprises that we've encountered over the years.

Scoping

Can you see what's wrong with this code?

url = require 'url'

exports.doStuff = (obj) ->
  result = url.parse obj.url
  return result.host

exports.doMoreStuff = (obj) ->
  results = []
  for asset in obj.assets
    url = asset.url
    results.push url
  return results

CoffeeScript doesn't shadow symbols from outer definitions. Calling doStuff() after doMoreStuff() results in the error "Cannot read property 'parse' of undefined" because the top-level symbol url gets redefined as part of the loop.

Our workaround is to always append lib to top-level variables with common names (url, path, asset, image). During code review, the reviewer would probably suggest that the url module be imported with the name urllib to avoid the mistake.

Looping

CoffeeScript has powerful constructs for loops: You can use for item in items to iterate over an array, for item, i in items to iterate with the index, for key, value of obj to iterate over the properties of an object, and for key of obj to iterate over just the property names. So what's wrong with this this code?

for id, player in obj
  player.send 'hello'

Just iterating over a map of player ID to player objects and sending the players a greeting, right? But the above compiles to:

var id, player, _i, _len;

for (player = _i = 0, _len = obj.length; _i < _len; player = ++_i) {
  id = obj[player];
  player.send('hello');
}

By confusing the in and of operators, the program loops over the map as if it were an array. The map probably doesn't have a length property and this code probably results in the contents of the for loop not being executed, which is very difficult to debug.

What's even worse is the opposite: iterating over an array like an object. For example:

for name of names
  list.append "Player: #{ name }"

This compiles to the following:

var name;

for (name in names) {
  list.append("Player: " + name);
}

This actually works, but iterating over an array using the in operator is a very bad idea. There are edge cases where the loop may skip elements of the array or worse. Left uncaught, these mistakes hang around unnoticed until something weird happens months later.

Implicit Returns

That functions in CoffeeScript return implicitly is a long-debated sticking point. We're leaning toward the side that thinks it's a bad idea.

Annoyingly, if you don't want your function to return anything in JavaScript, you have to add a return in CoffeeScript. For example, when the following example is compiled, a return is inserted before bar.quux(). If we don't want doStuff() to return anything — say, if we don't want to accidentally return a private value that someone might accidentally use later — we have to add an extra return statement to the end of the function.

exports.doStuff = (obj) ->
  bar = new Bar(obj.baz)
  bar.quux()

That's annoying, but it's not the bad part. Consider this very-contrived class and method, and assume that the renderer is part of a complicated drawing pipeline and that draw() gets called sixty times per second:

class Renderer

  draw: ->
    for entity in @entities
      if @camera.frustum.contains entity.model.aabb
        @device.drawModel entity.model

CoffeeScript compiles the draw() method into:

  Renderer.prototype.draw = function() {
    var entity, _i, _len, _ref, _results;
    _ref = this.entities;
    _results = [];
    for (_i = 0, _len = _ref.length; _i < _len; _i++) {
      entity = _ref[_i];
      if (this.camera.frustum.contains(entity.model.aabb)) {
        _results.push(this.device.drawModel(entity.model));
      } else {
        _results.push(void 0);
      }
    }
    return _results;
  };

Because of implicit returns, the for loop becomes the return value, and this creates an array unnecessarily. Adding a return fixes this, but doing so is an extra step that we often forget.

Modules that are One Large Object

Our game engine uses a component-entity system where reusable behaviors are added to stateless entities in the form of "component scripts." Components respond to events, such as when they're added to the scene or when a frame tick occurs. Here's a simplified version of our Defender component, which is attached to any entity that takes damage from an attack:

# Components/Defender.coffee

OnInit: ->
  @hp = 50
  @isDead = false
  console.log 'initialized:', this

OnDamageTaken: (value) ->
  @hp -= value
  if @hp <= 0
    @isDead = true

This compiles to a single object, which becomes the prototype for all instances of the component which are attached to entities:

({
  OnInit: function() {
    this.hp = 50;
    this.isDead = false;
    return console.log('initialized:', this);
  },
  OnDamageTaken: function(value) {
    this.hp -= value;
    if (this.hp <= 0) {
      return this.isDead = true;
    }
  }
});

Say you're a sloppy typist like me and you accidentally dedent the console.log() statement. This compiles to:

({
  OnInit: function() {
    this.hp = 50;
    return this.isDead = false;
  }
});

console.log('initialized:', this);

({
  OnDamageTaken: function(damage) {
    this.hp += damage;
    if (this.hp <= 0) {
      return this.isDead = true;
    }
  }
});

Assuming that the engine is eval-ing the code and using the result, OnInit never gets called! If you're like me and you rarely need look at the compiled JavaScript, this problem can take a while to find.

There's Still Hope

We've mitigated a lot of these mistakes through peer code reviews and automated checks. We created a CoffeeScript style guide for ourselves which helps lessen ambiguity during code review reviews, and we've added CoffeeLint to our Git pre-commit hook, which enforces some of our style rules. In researching implicit returns I found a third-party rule which checks for implicit returns. We even have a few simple checks that make sure we don't check in debugger statements. All of the problems mentioned are certainly annoyances, but they're not the end of the world.

blog comments powered by Disqus