This is part 2 of a series exploring how to build a full-pledged UML editor in JavaScript. 

 

Switching to CoffeeScript

 

In the 1st post I explained my assumption that there is no more need for server back-end for user-facing applications, which should just be client-side applications accessing cloud API's for resources & services (whether our own or 3rd-party). I'm targeting the Web as initial platform for the example application, so I chose to write the application in JavaScript. The person who did the most to revive & evangalize JavaScript - Douglas Crockford - is also the one who pointed out the many weaknesses it has, due mainly to the very short time in which it was developed. These weaknesses cause you to use overly-complex & error-prone syntax which is a real bummer for someone switching from Python.

 

Things don't have to be this way though, & luckily for us the guy behind Backbone.js - Jeremy Ashkenas - also developed CoffeeScript - a small language that compiles into JavaScript & solves most of these problems. In the words of Jeremy:  "... [CoffeeScript] attempts to take the beautiful dynamic semantics of JavaScript - object literals, function expressions, prototypal inheritance - and express them in a clear, readable, minimal way". Here's an example Jeremy gave to illustrate that (this and the quote are taken from the chapter he wrote in Alex MacCaw's book on CoffeeScript):

 

JavaScript:

var square = function(x) {
   return x * x;
}

 

 

CoffeeScript:

square = (x) -> x * x

 

 

So, in this post I'll show you how to convert a JavaScript application - the UML editor I'm building - into CoffeeScript. Starting from the next post, the series name will therefore be: How to build a UML editor in CoffeeScript.

 

 

The benefits of CoffeeScript

 

CoffeeScript is a language inspired by Ruby & Python, which achieves 3 main goals:

  • Fix the inherent problems in JavaScript
  • Add expressiveness to the language, without breaking it's semantics
  • Clean the code and make it more readable

 

To name just a few problems of JavaScript that CoffeeScript solves:

  • Context of functions changes ("this"), e.g., when used as event-handlers/callbacks
  • "var" is often forgotten, causing variables to leak to the global namespace
  • The global namespace
  • Using "==" instead of "===", which can cause varoius JavaScript glitches
  • Omitting the need for semi-colon, which also causes various glitches when forgotten

 

The improvements in expressiveness include:

  • list comprehension, e.g., squares = (x**x for x in arr)
  • string formatting, e.g., s = "Hello #{user.name}"
  • array & object unpacking (in function calls & assignments)

 

CoffeeScript borrows from Python the idiom of determining block scope by indentation, which cleans the code & improves readability. It also cleans the code by:

  • simple syntax for creating classes, constructors, referring to super, extending other classes
  • omitting the need for parens ("()", and "{}")
  • simple syntax for instance variables & static members (@ instead of this, :: instead of .prototype.)
  • properly walking & efficient "for .. in .." loops
  • existence checking on objects & method calls (e.g., "obj.do?()" which will not be invoked if obj or do are undefined)
  • do - immediate function execution, useful for creating closures
  • no need for "return" - the last expression in a function is the return value

 

Programmers familiar with Python & Ruby will find it easy to learn & use CoffeeScript. Although there are subtle things to be aware of, such as not having negative list indices (e.g., list[-1] for accessing the last element), which breaks the semantics of JavaScript.

 

It's important to note that the code CoffeeScript compiles to isn't slower than hand-written code. Also, the generated code passes the inspection of JSLint - the code quality inspection tool we mentioned in the 1st post, written by Douglass Crockford. Indeed, CoffeeScript adds a necessary compilation step to JavaScript, but in practice we anyway do that for minifying the code.

 

To learn more on CoffeeScript:

 

Transforming a JavaScript project to CoffeeScript

 

First of all, I'm going to change the directory structure, to make it more similar to the Django project directory structure, which I'm used to . I'll move the static resources to a static folder, which will include the output of the CoffeeScript compilation. The references to the the static files in our HTML file need to be updated, to reflect the new directory structure (WebStorm does this refactoring transparently). I'll add a folder for the 1st module of the application, which I'll call model_app. Inside it, I'll add 3 CoffeeScript source files, corresponding to the 3 JavaScript source files in the application:

 

 

Next, in order to transform the existing JavaScript files, I'll start with an automatic translation tool: Js2Coffee.org:

 

The CoffeeScript code the tool generates is a straight-forward translation, which won't utilize all the expressive power of CoffeeScript, mainly because it merely reflects the idioms of JavaScript, translated to CoffeeScript. So, after the automatic translation, we'll need to go over the code & add the improved idioms of CoffeeScript, such as:

  • String formatting
  • List comprehensions
  • Using "do" when creating closures
  • Using the "extends" keyword, instead of an "extend" method (such as the one in Backbone.js)

 

 

Another thing we'll need to do, is to fix any improper use of the global namespace we had in the JavaScript implementation. CoffeeScript prevents this by wrapping any code with a function. The common best-practice in JavaScript is to define a single container in the global namespace, in which you place all of your "global" resources. CoffeeScript enables you to do this by using the "exports" command. So, in our models source file, we would like to expose the classes & models like this:

 

exports = this
exports.Class = Class
exports.current_diagram = new ClassDiagram 

 

"this" will refer to the global namespace, which in our case is the browser "window".

 

 

So, the initial CoffeeScript source files look like this:

 

models.coffee:

class DiagramElement extends Backbone.Model
  defaults:
    x: 0
    y: 0
    width: 40
    height: 100

class Class extends DiagramElement
  defaults:
    name: ""
    super_classes: []
    public_methods: []
    private_methods: []

  get_all_members: =>
    result = []
    name = @get("name")
    if @get("super_classes").length > 0
      name += " : "
      name += @get("super_classes").join(",")
    result.push name
    _.each @get("public_methods"), (n) ->
      result.push n

    _.each @get("private_methods"), (n) ->
      result.push n

    result

class ClassDiagram extends Backbone.Collection
  model: Class
  source: ""
  get_classes: =>
    (model.get 'name' for model in @models)

exports = this
exports.Class = Class
exports.current_diagram = new ClassDiagram

 

 

controllers.coffee:

create_parser = ->
  previous_source = ""
  current_class = ""
  current_super_classes = []
  members_map =
    "+": []
    "-": []

  add_class = ->
    cls = new Class(
      name: current_class
      super_classes: _.clone(current_super_classes)
      public_methods: _.clone(members_map["+"])
      private_methods: _.clone(members_map["-"])
    )
    existing_class = current_diagram.find((c) ->
        c.get("name") is cls.name
    )
    if existing_class
      existing_class.set cls
    else
      current_diagram.add cls
    current_class = ""
    current_super_classes = []
    members_map["+"] = []
    members_map["-"] = []

  ->
    source = $("#source_input").val()
    unless source is previous_source
      previous_source = source
      lines = source.split("\n")
      _.each lines, (line) ->
        return  if line.trim().length is 0
        first_char = line.charAt(0)
        if members_map[first_char]
          members_map[first_char].push line.substr(1)
        else
          add_class()  unless current_class is ""
          if line.indexOf(":") >= 0
            parts = line.split(":")
            current_class = parts[0].trim()
            current_super_classes = parts[1].trim().split(",")
          else
            current_class = line

      add_class()

$ ->
  drawer = new ClassDiagramDrawer
  drawer.init()
  parse = create_parser()
  $("a[data-toggle=\"tab\"]").on "shown", (e) ->
    parse()

  current_diagram.on "add", drawer.draw_class, drawer

 

 

views.coffee:

class ClassDiagramDrawer
  svg: null
  current_offset: 0
  default_class_width: 150
  default_member_height: 20
  default_member_spacing: 20
  class_containers: {}
  init: ->
    w = 675
    h = 360
    pack = d3.layout.pack().size([ w - 4, h - 4 ]).value((d) ->
        d.size
    )
    @svg = d3.select("#chart").append("svg").attr("width", w).attr("height", h).attr("class", "pack").append("g").attr("transform", "translate(2, 2)")

  draw_class: (model) ->
    data = model.get_all_members()
    x = @current_offset
    y = 50
    margin = 10
    g = @svg.append("g").attr("x", x).attr("y", y)
    @class_containers[model.get("name")] = g
    g.append("rect").attr("x", x).attr("y", y).attr("width", @default_class_width).attr "height", data.length * @default_member_height
    that = this
    g.selectAll("text").data(data).enter().append("text").attr("x", x + margin).attr("y", (d, i) ->
        y + margin + i * that.default_member_height
    ).attr("dy", ".35em").text String
    g.append("line").attr("x1", x).attr("y1", y + @default_member_height).attr("x2", x + @default_class_width).attr "y2", y + @default_member_height
    @current_offset += @default_class_width + @default_member_spacing

exports = this
exports.ClassDiagramDrawer = ClassDiagramDrawer

 

 

 

Compiling the code

 

First of all we need to install CoffeeScript, which provides the coffee executable for compiling the source files. The installation is done using npm - the Node.js package manager, so you'll need to install the latest version of Node.js & npm. You can find more info in the CoffeeScript web site.

 

Once you have CoffeeScript installed, the command for compiling our source folder to the static folder is:
coffee --compile --output static/js model_app

 

Note that although our code depends on 3rd-party libraries, they aren't required for the compilation, which is straight-forward lexical compilation. 

 

We don't want to manuallly compile the source files each time we make a change, so luckily the coffee executable comes with another option "-w" for watching the source folder for changes, & compiling any file whenever it changes. CoffeeScript also comes with a build tool, similar to make & rake, called cake. To use it, you need to create a file named Cakefile, in which you define your build & managament tasks. Let's add a Cakefile with 2 tasks for now: build & watch:

 

Cakefile:

fs = require 'fs'

{print} = require 'util'
{spawn} = require 'child_process'


build = (callback) ->
  coffee = spawn 'coffee', ['-c', '-o', 'static/js', 'model_app']
  coffee.stderr.on 'data', (data) ->
    process.stderr.write data.toString()
  coffee.stdout.on 'data', (data) ->
    process.stdout.write data.toString()
  coffee.on 'exit', (code) ->
    callback?() if code is 0


task 'build', 'Build model_app', ->
  build()


watch = (callback) ->
  coffee = spawn 'coffee', ['-w', '-c', '-o', 'static/js', 'model_app']
  coffee.stderr.on 'data', (data) ->
    process.stderr.write data.toString()
  coffee.stdout.on 'data', (data) ->
    process.stdout.write data.toString()
  coffee.on 'exit', (code) ->
    callback?() if code is 0


task 'watch', 'Watch model_app and compile when it changes', ->
  watch()

 

 

To compile the code, whenever it changes, run this command:

cake watch

 

 

 

Note that there are more structured ways to make the build, for example using Stitch & Hem, which use the best-practices of Node.js library packaging, & also bundle the output javascript together into a single file. They also enable you to include the build process in the request/response cycle, so that there's no need for build, but this requires us to have a server back-end, which is something we're trying to avoid..

 

 

 

 

Next..

 

Now that the source code is shorter & cleaner, I can't wait for the next post to start adding functionality to the application, such as graphical relations & persistence. I also noticed that in the hurry of writing the previous post, I completely forgot to use Backbone.js Views, e.g., for the diagram widget. Next time.

 

1

Comments