exupero's blog
RSSApps

Clojure-flavored JavaScript

While ClojureScript is my preferred means of writing JavaScript, for simple cases like the one-off scripts I occasionally use for blog posts, it produces far more code than the few kilobytes I need. Advanced and minified compilation of (js/console.log "Hello World") may be "only" 1.4 kilobytes, but making any use of Clojure's persistent collections rapidly inflates build size: console logging an empty vector in place of a string produces a weighty 91 kilobytes of minified code. Persistent collections are one of the best parts of Clojure, but for small scripts I can manage without them. One thing I wanted to see if I could keep, however, was the Lisp syntax, including Clojure's basic macros like ->, doto, and if-let. To do that, I created a kind of poor-man's ClojureScript, what I call "Clojure-flavored JavaScript". So far I've only implemented as much as I need, but here are the rudiments.

After reading a form, ClojureScript performs an analysis phase before compiling the final JavaScript, but for my use, macro expansion is sufficient:

(clojure.walk/macroexpand-all
  '(when-let [a Math/PI]
     (doto js/document
       (.appendChild nil))))
(let*
 [temp__5804__auto__ Math/PI]
 (if
  temp__5804__auto__
  (do
   (let*
    [a temp__5804__auto__]
    (let*
     [G__294864 js/document]
     (. G__294864 appendChild nil)
     G__294864)))))

To generate JavaScript code, we can walk a macro-expanded form and handle whatever we find, which beside atoms like symbols can also include Clojure's various special forms (such as let*, fn*, and do), whatever clojure.core functions are used in expanded macros (such as first and seq, used by destructuring logic), and whatever functions we choose to use (such as str for string concatenation). Here we'll only implement enough of a compiler to compile the above form, so we'll only deal with a few atoms and special forms.

ClojureScript compilation uses an emits function to write compiled JavaScript code to *out*, so we'll follow a similar pattern:

(defn emits [& ss]
  (doseq [s ss]
    (cond
      (seq? s)    (apply emits s)
      (string? s) (.write *out* s))))

When generating symbols, we need to account for the Math namespace and special characters that JavaScript doesn't allow in names. Here I'm only handling #, which is present in symbols created by macros, and -, which is conventionally used in Lisp to separate words.

(defn emit-symbol [s]
  (let [s (if (= "Math" (namespace s))
            (str (namespace s) "." (name s))
            (name s))]
    (emits
      (-> (name s)
        (clojure.string/replace "#" "_HASH_")
        (clojure.string/replace "-" "_")))))

Keywords we'll emit as strings:

(defn emit-keyword [k]
  (emits "'" (name k) "'"))

Here's a function to handle the forms we've defined so far:

(defn emit [form]
  (cond
    (nil? form)     (emits "null")
    (number? form)  (emits (str form))
    (string? form)  (emits "'" (clojure.string/replace form "'" "\\'") "'")
    (symbol? form)  (emit-symbol form)
    (keyword? form) (emit-keyword form)))

To compile the example above we need to handle special forms and function invocations. Let's create create an emit-invoke multimethod that dispatches on the first form in a list:

(defmulti emit-invoke first)

By default, we'll just invoke whatever function name is given:

(defmethod emit-invoke :default [[f & args]]
  (emits (name f) "(")
  (doseq [arg (butlast args)]
    (emit arg)
    (emits ", "))
  (emit (last args))
  (emits ")"))

Add emit-invoke to the original emit function:

(defn emit [form]
  (cond
    (nil? form)     (emits "null")
    (number? form)  (emits (str form))
    (string? form)  (emits "'" (clojure.string/replace form "'" "\\'") "'")
    (symbol? form)  (emit-symbol form)
    (keyword? form) (emit-keyword form)
    (seq? form)     (emit-invoke form)))

And here's a function to carry out the whole compilation pipeline:

(defn compile-to-javascript [form]
  (with-out-str
    (emit (clojure.walk/macroexpand-all form))))

Let's compile with what we have to see how close we are:

(compile-to-javascript
  '(when-let [a Math/PI]
     (doto js/document
       (.appendChild nil))))
let*(, if(temp__5804__auto__, do(let*(, let*(, .(G__294918, appendChild, null), G__294918)))))

We need to define some special forms. Here's if, do, let*, and .:

(defmethod emit-invoke 'if [[_ pred then else]]
  (emits "((")
  (emit pred)
  (emits ") ? ")
  (emit then)
  (emits " : ")
  (emit else)
  (emits ")"))
(defmethod emit-invoke 'do [[_ & body]]
  (emits "(() => {\n")
  (doseq [form (butlast body)]
    (emit form)
    (emits ";\n"))
  (emits "return ")
  (emit (last body))
  (emits ";\n})()"))
(defmethod emit-invoke 'let* [[_ bindings & body]]
  (emits "(() => {\n")
  (doseq [[k v] (partition 2 bindings)]
    (if (= '_ k)
      (do
        (emit v)
        (emits ";\n"))
      (do
        (emits "var ")
        (emit k)
        (emits " = ")
        (emit v)
        (emits ";\n"))))
  (doseq [b (butlast body)]
    (emit b)
    (emits ";\n"))
  (emits "return ")
  (emit (last body))
  (emits ";\n})()"))
(defmethod emit-invoke '. [[_ target method & args]]
  (emit target)
  (emits "." (name method) "(")
  (when (seq args)
    (doseq [arg (butlast args)]
      (emit arg)
      (emits ", "))
    (emit (last args)))
  (emits ")"))

Now compiling gives us:

(() => {
var temp__5804__auto__ = Math.PI;
return ((temp__5804__auto__) ? (() => {
return (() => {
var a = temp__5804__auto__;
return (() => {
var G__294987 = document;
G__294987.appendChild(null);
return G__294987;
})();
})();
})() : null);
})()

Generated symbol names and unnecessary parentheses and semicolons can be cleaned up by a minifier such as Google's Closure Compiler:

(()=>{if(Math.PI){var a=document;a.appendChild(null)}else a=null;return a})();

While equivalent hand-written JavaScript could easily be smaller, the goal here is merely to be competitive.

To make a fully fledged Clojure-flavored JavaScript, you'll need to implement few more special forms, as well as handle core functions such as str and +.

In my brief experience using such a compiler, I have found it slightly dizzying to write Clojure code that only relies on JavaScript functions and mutative idioms, but so far the dizziness has been tolerable for small experiments.