CoffeeScript: Looking for the Catch

Saturday, November 06, 2010 6:27 PM

I'm just starting out with CoffeeScript, but it's currently looking incredible.  Normally I loathe languages that rewrite other languages, but CoffeeScript appears to be well thought out and targets the JavaScript syntax whilst leaving the semantics pretty much unchanged.  Runtime errors are found in the generated files, not the originals, which is a bit rubbish but I'm guessing fixable (as long as V8 supports it).

Let me share with you some code I wrote the other day.  (Feedback on the idioms in the code are welcomed: I'm just starting with node.js.)

var util = require('util'),
    EventEmitter = require('events').EventEmitter;

function SocketPlayer(client) {
  EventEmitter.call(this);
}
util.inherits(SocketPlayer, EventEmitter);

exports.Game = function (questionFactory, questionCount, player, setTimeout) {
  var self = this;
  this.setTimeout = setTimeout || process.setTimeout;
  this.questionFactory = questionFactory;
  this.delay = 1000;
  this.questionCount = questionCount;
  this.currentQuestionCount = 0;

  function advanceQuestion(wasRight) {
    self.currentQuestionCount++;
    self.currentQuestion = questionFactory();
    var dto = self.currentQuestion.dto();
    dto.wasRight = wasRight;
    player.send(dto);
  };
  player.on('answer', function(data) {
    console.log(data.answer);
    var wasRight = self.currentQuestion.isCorrect(data.answer);
    if (wasRight) {
      advanceQuestion(true);
    } else {
      var explanation = self.currentQuestion.explanationDto();
      explanation.delay = self.delay;
      explanation.wasRight = wasRight;
      player.send(explanation);
      self.setTimeout(advanceQuestion, self.delay);
      self.delay *= 2;
    }
  });

  advanceQuestion(false);
}

So, this represents an abstract game.  The player gets the next question if they get one right, an explanation of the right answer if they got it wrong.  (If it helps, think of hangman.)  I think this code is alright.  However, take a look at the version in CoffeeScript

util = require('util')
EventEmitter = require('events').EventEmitter

class SocketPlayer extends EventEmitter

class exports.Game
  constructor : (@questionFactory, @questionCount, @player, @setTimeout) ->
    @setTimeout ?= process.setTimeout
    @delay = 1000
    @currentQuestionCount = 0
    @advanceQuestion = (wasRight) ->
      @currentQuestionCount++
      @currentQuestion = @questionFactory() # Returns the next question
      dto = @currentQuestion.dto()
      dto.wasRight = wasRight # but the answer to the previous question
      @player.send(dto)
    @player.on 'answer', (data) =>
      console.log data.answer
      if wasRight = @currentQuestion.isCorrect data.answer
        @advanceQuestion true
      else
        explanation = @currentQuestion.explanationDto()
        explanation.delay = @delay
        explanation.wasRight = wasRight
        @player.send explanation
        @setTimeout (-> @advanceQuestion(false)), @delay
        @delay *= 2
    @advanceQuestion false

How About Testing?

Here's the tests I wrote in JavaScript:

var vows = require('vows'),
    eyes = require('eyes'),
    assert = require('assert')
    game = require('../src/game.js'),
    util = require('util'),
    EventEmitter = require('events').EventEmitter;
function guess(n) {
  this.isCorrect = function(a) { return a == n; }
  this.explanationDto = function() {
    return { correctAnswer : n };
  }

  this.dto = function() { return { question : "Guess what number I'm thinking of" }}
}

function StubPlayer() {
  EventEmitter.call(this)
  var self = this
  this.send = function(data) { self.data = data }
}
util.inherits(StubPlayer, EventEmitter);

function GuessingGame() {
  this.player = new StubPlayer();
  game.Game(
    function() { return new guess(1); },
    10,
    this.player,
    function(action, delay) {  });
  return this;
}

vows.describe('Guessing Game').addBatch({
  'Given a guessing game' : {
    topic : GuessingGame,
    'when you guess correctly' : {
      topic : function(topic) {
        var t = new GuessingGame();
        t.player.emit('answer', { 'answer' : 1 });
        return t;
      },
      'then it says you were right' : function(topic) {
        eyes.inspect(topic.player.data);
        assert.isTrue(topic.player.data.wasRight)
       }
    },
    'when you guess wrong' : {
      topic : function(topic) {
        var t = new GuessingGame();
        t.player.emit('answer', { 'answer' : 2 });
        return t;
      },
      'then it says you were wrong' : function(topic) {
        assert.isFalse(topic.player.data.wasRight);
      }
    }
  }
}).export(module);

Again, let's see what it was in CoffeeScript.

vows = require('vows')
eyes = require('eyes')
assert = require('assert')
game = require('../src/game2.js')
EventEmitter = require('events').EventEmitter

class Guess
  constructor : (n) ->
    @isCorrect = (a) -> a == n
    @explanationDto = -> { correctAnswer : n }
    @dto = -> { question :  "Guess what number I'm thinking of" }

class StubPlayer extends EventEmitter
  send : (data) -> @data = data

class GuessingGame extends game.Game
  constructor : ->
    super((-> new Guess 1),
      10,
      new StubPlayer,
      -> 0)

vows.describe('Guessing Game CoffeeScript example').addBatch({
  'Given a guessing game' : {
    topic : GuessingGame
    'when you guess correctly' : {
      topic : (topic) ->
        t = new GuessingGame
        t.player.emit 'answer', { 'answer' : 1 }
        return t
      'then it says you were right' : (topic) ->
        # eyes.inspect topic.player.data
        assert.isTrue topic.player.data.wasRight
       }
    'when you guess wrong' : {
      topic : (topic) ->
        t = new GuessingGame()
        t.player.emit 'answer', { 'answer' : 2 }
        return t
      'then it says you were wrong' : (topic) ->
        # eyes.inspect topic
        assert.isFalse topic.player.data.wasRight
    }
  }
}).export module

The actual vows stuff is slightly shorter, but it's the setting up of the stub classes that really makes CoffeeScript shine.  StubPlayer is a very good example of this.  All it needs to be is an EventEmitter that captures what it is sent.  The sheer ceremony involved in declaring that in JavaScript was pretty painful.

The Smooth and the Rough

Although this code is pretty short, it's a pretty good tour of some tricksy things about CoffeeScript.  First, it's worth understand that CoffeeScript really is JavaScript.  If you don't understand JavaScript, you won't get anywhere with CoffeeScript.  Let's see some examples of this:

  • the difference between -> and => requires you to understand how "this" behaves in JavaScript. 
  • If you take a look at the function "advanceQuestion", you'll see that it's instantiated in the constructor, not added to the prototype.  This makes no sense until you realize the function is called from the constructor, and in JavaScript, the whole idea of calling a prototype method from the constructor makes no sense at all.

Some other cool things about constructors:

  • extends deals with all of that util.inherits nonsense
  • super is vastly more pleasant than copying class names all over the place.  But you still need to explicitly call super
  • parameters that begin with an @ sign are automatically made instance fields (more languages should have this feature)
  • Just like in JavaScript, the classes are just variables.  Declare a variable as "exports.Game" and you'll export "Game" from the module.  Everything else is private.

The ugly:

  • You don't have to type in brackets, but you need to careful where you leave them out.  For instance, if "addBatch" doesn't have an explicit bracket, the implicit close bracket ends up after module, rather than before export.  (This might be fixable.)
  • Debugging pretty much requires you to read the generated JavaScript.  Luckily, it's pretty good JavaScript.
Comments
No comments posted yet.
Something to add?

Talking sense? Talking rubbish? Something I'm missing? Let me know!

Fields denoted with a "*" are required.

 (will not be displayed)

 
Please add 6 and 8 and type the answer here:

Preview Your Comment