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.