Saturday, April 21, 2012

Preparing for round-trip conversions between CoffeeScript and Javascript

I have been working quite a bit on a Cordova/PhoneGap plugin Cordova/PhoneGap SQLite plugin (introduced here) and have encountered a need for round-trip conversions between CoffeeScript and JavaScript. The Javascript portion of the iOS version was originally authored in CoffeeScript and compiled to Javascript. For the Android version, I had originally made an adaptation from a version in the Cordova/PhoneGap repository as of October 2011 and then the Javascript portion was largely rebuilt by someone else, based on generated code from the iOS version, to provide some improvements for batch processing. I would like to refactor the code to a common base that can be easily enhanced and maintained in both CoffeeScript and Javascript.


The problem


I already posted several reasons why I find CoffeeScript so much better to work with and I continue to feel the same way for this project, however there are still people in the community who try to stay away from CoffeeScript. Yes there are tools like js2coffee to convert Javascript back to CoffeeScript but there are quite a few problems right now including:

  • By default, the coffee compiler puts the entire code into a special top-level block with the extra indentation but js2coffee does not take the code back out of the block. The only workaround is to add a -b flag to coffee to skip the extra block.
  • Every CoffeeScript class is converted to a block that is achieving the effect in Javascript using constructor functions and prototype members. The conversion back to CoffeeScript will make a small tree of constructor and prototype functions. This becomes worse when class members and/or class functions are used.
  • When var declarations are generated in Javascript, the conversion back to CoffeeScript adds extra statements assigning the variables to undefined.
  • Of course, comments and perhaps extra line spacing are normally ignored. I believe there is a version of js2coffee that can preserve the comment lines but have not taken an opportunity to try it out.

Approach


Of course, the biggest challenge programming-wise is to convert the classes to using constructor and prototype functions. Fortunately I do not have to deal with inheritance for this case so basically each member function is converted to a prototype member declaration, static members get special treatment, and the class declarations are replaced with constructor functions.


The other major change is to enclose the entire CoffeeScript code into a do block, so that only 2 functions will have to be exported.


Changing a member function into a prototype function would indent the function code one block less, while putting the code into a do block would indent the code one block more. So I was able to let the function code remain with some extra indentation, which will be perfect when the code will be put into a do block.


Sample class


So here is a CoffeeScript class, to handle a sqlite transaction:



class SQLitePluginTransaction


  constructor: (@dbPath) ->
    @executes = []


  executeSql: (sql, values, success, error) ->
    txself = @
    successcb = null
    if success
      successcb = (execres) ->
        saveres = execres
        res =
          rows:
            item: (i) ->
              saveres.rows[i]
            length: saveres.rows.length
          rowsAffected: saveres.rowsAffected
          insertId: saveres.insertId || null
        success(txself, res)
    errorcb = null
    if error
      errorcb = (res) ->
        error(txself, res)
    @executes.push getOptions({ query: [sql].concat(values || []), path: @dbPath }, successcb, errorcb)
    return


  complete: (success, error) ->
    throw new Error "Transaction already run" if @__completed
    @__completed = true
    txself = @
    successcb = (res) ->
      success(txself, res)
    errorcb = (res) ->
      error(txself, res)
    begin_opts = getOptions({ query: [ "BEGIN;" ], path: @dbPath })
    commit_opts = getOptions({ query: [ "COMMIT;" ], path: @dbPath }, successcb, errorcb)
    executes = [ begin_opts ].concat(@executes).concat([ commit_opts ])
    opts = { executes: executes }
    Cordova.exec("SQLitePlugin.backgroundExecuteSqlBatch", opts)
    @executes = []
    return

and the generated Javascript (with no extra block indentation, small font to save space):




SQLitePluginTransaction = (function() {


  function SQLitePluginTransaction(dbPath) {
    this.dbPath = dbPath;
    this.executes = [];
  }


  SQLitePluginTransaction.prototype.executeSql = function(sql, values, success, error) {
    var errorcb, successcb, txself;
    txself = this;
    successcb = null;
    if (success) {
      successcb = function(execres) {
        var res, saveres;
        saveres = execres;
        res = {
          rows: {
            item: function(i) {
              return saveres.rows[i];
            },
            length: saveres.rows.length
          },
          rowsAffected: saveres.rowsAffected,
          insertId: saveres.insertId || null
        };
        return success(txself, res);
      };
    }
    errorcb = null;
    if (error) {
      errorcb = function(res) {
        return error(txself, res);
      };
    }
    this.executes.push(getOptions({
      query: [sql].concat(values || []),
      path: this.dbPath
    }, successcb, errorcb));
  };


  SQLitePluginTransaction.prototype.complete = function(success, error) {
    var begin_opts, commit_opts, errorcb, executes, opts, successcb, txself;
    if (this.__completed) throw new Error("Transaction already run");
    this.__completed = true;
    txself = this;
    successcb = function(res) {
      return success(txself, res);
    };
    errorcb = function(res) {
      return error(txself, res);
    };
    begin_opts = getOptions({
      query: ["BEGIN;"],
      path: this.dbPath
    });
    commit_opts = getOptions({
      query: ["COMMIT;"],
      path: this.dbPath
    }, successcb, errorcb);
    executes = [begin_opts].concat(this.executes).concat([commit_opts]);
    opts = {
      executes: executes
    };
    Cordova.exec("SQLitePlugin.backgroundExecuteSqlBatch", opts);
    this.executes = [];
  };

  return SQLitePluginTransaction;

})();

and when converted back to CoffeeScript:

SQLitePluginTransaction = (->
  SQLitePluginTransaction = (dbPath) ->
    @dbPath = dbPath
    @executes = []
  SQLitePluginTransaction::executeSql = (sql, values, success, error) ->
    errorcb = undefined
    successcb = undefined
    txself = undefined
    txself = this
    successcb = null
    if success
      successcb = (execres) ->
        res = undefined
        saveres = undefined
        saveres = execres
        res =
          rows:
            item: (i) ->
              saveres.rows[i]

            length: saveres.rows.length

          rowsAffected: saveres.rowsAffected
          insertId: saveres.insertId or null

        success txself, res
    errorcb = null
    if error
      errorcb = (res) ->
        error txself, res
    @executes.push getOptions(
      query: [ sql ].concat(values or [])
      path: @dbPath
    , successcb, errorcb)

  SQLitePluginTransaction::complete = (success, error) ->
    begin_opts = undefined
    commit_opts = undefined
    errorcb = undefined
    executes = undefined
    opts = undefined
    successcb = undefined
    txself = undefined
    throw new Error("Transaction already run")  if @__completed
    @__completed = true
    txself = this
    successcb = (res) ->
      success txself, res

    errorcb = (res) ->
      error txself, res

    begin_opts = getOptions(
      query: [ "BEGIN;" ]
      path: @dbPath
    )
    commit_opts = getOptions(
      query: [ "COMMIT;" ]
      path: @dbPath
    , successcb, errorcb)
    executes = [ begin_opts ].concat(@executes).concat([ commit_opts ])
    opts = executes: executes
    Cordova.exec "SQLitePlugin.backgroundExecuteSqlBatch", opts
    @executes = []

  SQLitePluginTransaction
)()

This code is not so great to work with, considering the extra block with indentation that is added in the round trip from a class declaration.

Prototype members

So the first task is to convert the member functions to prototype members, the diffsS are shown here:

@@ -111,7 +112,7 @@ class SQLitePluginTransaction
   constructor: (@dbPath) ->
     @executes = []
 
-  executeSql: (sql, values, success, error) ->
+SQLitePluginTransaction::executeSql = (sql, values, success, error) ->
     txself = @
     successcb = null
     if success
@@ -132,7 +133,7 @@ class SQLitePluginTransaction
     @executes.push getOptions({ query: [sql].concat(values || []), path: @dbPath }, successcb, errorcb)
     return
 
-  complete: (success, error) ->
+SQLitePluginTransaction::complete = (success, error) ->
     throw new Error "Transaction already run" if @__completed
     @__completed = true
     txself = @

So all that remains in the original class block is the constructor function. The generated Javascript is omitted in order to save some space. Here is the CoffeeScript that comes out of the round trip:

SQLitePluginTransaction = (->
  SQLitePluginTransaction = (dbPath) ->
    @dbPath = dbPath
    @executes = []
  SQLitePluginTransaction
)()
SQLitePluginTransaction::executeSql = (sql, values, success, error) ->
  errorcb = undefined
  successcb = undefined
  txself = undefined
  txself = this
  successcb = null
  if success
    successcb = (execres) ->
      res = undefined
      saveres = undefined
      saveres = execres
      res =
        rows:
          item: (i) ->
            saveres.rows[i]

          length: saveres.rows.length

        rowsAffected: saveres.rowsAffected
        insertId: saveres.insertId or null

      success txself, res
  errorcb = null
  if error
    errorcb = (res) ->
      error txself, res
  @executes.push getOptions(
    query: [ sql ].concat(values or [])
    path: @dbPath
  , successcb, errorcb)

SQLitePluginTransaction::complete = (success, error) ->
  begin_opts = undefined
  commit_opts = undefined
  errorcb = undefined
  executes = undefined
  opts = undefined
  successcb = undefined
  txself = undefined
  throw new Error("Transaction already run")  if @__completed
  @__completed = true
  txself = this
  successcb = (res) ->
    success txself, res

  errorcb = (res) ->
    error txself, res

  begin_opts = getOptions(
    query: [ "BEGIN;" ]
    path: @dbPath
  )
  commit_opts = getOptions(
    query: [ "COMMIT;" ]
    path: @dbPath
  , successcb, errorcb)
  executes = [ begin_opts ].concat(@executes).concat([ commit_opts ])
  opts = executes: executes
  Cordova.exec "SQLitePlugin.backgroundExecuteSqlBatch", opts
  @executes = []

So the member functions are much better to work with after going through the round trip and now the issue remains with the constructor.

Constructor function

So all that remains of the class declaration:ss

class SQLitePluginTransaction


  constructor: (@dbPath) ->
    @executes = []

So to replace this with the constructor function, we can extract the constructor function from the generated code above leaving out the extra declaration block:
  SQLitePluginTransaction = (dbPath) ->
    @dbPath = dbPath
    @executes = []

But there is something that js2coffee does not tell us: we need to add an empty return statement otherwise the constructor function will return the last expression evaluated and the user application code will fail. So here is the desired function constructor code:

SQLitePluginTransaction = (dbPath) ->
    @dbPath = dbPath
    @executes = []
    return

and after a round trip:

SQLitePluginTransaction = (dbPath) ->
  @dbPath = dbPath
  @executes = []

SQLitePluginTransaction::executeSql = (sql, values, success, error) ->
  errorcb = undefined
  successcb = undefined
  txself = undefined
  txself = this
  successcb = null
  if success
    successcb = (execres) ->
      res = undefined
      saveres = undefined
      saveres = execres
      res =
        rows:
          item: (i) ->
            saveres.rows[i]

          length: saveres.rows.length

        rowsAffected: saveres.rowsAffected
        insertId: saveres.insertId or null

      success txself, res
  errorcb = null
  if error
    errorcb = (res) ->
      error txself, res
  @executes.push getOptions(
    query: [ sql ].concat(values or [])
    path: @dbPath
  , successcb, errorcb)

SQLitePluginTransaction::complete = (success, error) ->
  begin_opts = undefined
  commit_opts = undefined
  errorcb = undefined
  executes = undefined
  opts = undefined
  successcb = undefined
  txself = undefined
  throw new Error("Transaction already run")  if @__completed
  @__completed = true
  txself = this
  successcb = (res) ->
    success txself, res

  errorcb = (res) ->
    error txself, res

  begin_opts = getOptions(
    query: [ "BEGIN;" ]
    path: @dbPath
  )
  commit_opts = getOptions(
    query: [ "COMMIT;" ]
    path: @dbPath
  , successcb, errorcb)
  executes = [ begin_opts ].concat(@executes).concat([ commit_opts ])
  opts = executes: executes
  Cordova.exec "SQLitePlugin.backgroundExecuteSqlBatch", opts
  @executes = []

Much better, hey? The only major problem I see is that the empty return statements were forgotten by js2coffee. Yeah some extra undefined declarations can also be removed. But at least this is usable, in case I receive patches to the Javascript version.

Class members and methods

I have actually just learned something about how class members work in CoffeeScript, which will lose their meaning in the translation to Javascript. So for one of the classes, I had first factored the regular member functions into prototype members and the following remains:

class SQLitePlugin

  constructor: (@dbPath, @openSuccess, @openError) ->
    throw new Error "Cannot create a SQLitePlugin instance without a dbPath" unless dbPath
    @openSuccess ||= () ->
      console.log "DB opened: #{dbPath}"
      return
    @openError ||= (e) ->
      console.log e.message
      return
    @open(@openSuccess, @openError)

  # Note: Class member
  # All instances will interact directly on the prototype openDBs object.
  # One instance that closes a db path will remove it from any other instance's perspective as well.
  openDBs: {}

  # Note: Class method
  @handleCallback: (ref, type, obj) ->
    callbacks[ref]?[type]?(obj)
    callbacks[ref] = null
    delete callbacks[ref]
    return

Which becomes after a round-trip:

SQLitePlugin = (->
  SQLitePlugin = (dbPath, openSuccess, openError) ->
    @dbPath = dbPath
    @openSuccess = openSuccess
    @openError = openError
    throw new Error("Cannot create a SQLitePlugin instance without a dbPath")  unless dbPath
    @openSuccess or (@openSuccess = ->
      console.log "DB opened: " + dbPath
    )
    @openError or (@openError = (e) ->
      console.log e.message
    )
    @open @openSuccess, @openError
  SQLitePlugin::openDBs = {}
  SQLitePlugin.handleCallback = (ref, type, obj) ->
    _ref = undefined
    _ref[type] obj  if typeof _ref[type] is "function"  if (_ref = callbacks[ref])?
    callbacks[ref] = null
    delete callbacks[ref]

  SQLitePlugin
)()

and the hints show up again: class member like SQLitePlugin::openDBs = ... and class method like SQLitePlugin.handleCallback = ...

So to fix the class member and method:
@@ -62,13 +61,13 @@ class SQLitePlugin
       return
     @open(@openSuccess, @openError)
 
-  # Note: Class member
-  # All instances will interact directly on the prototype openDBs object.
-  # One instance that closes a db path will remove it from any other instance's perspective as well.
-  openDBs: {}
+# Note: Class member
+# All instances will interact directly on the prototype openDBs object.
+# One instance that closes a db path will remove it from any other instance's perspective as well.
+SQLitePlugin::openDBs = {}
 
-  # Note: Class method
-  @handleCallback: (ref, type, obj) ->
+# Note: Class method (will be exported by a member of root.sqlitePlugin)
+SQLitePlugin.handleCallback = (ref, type, obj) ->
     callbacks[ref]?[type]?(obj)
     callbacks[ref] = null
     delete callbacks[ref]

So to fix the constructor:

@@ -50,8 +50,11 @@ getOptions = (opts, success, error) ->
     opts.callback = cbref(cb) if has_cbs
     opts
 
-class SQLitePlugin
-  constructor: (@dbPath, @openSuccess, @openError) ->
+# Prototype constructor function
+SQLitePlugin = (dbPath, openSuccess, openError) ->
+    @dbPath = dbPath
+    @openSuccess = openSuccess
+    @openError = openError
     throw new Error "Cannot create a SQLitePlugin instance without a dbPath" unless dbPath
     @openSuccess ||= () ->
       console.log "DB opened: #{dbPath}"
@@ -60,6 +63,7 @@ class SQLitePlugin
       console.log e.message
       return
     @open(@openSuccess, @openError)
+    return

Further

The one thing I have not really gone through here is to put the contents into a do -> block and then use the -b flag to tell the coffee compiler not to add an extra block. Then the results of round-trip transformations between CoffeeScript and Javascript should start to become less painful.