Michael J. Forster
Building an Application with the Closure Library… and Lisp!

Using the project directory and Hunchentoot infrastructure I created previously, I will use Common Lisp to write the notepad application described in Google’s Building an Application with the Closure Library.

The Google tutorial illustrates the Closure namespace mechanism, DOM construction, and use of a Closure Library class. I’m interested in the first two features, in particular.

A First Pass

I start by creating and editing a notepad.lisp file to define Hunchentoot easy handlers corresponding to the notepad.html and notepad.js from the tutorial:

(hunchentoot:define-easy-handler (notepad-js :uri "/notepad.js") ()
  (setf (hunchentoot:content-type*) "text/javascript")
  (ps:ps
    (ps:chain goog (provide "tutorial.notepad"))

    (ps:chain goog (require "goog.dom"))
    (ps:chain goog (require "goog.ui.Zippy"))

    (setf (ps:@ tutorial notepad append-notes)
          (lambda (data note-container)
            (dolist (datum data)
              (ps:chain goog dom
                        (append-child note-container
                                      (ps:chain tutorial notepad
                                                (make-note-dom datum)))))))

    (setf (ps:@ tutorial notepad make-note-dom)
          (lambda (note-datum)
            (let ((header-element
                   (ps:chain goog dom
                             (create-dom "div"
                                         (ps:create :style "background-color:#EEE")
                                         (ps:@ note-datum :title))))
                  (content-element
                   (ps:chain goog dom
                             (create-dom "div"
                                         nil
                                         (ps:@ note-datum :content)))))
              (ps:new (ps:chain goog ui (-Zippy header-element content-element)))
              (ps:chain goog dom
                        (create-dom "div"
                                    nil
                                    header-element
                                    content-element)))))))

(hunchentoot:define-easy-handler (notepad-html :uri "/notepad.html") ()
  (cl-who:with-html-output-to-string (*standard-output* nil :prologue t)
    (:html
     (:head
      (:title "Notepad")
      (:script :src "/js/goog/base.js")
      (:script :src "/notepad.js"))
     (:body
      (:div :id "notes")
      (:script
        (cl-who:str
         (ps:ps
           (defun main ()
             (let ((note-data
                    (list (ps:create :title "Note 1"
                                     :content "Content of Note 1")
                          (ps:create :title "Note 2"
                                     :content "Content of Note 2")))
                   (note-list-element
                    (ps:chain goog dom (get-element "notes"))))
               (ps:chain tutorial notepad
                         (append-notes note-data
                                       note-list-element))))
           (main))))))))

Next, I add notepad.lisp to the components list of the system definition in grok-google-closure-lisp.asd. Then, I stop the Hunchentoot acceptor, reload the project, and start the Hunchentoot acceptor:

GROK-GOOGLE-CLOSURE-LISP> (stop)
GROK-GOOGLE-CLOSURE-LISP> (ql:quickload "grok-google-closure-lisp")
...
=> ("grok-google-closure-lisp")
GROK-GOOGLE-CLOSURE-LISP> (start)

Now, I can browse the application URL http://localhost:4242/notepad.html.

I want to note that I did not translate the tutorial’s Javascript to Parenscript directly. That was deliberate.

The debate over the pros and cons (even the definition) of OOP rages elsewhere, but, here, I am concerned only with the simplicity, directness, and clarity of the program. I think the tutorial is less simple, direct, and clear than it could be.

To begin with, the definition of main() represents note data simply enough. However, the call to makeNotes() doesn’t just make notes (it doesn’t even make notes, it makes DOM nodes), it also, ultimately, appends them to the parent DOM.

Of course, makeNotes() doesn’t append the nodes itself: the makeNoteDom() method of the Note object does that, after it constructs the node from its internal data, and using a reference to the parent DOM included in the data for each note!

Why do makeNotes() and makeNoteDom() make DOM nodes and have the side effect of changing the parent DOM? Why does the Note object have and use a reference to the parent DOM?

One more annoyance (a smaller one): Why does makeNotes() build and return an unused array of the constructed nodes?

This, as Rich Hickey might say, is complected.

Thus, I represent and access the data as a list of property lists (which Parenscript will translate to an array of Javascript objects) and eliminate the Note constructor, I use MAKE-NOTE-DOM only to construct a DOM node from a note and eliminate the note field referencing the parent DOM, and I have APPEND-NOTES append the constructed nodes to the parent DOM.

I also want to draw attention to the definitions of APPEND-NOTES and MAKE-NOTE-DOM. To work with Closure’s namespace convention I use goog.provide() and, rather than DEFUNing the functions, I assign anonymous functions to those property names in the namespace object:

(ps:chain goog (provide "tutorial.notepad"))
...

(setf (ps:@ tutorial notepad append-notes)
      (lambda (data note-container)
        ...))

(setf (ps:@ tutorial notepad make-note-dom)
      (lambda (note-datum)
        ...))

Then, of course, rather than calling the functions directly, I must use Parenscript’s CHAIN to access the property in the namespace:

(ps:chain tutorial notepad
          (append-notes note-data
                        note-list-element))

I would like to suppress these details of defining functions in a Closure-like namespace.

Parenscript Namespaces

Parenscript offers a mechanism to prefix Javascript names when translating symbols in a Lisp package. Using that, I can rewrite the function definitions, storing them in a tutorial-notepad.paren file:

(in-package "TUTORIAL.NOTEPAD")

(ps:chain goog (require "goog.dom"))
(ps:chain goog (require "goog.ui.Zippy"))

(defun tutorial.notepad::append-notes (data note-container)
  (dolist (datum data)
    (ps:chain goog dom
              (append-child note-container
                            (ps:chain tutorial
                                      notepad
                                      (make-note-dom datum))))))

(defun tutorial.notepad::make-note-dom (note-datum)
  (let ((header-element
         (ps:chain goog dom
                   (create-dom "div"
                               (ps:create :style "background-color:#EEE")
                               (ps:@ note-datum :title))))
        (content-element
         (ps:chain goog dom
                   (create-dom "div"
                               nil
                               (ps:@ note-datum :content)))))
    (ps:new (ps:chain goog ui (-Zippy header-element content-element)))
    (ps:chain goog dom
              (create-dom "div"
                          nil
                          header-element
                          content-element))))

Then, I can define a Lisp package, set the Parenscript prefix, and compile the tutoral-notepad.paren file:

(defpackage #:tutorial.notepad
  (:use #:cl))
(in-package #:tutorial.notepad)
(setf (ps:ps-package-prefix "TUTORIAL.NOTEPAD")
      "tutorial.notepad.")
(ps:ps-compile-file "/tmp/tutorial-notepad.paren")
=> ...

However, that won’t work. It doesn’t provide the Closure namespace. It prefixes all symbols in the package, including those from external Javascript libraries, generating

tutorial.notepad.goog.require('goog.dom');
tutorial.notepad.goog.require('goog.ui.Zippy');

instead of

goog.require('goog.dom');
goog.require('goog.ui.Zippy');

Finally, it defines a Javascript function with a prefixed name rather than assigning an anonymous function to the property in the namespace, generating

function tutorial.notepad.appendNotes(tutorial.notepad.data,
                                      tutorial.notepad.noteContainer) {
    for (var tutorial.notepad.datum = null, _js_idx4 = 0;
         _js_idx4 < tutorial.notepad.data.length;
         _js_idx4 += 1) {
        tutorial.notepad.datum = tutorial.notepad.data[_js_idx4];
        tutorial.notepad.goog.tutorial.notepad.dom.tutorial.notepad.appendChild(tutorial.notepad.noteContainer,
            tutorial.notepad.tutorial.tutorial.notepad.notepad.tutorial.notepad.makeNoteDom(tutorial.notepad.datum));
    };
};

instead of

tutorial.notepad.appendNotes = function (data, noteContainer) {
    for (var datum = null, _js_idx101 = 0;
          _js_idx101 < data.length;
         _js_idx101 += 1) {
        datum = data[_js_idx101];
        goog.dom.appendChild(noteContainer, tutorial.notepad.makeNoteDom(datum));
    };
};

A Simple Solution to a Simple Problem

Tempting as it is to consider patching Parenscript to address the above issues, it’s important to remember that Parenscript trades complete translation of Common Lisp for a reduction in Javascript runtime overhead.

Following Parenscript’s lead (and, indeed, that of Common Lisp’s DEFUN), I can write a Parenscript macro (using DEFPSMACRO) that simply hides the details of building a function and assigning it to a name in the namespace:

(ps:defpsmacro defun-in-namespace (namespace-list lambda-list &body body)
  "Defines a new function with the fully qualified Google
namespace /namespace-list/.  Assumes the namespace has been
defined via goog.provide()."
  `(setf (ps:@ ,@namespace-list)
         (lambda (,@lambda-list)
           ,@body)))

Then, I can define the functions in the NOTEPAD-JS handler as follows:

(defun-in-namespace (tutorial notepad append-notes) (data note-container)
  (dolist (datum data)
    (ps:chain goog dom
              (append-child note-container
                            (ps:chain tutorial notepad
                                      (make-note-dom datum))))))

(defun-in-namespace (tutorial notepad make-note-dom) (note-datum)
  (let ((header-element
         (ps:chain goog dom
                   (create-dom "div"
                               (ps:create :style "background-color:#EEE")
                               (ps:@ note-datum :title))))
        (content-element
         (ps:chain goog dom
                   (create-dom "div"
                               nil
                               (ps:@ note-datum :content)))))
    (ps:new (ps:chain goog ui (-Zippy header-element content-element)))
    (ps:chain goog dom
              (create-dom "div"
                          nil
                          header-element
                          content-element))))
  1. michaeljforster posted this