Closure Mode for CoffeeScript
How We Got Here
The Closure Tools were created to make it easier to maintain large JavaScript codebases, leveraging organizational constructs such as type-checking and namespaces. However, in creating the Closure Library, it was decided that it must always be possible to run Library code as ordinary JavaScript, i.e., without any sort of processing step. Therefore, it was not permissible for the Library to be written in any sort of superset/alternate form of JavaScript that was not recognized natively by browsers. The only viable option was to include any metadata needed by the Closure Tools inside JavaScript comments. This made it possible to build a rich set of tools, though it resulted in verbose source code due to all of the annotations.
Previously, most other attempts to address the JavaScript tooling issue were to
use an existing programming language (that already had mature tools) and then
create a tool that translated that existing language into JavaScript. This was
the approach taken by the Google
Web Toolkit (GWT) and ocamljs,
among others.
The common problem with that approach is that there is inevitably an "impedence
mistmatch" between JavaScript and the language being translated into JavaScript.
For example, Java supports int
s and float
s whereas
JavaScript only has number
. Further, JavaScript has first-order
functions that are not available in Java, so it is not possible to use the full
language when limiting oneself to GWT.
Recently, more developers have come to embrace JavaScript and there is more interest in the advancement of the language. As such, creating new languages (such as CoffeeScript) that are designed from the outset to be translated into JavaScript eliminates the "impedence mismatch" problem from before while making it possible to experiment with new language features. Google's Traceur project is specifically designed to make it possible to experiment with future features of JavaScript in the browser today.
Therefore, rather than awkwardly adding metadata in the form of comments in
source code, a more elegant solution is to use a programming language with a
grammar that makes it possible to express that metadata directly. For example,
in Closure Library code, a constructor function is annotated with
@constructor
in a JSDoc comment, whereas in CoffeeScript, there is
explicit support for classes via a class
construct. This stronger
grammar makes it easier to generate the JavaScript with annotations for the
Closure Compiler, which is precisely the goal of Closure Mode for CoffeeScript.
Features of Closure Mode
This section enumerates the changes in JavaScript code generation when Closure Mode is used to generate JavaScript from CoffeeScript.Creating a Class
- A
goog.provide()
statement is automatically inserted for the class. -
Normally, when writing Closure Library code by hand, the name of a
class appears at least twice: once in a
goog.provide()
statement and again in the constructor function declaration. This results in repeated information that may get out of sync. Because a class is an official construct in CoffeeScript, the name of the class appears in only one place, so there is no possibility of inconsistency in the generated JavaScript. - JSDoc with a
@constructor
annotation is automatically inserted before the constructor function. - This ensures that if type-checking is enabled in the Closure Compiler, it will be able to recognize a constructor/class properly.
- JSDoc with an
@extends
annotation is automatically inserted before the constructor function. - This ensures that if type-checking is enabled in the Closure Compiler, it will be able to recognize a subclass relationship properly.
- The
goog.inherits()
call required for subclassing is automatically inserted after the constructor function. -
Normally,
CoffeeScript needs to insert two functions into the generated JavaScript in
order to establish the subclass relationship:
__extends()
and__hasProp()
. After inserting these functions and declaring the constructor function,__extends()
is called wheregoog.inherits()
is normally called in Closure Library code. Therefore, instead of declaring any new functions, Closure Mode simply callsgoog.inherits()
. -
super
calls in CoffeeScript are rewritten in a Library-appropriate way. -
The
__extends
function in CoffeeScript introduces a property named__super__
whereas thegoog.inherits
function in the Closure Library introduces a functionally equivalent property namedsuperClass_
. Both of these properties are used in calling superclass constructors and methods, so they need to be updated in Closure Mode, accordingly. - An
include
construct is introduced into the CoffeeScript grammar that producesgoog.require()
statements. -
In Closure Mode, the following line of CoffeeScript:
include goog.dom
becomes the following JavaScript:goog.require('goog.dom');
It is more analogous to theimport
statement in Python. (include
was chosen instead ofimport
becauseimport
is a reserved word in JavaScript, so it already has some special handling in CoffeeScript.) - An
include...as
construct is introduced into the CoffeeScript grammar that producesgoog.require()
statements. -
In Closure Mode, an
include
statement has an optionalas
clause that makes it possible to refer to the imported namespace via a shorthand. This is implemented such that it leverages goog.scope(), which is processed in a special way by the Closure Compiler in ScopedAliases.java. For example, the following line of CoffeeScript:include goog.array as arr
Becomes the following Closure Library code:goog.require('goog.array'); goog.scope(function() { var arr = goog.array; ; }); // close goog.scope()
Therefore, throughout the rest of the file, wherevergoog.array
would be used,arr
can be used as an alias instead. goog.provide()
andgoog.require()
statements are automatically sorted alphabetically in the generated JavaScript code.- This is not necessarily a big win, but it seemed like a nice touch!
Creating a Subclass
Expressing Dependencies
Future Work
Although Closure Mode makes it possible to write Closure-Compiler-friendly JavaScript, it does not yet support all of the annotations in the Closure Library. This section lists the high-priority features that need to be added.Type Information
For many, of the most compelling features of the Closure Compiler is its type-checker. Type-checking is only effective when the JavaScript being compiled is annotated with type information. There already appears to be some interesting work being done in this area, so perhaps the efforts of UberScript and Closure Mode can be combined.Enums
In Closure, an enum is simply an object literal whose values are treated as though they were enum values. Because these values are frequently inlined by the Compiler and the enum definition is removed during compilation, it is illegal to iterate over an enums values using thefor..in
operator
in JavaScript when code is compiled in Advanced mode. One alternative is to
add a method to the enum that returns an array of the values:
/** @enum {string} */ TrafficLight = { RED: '#ff0000', YELLOW: '#ffff00', GREEN: '#00ff00' }; TrafficLight.values = function() { return [TrafficLight.RED, TrafficLight.GREEN, TrafficLight.BLUE]; };Unfortunately, this method is tedious to maintain by hand because it must be kept in sync with the actual enum values. If
enum
were a recognized
construct like class
in CoffeeScript, then both the declaration and
the values()
method could be generated so they were guaranteed to
be in sync.
Utilities
Currently, Closure Mode focuses on converting classes effectively, but some Closure Library files are just collections of related utility functions (goog.array
, goog.string
, etc.). It should also be
possible to create these types of libraries efficiently in CoffeeScript.
Leveraging More Closure Library Built-ins
By default, array iterators in CoffeeScript are translated into verbosefor
loops (though they are admittedly more efficient in
terms of function calls):
# CoffeeScript alert note for note in ['do', 're', 'mi'] roots = (Math.sqrt num for num in [1, 4, 9])
// Generated JavaScript var note, num, roots, _i, _len, _ref; _ref = ['do', 're', 'mi']; for (_i = 0, _len = _ref.length; _i < _len; _i++) { note = _ref[_i]; alert(note); } roots = (function() { var _j, _len2, _ref2, _results; _ref2 = [1, 4, 9]; _results = []; for (_j = 0, _len2 = _ref2.length; _j < _len2; _j++) { num = _ref2[_j]; _results.push(Math.sqrt(num)); } return _results; })();In Closure Mode, the generated JavaScript would be more familiar to a Closure developer if it leveraged the existing
goog.array.forEach()
and
goog.array.map()
functions in the Closure Library:
goog.array.forEach(['do', 're', 'mi'], function(note) { alert(note); }); var roots = goog.array.map([1, 4, 9], function(num) { return Math.sqrt(num); });