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.