Thursday, July 21, 2011

The State of CoffeeScript

Despite its popularity, I've never been a fan of JavaScript. In my opinion, it is a language that suffers from poor leadership and design. The biggest strength of JavaScript, however, is that it works on every browser. There is no alternatives to JavaScript if you want to program for the web. That is, until now.

CoffeeScript, created Jeremy Ashkenas, is a programming language that compiles into JavaScript. It boasts many neat features: More concise syntax, functional constructs, sensible use of the equality operator, well defined check for 'existence', language support for inheritance, and others. But more importantly is that it represents a new way of thinking about writing programs for the web: writing your code using a more powerful language, then transform your code into the ubiquitous JavaScript.

There is a similar trend in the Java world. Development of languages like Scala, Jython, JRuby, and Clojure taught us that you don't have to use Java's ugly syntax to run on Java's widespread platform. Java itself can be a low level library used to create high level programming languages, yielding better productivity, smaller code base, and significant savings in development costs for your company.

The problem with CoffeeScript though, is that it is not quite there yet. It has a dependency on node.js, which makes it difficult to get started on for developers on Windows. Since Windows is such a big platform, the lack of availability on Windows is a significant constraint on the growth and use of CoffeeScript. (I was incredibly wrong on this: You can use CoffeeScript on Windows).


CoffeeScript, inspired by Ruby, uses whitespace to defines blocks of code (e.g., the body of a function, definition of a class). However, it doesn't quite handle whitespaces intelligently. In my opinion, it is too lenient on the use of white spaces, which leads to subtle bugs at runtime.

As example, can you spot the difference between the following two pieces of code?



and


They compile into very different JavaScript:

Example 1:
(function() {
  var EntityGroup;
  EntityGroup = (function() {
    EntityGroup.prototype.nodes = [];
    function EntityGroup() {
      return;
    }
    return EntityGroup;
  })();
  ({
    update: function(elapsed, target) {
      var node, _i, _len, _ref;
      _ref = this.nodes;
      for (_i = 0, _len = _ref.length; _i < _len; _i++) {
        node = _ref[_i];
        node.update(elapsed, this);
      }
    }
  });
}).call(this);
Example 2:
(function() {
  var EntityGroup;
  EntityGroup = (function() {
    EntityGroup.prototype.nodes = [];
    function EntityGroup() {
      return;
    }
    EntityGroup.prototype.update = function(elapsed, target) {
      var node, _i, _len, _ref;
      _ref = this.nodes;
      for (_i = 0, _len = _ref.length; _i < _len; _i++) {
        node = _ref[_i];
        node.update(elapsed, this);
      }
    };
    return EntityGroup;
  })();
}).call(this);

The difference is that in the first example, the update function was indented using tabs (perhaps because it was copied and pasted from another file).



The same type of behavior can be seen if a function with a different level of indentation exists between the constructor() and update() functions. However, such problem does not occur if the function with a different level of indentation comes after update(). Languages like Python rejects ambiguous indentation by issuing an error instead of making subtle and unexpected assumptions at runtime. CoffeeScript should adopt the same philosophy, especially since it has a static compilation phase .

CoffeeScript adds language level support for classes and inheritance (or rather, "better language level support"). However, it still retains some flaws from its JavaScript heritage. For example, you get access to a new language construct, "super()" but you don't get access to the parent class directly. If you want call a method with a different name from the parent class, you'll need to resort to an undocumented 'ClassName.__super__' reference (and hope it works in future versions).

For example:

# Base implementation of a View. It always renders a header, body, and 
# a footer. The 3 sections are divided with a horizontal separator.
# Subclasses must implment its own specific method of rendering the
# header, body, and footer.
class View
    renderHeader: () ->
        console.log(" ====  Hi  ==== ")

    renderBody: () ->
        console.log(" |    View    | ")

    renderFooter: () ->
        console.log(" ============== ")

    renderSeparator: () ->
        console.log(" -------------- ")

    render: () ->
        @renderHeader()
        @renderSeparator()
        @renderBody()
        @renderSeparator()
        @renderFooter()

# A class that renders view content in the footer area twice. The body 
# area is blank. However, it can be configured at runtime to use 'normal'
# rendering logic.
class RepeatView extends View
    legacyMode: false
    constructor: (legacyMode) ->
        @legacyMode = legacyMode

    renderBody: () ->
        # renderBody now handled in footer, unless in 'legacyMode'
        if @legacyMode
            super()

    renderFooter: () ->
        # if in legacy mode, try to use parent's render method to do
        # rendering twice.
        if @legacyMode
            super()
        else
            # So we still have to know about the class name when we 
            # need to access the parent class. Sigh. 
            RepeatView.__super__.renderBody()
            RepeatView.__super__.renderBody()
            super()

view = new View
view.render()

console.log("\n\nRepeated View (legacy mode)")
repeatView = new RepeatView(true)
repeatView.render()

console.log("\n\nRepeated View (non-legacy mode)")
repeatView = new RepeatView(false)
repeatView.render()

The output looks like this:

====  Hi  ====
--------------
|    View    |
--------------
==============


Repeated View (legacy mode)
====  Hi  ====
--------------
|    View    |
--------------
==============


Repeated View (non-legacy mode)
====  Hi  ====
--------------
--------------
|    View    |
|    View    |
============== 

This problem with access to the parent instance is no major flaw. It just shows that the language itself still needs polishing.

CoffeeScript also does not have built in support for minifying code, or static optimization such as dead code detection and elimination. A more mature compilation language would support such features, perhaps by incorporating open source libraries such as the Closure Compiler. CoffeeScript is not there yet, and perhaps should not be the focus of such a young language, but it should get there eventually.

CoffeeScript is not quite ready for production environment yet. But I think it represents a brilliant look at the future way of web programming: That is, you no longer have to code in JavaScript directly, let an optimizing compiler take care of that for you.

2 comments:

  1. Misterm8:21 PM

    A nice balanced look at some of the rough edges in CoffeeScript.

    To me this embodies the spirit of CoffeeScript (amongst other projects) and the future of the JS community at large: let the developers push the language in the direction they want it to go...and if it's not there yet...build on top of it.

    Coffeescript is very much a work in progress, and that definitely needs to be considered. I agree with your assessment that it's not quite ready for "prime time", but I think it's pretty close and the community makes progress every day. I'm surprised how much the tooling has improved since I personally started playing with it.

    The nice thing is Coffeescript is continuing to improve, and I have found Jeremy/the community to be very welcoming and open to new ideas. Not every idea will be met with agreement but I don't think I've seen very many language feature requests where Jeremy (or someone else) hasn't said "Well, what would that look like? Let's think about it".

    One small nitpick with the article, something I disagree with or feel at least needs to be qualified:

    CoffeeScript does not depend on Node.

    The project is definitely based in Node, with the main "coffee" tool written for Node, and the build processing using Cake...

    ...however the CoffeeScript compiler itself is just JavaScript and will run anywhere JavaScript is available. (including the browser)

    Indeed, there are a variety of wrappers around it using JS run-times for languages like Java, Ruby, Windows Script Host, etc.

    If you can run any of these, you are good to go for experimenting/using CoffeeScript.

    ReplyDelete
  2. Thanks M. I didn't know about the ability to run the CoffeeScript compiler without node.js. That's fantastic!

    I really should've done my research before putting that down.

    ReplyDelete