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 DEFUN
ing 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))))