Wednesday, January 30, 2013

Still not convinced about coffeescript?

Here is another example why you should start writing Coffeescript. The business story in this example is rather simple - I need to load all document from a collection (named Accounts), perform an asynchronous operation (notifyOwner) on each of them, and save all of them back to DB after all notifyOwner operations finish successfully. The following methods are available to achieve this simple task:
Account.find() #find all accounts
account.notifyOwner() 
account.save()
All three methods are asynchronous and return a jQuery promise which resolve with either the results or itself for chaining. Because there are multiple accounts involved, we will also need to take advantage of jQuery.when.

Here is how we implement the business logic in Coffeescript

Account.find()
  .then (accounts)->
     $.when (account.notifyOwner() for account in accounts)...
  .then (accounts...)->
     $.when (account.save() for account in accounts)...
  .done ->
     console.log 'successfully notified all owners'
It takes advantage of coffeescript's splats feature to overcome jQuery.when's inability to take in an array of deferred.
  #resolves only when all three deferred resolve) 
  jQuery.when(deferred1, deferred2. deferred3) 
  
  #immediately resolves with the array passed in) 
  jQuery.when([deferred1, deferred2. deferred3]) 
What Javascript does that Coffeescript translate to?
Account.find().then(function(accounts) {
  var account;
  return $.when.apply($, (function() {
    var _i, _len, _results;
    _results = [];
    for (_i = 0, _len = accounts.length; _i < _len; _i++) {
      account = accounts[_i];
      _results.push(account.notifyOwner());
    }
    return _results;
  })());
}).then(function() {
  var account, accounts;
  accounts = 1 <= arguments.length ? [].slice.call(arguments, 0) : [];
  return $.when.apply($, (function() {
    var _i, _len, _results;
    _results = [];
    for (_i = 0, _len = accounts.length; _i < _len; _i++) {
      account = accounts[_i];
      _results.push(account.save());
    }
    return _results;
  })());
}).done(function() {
  return console.log('successfully notified all owners');
});
If you don't mind a bit help from underscore, We probably can simplify the iteration logic:
Account.find().then(function(accounts) {
  var account;
  return $.when.apply($, (function() {
    return _(accounts).map(function(account){
      return account.notifyOwner();
    });
  })());
}).then(function() {
  var accounts = 1 <= arguments.length ? [].slice.call(arguments, 0) : [];
  return $.when.apply($, (function() {
    return _(accounts).map(function(account){
      return account.save();
    });
  })());
}).done(function() {
  return console.log('successfully notified all owners');
});
So, that's clearly a lot more key strokes than Coffeescript but that's not the point. The point is how easy to read this Javascript version vs the Coffeescript version. Code could be written only once but it often times has to be read multiple times afterwards. Which version will you rather read?

Now, are you convinced?

2 comments:

  1. Great writeup!

    I am using coffeescript but didn't know the trick you showed here. Now I'm applying it to my code but still has some boilerplate code I hate to write. It is because jQuery.getJSON returns an array. I have below code

    someHandler = (responses...) -> console.log(responses)

    urls = ['a.json', 'b.json']

    $.when(($.map(urls, (url) -> $.getJSON(url))...).then(someHandler)

    I expect 'responses' passed to someHandler is always an array of responses even when there is only one urls. However, because getJSON returns an array like [data, 'success']. The 'responses...' is no good. I'm using a workaround

    someHandler = (responses...) -> responses = if responses[1] instanceof Array then responses else [responses]

    Have you had this problem? Any suggestions?

    Thank you.
    Guoliang Cao

    ReplyDelete
  2. Guoliang,

    Try use underscore map instead of $.map which might flatten the resulting array unnecessarily for you. Underscore map always return an array.
    Let me know it worked for you.

    Kai

    ReplyDelete