Fun with Parenscript

About this post

This post is a stream-of-consciousness rambling as I kick the tires of Parenscript. This is my first time using Parenscript. See the Parenscript homepage for docs, tutorials and the like. The lisp code in this post was only tested on SBCL, but in theory should work with any lisp implementation that Parenscript supports.

About Parenscript

Parenscript is a Common Lisp library that compiles a subset of Common Lisp down to dependency-free, readable JavaScript. In the wider world of compile-to-js languages, Parenscript is a niche market, but it has a few "features" that I like.

  1. It's Common Lisp, so there's that.
  2. You can share Parenscript code between the client and server, if you're running Common Lisp on the server.
  3. No runtime dependencies. Compiles down to vanilla JS (version 1.3 by default, but you can pick your poison with *js-target-version*).
  4. There is no need for boilerplate or type definitions if you want to call JavaScript code.
  5. I get the impression that Parenscript, much like Common Lisp, is more-or-less "done." Not "done" in the sense of bug-free and perfect, but in the sense that breaking changes (or any changes, for that matter) are rarely made. The older I get, the more I like stable things.

Except for #1, these points are not necessarily unique to Parenscript, but, honestly, #1 was enough to make me want to try it out.

A Parenscript crash course

This section will present a bare-minimum introduction to the parts of Parenscript necessary to understand the code presented in this post. See the official Parenscript tutorial for more info.

One of the nice things about Parenscript is that it's basically the thinnest possible abstraction for transpiling lisp to JavaScript, so most of the the time you can guess what JavaScript will be produced for a given Parenscript expression. Here are few simple examples, with the Parenscript on the left and the corresponding JavaScript to the right. Note that the first top-level comment is just for clarification. Parenscript comments are not actually copied to the compiled JavaScript.

;; Parenscript                        /* JavaScript */
(defvar *my-global* 1234)             var MYGLOBAL = 1234;

(defun say-hello (name)               function sayHello(name) {
  (alert (concatenate                    return alert('Hello ' + name);
           'string "Hello " name)))   };

(defun foo (x)                        /** Add 42 to x. */
  "Add 42 to x."                      function foo(x) {
  (let ((y 42))                          var y = 42;
    (+ x y)))                            return x + y;
                                      };

(length '(1 2 3 4))                   [1, 2, 3, 4].length;

some-symbol                           someSymbol;
-some-symbol                          SomeSymbol;

(some-function x y z)                 someFunction(x, y, z);

"hi"                                  'hi';
'hi                                   'hi';
:hi                                   'hi';

Parenscript also includes special forms and macros for creating and accessing JavaScript objects and arrays. I won't bother typing out an English explanation of what these do, since they should be obvious from the examples below. See the Parenscript reference for details.

;; Parenscript                        /* JavaScript */
(defvar *my-array* [])                var MYARRAY = [];

'(1 2 foo)                            [1, 2, 'foo'];
'(1 (2 foo) 3)                        [1, [2, 'foo'], 3];
(list 1 2 foo)                        [1, 2, foo];
(array 1 2 foo)                       [1, 2, foo];
(make-array 1 2 foo)                  new Array(1, 2, foo);
(new (-array 1 2 foo))                new Array(1, 2, foo);

([] 1 2 foo)                          [1, 2, foo];
([] 1 (2 foo) 3)                      [1, [2, foo], 3];
([] 1 (2 foo) (3 (bar bop)))          [1, [2, foo], [3, [bar, bop]]];

(defvar *my-object* {})               var MYOBJECT = {  };

(create)                              ({  });
(create a 1 b "hi")                   ({ a : 1, b : 'hi' });
(create :a 1 :b "hi")                 ({ 'a' : 1, 'b' : 'hi' });
(create 'a 1 'b 'hi)                  ({ 'a' : 1, 'b' : 'hi' });

(getprop obj 'foo)                    obj.foo;
(getprop obj foo)                     obj[foo];
(getprop obj "foo")                   obj['foo'];
(getprop obj :foo)                    obj['foo'];

(@ foo bar baz 3 quux)                foo.bar.baz[3].quux;
(@ foo bar (bop 7))                   foo.bar[bop(7)];

(chain foo bar baz 3 quux)            foo.bar.baz[3].quux;
(chain foo bar (bop 7))               foo.bar.bop(7);

Pre-compiling Parenscript for static sites

Parenscript, being a lisp library, is geared towards a workflow where you are running Common Lisp on the server and serving up your Parenscript-compiled JS via hunchentoot or some other lisp-powered webserver. Here's an example from the Parenscript tutorial:

(define-easy-handler (example3 :uri "/example3.js") ()
  (setf (content-type*) "text/javascript")
  (ps
    (defun greeting-callback ()
      (alert "Hello World"))))

Notice that there is no stand-alone, command-line driven program that takes a Parenscript file as input and produces a JavaScript file as output. It's a lisp-centric workflow targeting existing Common Lisp users who are used to being able to seamlessly switch between editing code and compiling or evaluating forms in a REPL, without the need for an external compilation or build step. Also, it's reasonable to assume that one of the main reasons someone might choose Parenscript as their compile-to-js language of choice is that they are already familiar with Common Lisp and possibly are looking to avoid subjecting themselves to the pain of learning one or more of Webpack/Browserify/Rollup/Gulp/Grunt/NPM/Yarn/Bower, etc. There are other advantages to the Parenscript workflow beyond familiarity for Common Lisp users, like the ability to share code between the server and client side and the ability to call Common Lisp functions at JavaScript-output time.

However, in cases where you don't plan to write any backend code, so-called "serverless" architectures or static websites (like this one), it's convenient to be able to emit a JavaScript file at build-time. Thankfully, Parenscript includes the function ps-compile-file. It's trivial to write a function that calls ps-compile-file and writes the output to a new file. For example:

(defun ps-compile-file-to-file (&key (input #P"parenscript.lisp") output)
  (let ((outpath (or output (uiop:make-pathname* :defaults input :type "js"))))
    (uiop:with-enough-pathname (enough-outpath :pathname outpath)
      (with-open-file (*parenscript-stream* enough-outpath :direction :output :if-exists :supersede)
        (ps-compile-file input)))))

The ps-compile-file-to-file function accepts an :input keyword argument that should name a file containing Parenscript code that you want to compile. By default, it will write the JavaScript output to a file with the same name as input, but with file extension "js".

A complete example that you can load in your SLIME REPL is here: ps-compile-file.lisp. The comment at the top of that file describes how to configure emacs to bind ps-compile-file-to-file to a keybinding or arrange to call it automatically on every buffer save. If you want to get crazy, you could convert ps-compile-file.lisp into a runnable script or executable with something like cl-launch or roswell, and then hook it into your JavaScript build system of choice. I do not want to get crazy, so it's emacs keybindings for me.

The above ps-compile-file-to-file hack is good enough for the purpose of this post, but if you're doing any serious work in Parenscript, you might want to check out trident-mode.

For reference, all the Parenscript code in this post is included in the file parenscript.lisp, and the corresponding JavaScript produced by ps-compile-file-to-file is in parenscript.lisp.

Now that I have decent workflow setup for transpiling Parenscript code, I can start hacking.

Three easy pieces

Example 1: Toggling DOM content

I'll start with a small program that does something useless. Something where the implementation is trivial, but with a bit more complexity than "hello, world" so I can get a feel for Parenscript without getting bogged down in the implementation details.

This is JavaScript, so how about some good-old DHTML? Let's try toggling some DOM content with a link.

(defun normalize-display (display)
  "Convert the empty string to the string 'block', otherwise return
`display' unmodified."
  (if (string= display "") "block" display))

(defmacro swap (a b)
  "Swap the values of variables `a' and `b'.

Note that `a' and `b' are both evaluated twice, and must be valid
JavaScript l-values."
  (with-ps-gensyms (tmp)
    `(let ((,tmp ,a))
       (setf ,a ,b)
       (setf ,b ,tmp))))

(defun toggle-content (id-a id-b id-link)
  "Toggle the display of the elements given by `id-a' and `id-b'."
  (let* ((link (chain document (get-element-by-id id-link)))
         (element-a (chain document (get-element-by-id id-a)))
         (element-b (chain document (get-element-by-id id-b)))
         (a-display (normalize-display (@ element-a style display)))
         (b-display (normalize-display (@ element-b style display)))
         (link-text (concatenate 'string "Show "
                                 (if (equal a-display "block") id-a id-b)))
         (state (concatenate 'string a-display "," b-display)))
    (when (not (or (equal state "block,none")
                   (equal state "none,block")))
      (throw (concatenate 'string "Invalid state: " state)))
    (swap (@ element-a style display) (@ element-b style display))
    (setf (@ link inner-h-t-m-l) link-text)))

(chain document
       (get-element-by-id "toggle-link")
       (add-event-listener
        "click"
        (lambda (event)
          (chain event (prevent-default))
          (toggle-content "toggle-jelly" "toggle-cat" "toggle-link"))))

You can see the full source file in parenscript.lisp and the corresponding JavaScript in parenscript.js. If you want to see the corresponding HTML, just use your browser's "view source".

My first impression of Parenscript is that it feels more like programming JavaScript with a lispy syntax than it feels like programming in Common Lisp (fair enough, since I am ultimately writing JavaScript). For example, if I had been writing Common Lisp, I'd have probably used a cons to represent the combined a-display and b-display state tuple. But Parenscript doesn't have cons because there is no equivalent datastructure in JavaScript. I tried using a list/array, but in Parenscript equal compiles down to JavaScript's == operator, which isn't suitable for comparing JS arrays, so I just serialized the state to a string, which can be compared directly. Of course, if I was worried about efficiency, I could have expanded the when conditional to explicitly check a-display and b-display separately, or, if I was writing Common Lisp, I could have used a pattern-matching library.

The other minor point of interest in the above code is the swap macro. Common Lisp has rotatef, but Parenscript doesn't so I added swap. I later realized that Parenscript does define psetf, which could have been used instead–oops! Also, defining a macro for this is probably overkill, but it was a good excuse to try out Parenscript's macros.

Finally, here is the working example in all its glory. Scroll down a bit to get the full effect.

Show toggle-cat















This space reserved for

    JELLY STAINS!















There you have it: some DOM content being toggled by Parenscript. Isn't she glorious?

Example 2: Fun with SVG

For this example, I'll try drawing a simple SVG bar chart. The point of this example is to write more Parenscript, not necessarily to produce a beautiful plot, so I won't use any 3rd party plotting package. Instead, I'll write Parenscript to call the SVG DOM API directly. I'll plot a bar chart of the top 25 US states sorted by total agriculture exports. Exciting, I know! The data comes from Plotly's datasets repo, but I've copied it here to try and prevent the inevitable broken link: 2011_us_ag_exports.csv. If your user-agent supports it, you can hover over the individual bars in the chart to reveal a tooltip showing the exact values. I've only tested this in Chrome on Linux and Android, so if the chart looks like a jumbled pile of nonsense in your browser: ¯\_(ツ)_/¯.

Sorry, your browser does not support inline SVG.

The Parenscript code for this example starts by importing the tiny Parenscript runtime library, which consists of the following seven functions: member, map, mapcar, reduce, map-into, set-difference, and nconc. The lisp form is needed to tell the Parenscript compiler to evaluate *ps-lisp-library* in the host lisp, kind of like a comma inside a back-quoted expression.

(lisp *ps-lisp-library*)

Next, I define some small utilities. In addition to functional stand-bys like curry, flip, take, and zip, there is the array function which just returns its arguments as a JS array. Note that Parenscript already defines array as a special form that compiles down to a JavaScript array literal, and a macro list which expands to array. However, it's useful to define a real function for this that can be passed as an argument to other functions like map or reduce. The utils are rounded out with a function to sort the CSV table on a particular column (numeric-sort-by!) and a function to create a JS object from a sequence of key-value pairs (make-object-from).

;;; You can't actually call this function from Parenscript directly,
;;; since if "array" appears in the car of a sexp, Parenscript
;;; compiles it down to an array literal, like so: (array 1 2 3) -->
;;; [1,2,3]. This function is useful, however, to pass as an argument
;;; to higher-order functions, since #'make-array, #'array, and #'list
;;; won't work.
(defun array (&rest args)
  "Return the unmodified arguments array."
  args)

(defun curry (fn &rest curried-args)
  "Bind the first N `args' of `fn'."
  ;; We could just delegate to JavaScript's Function.prototype.bind().
  (lambda (&rest rest-args)
    (apply fn (append curried-args rest-args))))

(defun flip (fn)
  "Reverse the order of arguments for 2-ary function `fn'."
  (lambda (a b) (fn b a)))

(defun numeric-sort-by! (key table)
  "Numerically sort `table' on `key' in descending order."
  (chain table (sort (lambda (a b)
                       (- (+ (getprop b key)) (+ (getprop a key)))))))

(defun take (n array)
  "Take the first `n' items from `array'."
  (chain array (slice 0 n)))

(defun zip (&rest args)
  (apply #'mapcar #'array args))

(defun make-object-from (key-value-pairs)
  "Construct an Object from `key-value-pairs'.

Example:

(make-object-from ([] (a 1) (b 2) (c 3)))
-> {a: 1, b: 2, c: 3}"
  (let ((o (create)))
    (dolist (kv key-value-pairs o)
      (setf (getprop o (@ kv 0)) (@ kv 1)))))

Next comes the code for fetching the CSV file and parsing it into an array of objects. Note that even though Parenscript doesn't know anything about ES6, there is no problem using Promises or the fetch API, since they are just regular objects and/or function calls.

(defun fetch-text (url)
  "Fetch `url' and return the response body as text."
  (chain (fetch url)
         (then (lambda (response) (chain response (text))))
         (catch (lambda (error) (throw error)))))

(defun parse-csv (csv-string &optional (numeric-columns (array)))
  "Parse `csv-string' into an array of objects.

The first line of the `csv-string' is expected to contain the
header. Each line of the csv file is then parsed into an object with
properties taken from the header line.

Values for columns in `numeric-colums' are stored as numbers, rather
than strings.

Example:
(parse-csv \"a,b,c\n1,2,3\n4,5,6\" '(\"a\" \"b\"))
-> [{a: 1, b: 2, c: \"3\"}, {a: 4, b: 5, c: \"6\"}]"
  (flet ((convert (alist)
           ;; Note that alist is not a true a-list, since JavaScript
           ;; doesn't have conses. It's just a two-element array.
           (map (lambda (kv)
                  (destructuring-bind (key value) kv
                    (if (member key numeric-columns)
                        (list key (+ value))
                        kv)))
                alist)))
    (let* ((lines (chain csv-string (split #\newline)))
           (columns (chain lines (shift) (split #\,))))
      (map (lambda (line)
             (make-object-from (convert (zip columns (chain line (split #\,))))))
           lines))))

The CSV parser is fragile and will break if you give it a malformed file or something it's not expecting like quoted values, leading/trailing whitespace, etc. But for the purpose of this example, it does the job. Note that I explicitly provide a default value for the optional argument numeric-columns, like so: &optional (numeric-columns (array)). In my first attempt to define the function, I instead wrote &optional numeric-columns. In Common Lisp, this would cause numeric-colums to be nil if the caller doesn't provide it, but in Parenscript, the resulting JavaScript variable is undefined, which causes an error when I later call (member key numeric-columns). In Common Lisp, nil is synonymous with the empty list, so (member key nil) is fine and always returns nil. By providing the default value of an empty array in the Parenscript code, I avoid the undefined variable error. Alternatively, I could use Parenscript's defined macro to first test whether numeric-columns is defined before calling member.

Now that I can fetch and parse the CSV file, I need to write the functions for drawing the SVG graph. This code isn't very interesting. It's an imperative blob that creates SVG DOM elements and sets their attributes, with magic constants and offsets used here and there for alignment. This set of functions puts the dirty in quick-and-dirty. Again, the point of this example is just to write a bit more Parenscript, not to recreate d3.js or plotly.js. In the next example, I'll use plotly.js to plot the same data, for a more real-world example.

I start out with a handful of convenience macros to make creating svg elements less verbose and more "lispy." These could just as well be functions, and probably should be. But the macros are simple enough that making them macros instead of functions isn't too egregious. In fact, since I always recompile the whole Parenscript file, this removes one of the major drawbacks of macros, namely the need to recompile all macro call sites any time the macro definition changes. And since I'm only having fun with Parenscript, why not indulge in few gratuitous, zero-cost abstractions?

(defvar +svg-ns+ "http://www.w3.org/2000/svg")

(defmacro create-svg-element (tag-name)
  `(chain document (create-element-n-s +svg-ns+ ,tag-name)))

(defmacro set-attr (element attr value)
  `(chain ,element (set-attribute ,attr ,value)))

(defmacro append-child. (element child)
  `(chain ,element (append-child ,child)))

Next comes the top-level drawing function that is responsible for overall layout. It calls out to subroutines to create the various graph components. I set the svg element's viewBox attribute to give myself a scale-independent canvas on which to place elements. The browser is then free to scale the svg element to fit in the available layout and will preserve the aspect ratio defined by the viewBox.

One implementation note: Due to a bug in the implementation of reduce, I had to pass -1 rather than 0 as the init argument in the call to reduce, below. JavaScript has had Array.prototype.reduce since ECMAScript 5.1 / JS 1.8, so I could just use that instead, but it was easy to work around the bug by supplying -1 as the init value, so I stuck with the reduce provided by *ps-lisp-library*. See the section Bugs & Oddities, below, for more info on the reduce bug.

(defun draw-svg-bar-chart (svg-id table)
  "Draw a bar chart from the data in `table' on the svg element given
by `svg-id'."
  (let* ((svg-element (chain document (get-element-by-id svg-id)))
         (svg-height 100)
         (svg-width 500)
         (view-box (chain (list 0 0 svg-width svg-height) (join " ")))
         ;; Reserve h-offset pixels at the top for a title and
         ;; w-offset pixels on the left for the y-axis and label.
         (h-offset 20)
         (w-offset 50)
         (max-height (- svg-height h-offset))
         ;; Cannot use 0 as the init value since 0 is falsey in
         ;; JavaScript! See the implementation of reduce for why. Note
         ;; that reduce is defined in *ps-lisp-library*.
         (max-value (reduce (lambda (acc row)
                              (max acc (getprop row "total exports")))
                            table -1)))
    (set-attr svg-element "viewBox" view-box)
    (flet ((svg-append-all (&rest elements)
             (dolist (element elements)
               (append-child. svg-element element))))
      (svg-append-all
       ;; Y-axis goes first so grid lines are layered at the bottom.
       (make-y-axis
        :w-offset w-offset
        :width svg-width
        :height svg-height
        :tick-scale (/ max-value max-height))
       (make-plot-title
        "Top 25 US States by Agriculture Exports (2011)"
        ;; Title is centered on the x position.
        :x (+ w-offset (/ (- svg-width w-offset) 2))
        :y h-offset)
       (make-y-label
        "Millions USD"
        ;; Label is rotated 90 deg, so x and y are "swapped"
        :x (- (/ svg-height 2))
        :y h-offset)
       (make-bars
        "code" "state" "total exports" table
        :width (- svg-width w-offset)
        :height svg-height
        :value-scale (/ max-height max-value)
        :x w-offset)))))

Next, here is the source for make-plot-title. I won't list the sources for make-y-label, make-y-axis, and make-bars since they're all pretty similar; they all just create some SVG elements and twiddle their attributes. You can find them all in the file parenscript.lisp.

(defun make-plot-title
    (title &key (fill "grey") (font-size "10pt") (text-anchor "middle") (x 0) (y 0))
  (let ((plot-title (create-svg-element "text")))
    (setf (@ plot-title id) "plot-title")
    (setf (@ plot-title text-content) title)
    (setf (@ plot-title style font-size) font-size)
    (set-attr plot-title "x" x)
    (set-attr plot-title "y" y)
    (set-attr plot-title "fill" fill)
    (set-attr plot-title "text-anchor" text-anchor)
    plot-title))

Finally, here is the Promise chain to string it all together.

(chain (fetch-text "2011_us_ag_exports.csv")
       (then (curry (flip parse-csv) '("total exports")))
       (then (curry numeric-sort-by! "total exports"))
       (then (curry take 25))
       (then (curry draw-svg-bar-chart "svg-chart")))

Example 3: Fun with Plotly.js

In this last example, I'll plot the same data from example 2, this time letting plotly.js do the heavy lifting. The Parenscript code for this example will be minimal, but it will give a feel for what it's like to use a JavaScript library from Parenscript. I'll start with a bar chart, similar to the one above. To get started, I just include Plotly.js, like so:

<script src="https://cdn.plot.ly/plotly-1.36.1.min.js"></script>

Example 3.1: Plotly bar chart

As expected, the plotly bar chart is a lot nicer on the eyes than my hand-rolled SVG chart, above. And, indeed, so is the code to produce it.

;;; This macro is analogous to Parenscript's `[]' macro. That is,
;;; `{}.' is to `create' as `[]' is to `array'. Note that Parenscript
;;; already defines `{}' as a symbol-macro that expands to (create);
;;; hence, this macro has a trailing period to avoid the Parenscript
;;; compiler warning about redefining `{}'.
;;;
;;; Actually, because Parenscript's `{}' is a symbol-macro, we *could*
;;; name this macro `{}', and SBCL at least allows both to co-exist
;;; peacefully. If the `{}' appears in the function position, this
;;; macro expansion is used; if it appears anywhere else, the
;;; symbol-macro expansion is used. But the Parenscript compiler
;;; warnings are annoying, so better to just use a unique name here.
(defmacro {}. (&rest args)
  "Create nested object literals."
  `(create ,@(mapcar (lambda (arg)
                       (if (and (consp arg) (not (equal '{}. (car arg))))
                           (cons '{}. arg)
                           arg))
                     args)))

(defun extract-column (column table)
  "Return an array of the values for `column' in `table'."
  (map (lambda (row) (getprop row column)) table))

(defun draw-plotly-bar-chart (chart-id table)
  (let ((layout ({}. :title "Top 25 US States by Agriculture Exports (2011)"
                     :yaxis (:title "Millions USD")))
        (data (list (create :x (extract-column "code" table)
                            :y (extract-column "total exports" table)
                            :text (extract-column "state" table)
                            :type "bar"))))
    (chain -plotly (plot chart-id data layout))))

(chain (fetch-text "2011_us_ag_exports.csv")
       (then (curry (flip parse-csv) '("total exports")))
       (then (curry numeric-sort-by! "total exports"))
       (then (curry take 25))
       (then (curry draw-plotly-bar-chart "plotly-bar")))

Now that I've got two pieces of code producing more-or-less the same bar chart, I'm starting to see some code duplication. I was tempted to define some global variables and helper functions, so that I don't have to fetch an parse the CSV file twice, for example, or keep typing out the chart title and y-axis label every time. But for expository purposes, I think it's actually clearer with a bit of code duplication, so I'm leaving it.

See the section Bugs & Oddities, below, for more about the {}. macro.

Example 3.2: Plotly choropleth

Finally, here is a choropleth of the same data from the bar charts above, only all 50 states this time. This example comes straight out of the plotly.js docs.

(defun draw-plotly-choropleth (chart-id table)
  (let ((layout ({}. :title "US Agriculture Exports by State (2011)"
                     :geo (:scope "usa"
                                  :showlakes t
                                  :lakecolor "rgb(255,255,255)")))
        (data (list (create :type "choropleth"
                            :locationmode "USA-states"
                            :locations (extract-column "code" table)
                            :z (extract-column "total exports" table)
                            :text (extract-column "state" table)
                            :zmin 0
                            :zmax 17000
                            :colorbar ({}. :title "Millions USD"
                                           :thickness 0.2)
                            :colorscale ([] (0 "rgb(242,240,247)")
                                            (0.2 "rgb(218,218,235)")
                                            (0.4 "rgb(188,189,220)")
                                            (0.6 "rgb(158,154,200)")
                                            (0.8 "rgb(117,107,177)")
                                            (1 "rgb(84,39,143)"))
                            :marker ({}. :line (:color "rgb(255,255,255)"
                                                       :width 2))))))
    (chain -plotly (plot chart-id data layout ({}. :show-link false)))))

(chain (fetch-text "2011_us_ag_exports.csv")
       (then (curry (flip parse-csv) '("total exports")))
       (then (curry draw-plotly-choropleth "plotly-choropleth")))

As you can see, calling third-party JavaScript libraries from Parenscript is frictionless and requires zero boilerplate.

Bugs & Oddities

In this section, I note small bugs, rough edges, and implementation details that caught my attention. Some of these are things I might consider filing an issue or opening a pull request for on github. Others aren't really bugs, just things I found interesting.

reduce sometimes ignores its init argument

Update(May 2019): this now fixed.

As I noted above, there appears to be a bug in the implementation of reduce in *ps-lisp-library* which ignores its init argument whenever the caller passes something that evaluates as false in JavaScript. The init argument is sometimes ignored because reduce contains an expression to initialize the accumulator that compiles down to the following JavaScript:

acc = init ? init : list[0]

So, if the caller supplies an init value that JavaScript considers false – like 0, "", or false to name a few – then then acc will get initialized to the first list item, rather than init. That is, the conditional should test whether init is undefined explicitly, rather than any false value.

Here are some examples from the browser's JS console demonstrating the bug:

reduce((acc, e) => acc * e, [1,2,3,4,5], 0)           /* expect 0 */
--> 120
reduce((acc, e) => acc && e, [true,true,true], false) /* expect false */
--> true
reduce((acc,e) => acc, ["foo", "bar"], "")            /* expect "" */
--> "foo"

let ignores garbage in binding forms

Parenscript's implementation of let ignores extra garbage at the end of each binding form. For example:

;; Parenscript         /* JavaScript */
(let ((x 4 (/ 0 0)))   (function () {
  x)                      var x = 4;
                          return x;
                       })();

For comparison, SBCL, CCL, and CLISP all signals errors complaining that the let binding is malformed. Of course, it's not really fair to compare Parenscript to production-grade Common Lisp implementations, I just happened to notice it when reading over Parenscript's let implementation. Besides, who's to say this is a bug and not a feature!

ps::encode-js-identifier deprecation warnings

Update(May 2019): this is now fixed.

The implementation of ps::encode-js-identifier in utils.lisp issues a warning if the user creates an identifier with an embedded dot (.) or sqaure brackets ([]), which is a useful shorthand for referencing object properties or indexing into arrays (and was presumably supported in older versions of Parenscript). For example, foo.bar or foobar[3]. I believe the current recommendation is to use the @ macro instead, like so: (@ foo bar) or (@ foobar 3). However, ps::encode-js-identifier will only issue the warning about embedded dot and/or square brackets if certain other special characters are present in the identifier name which cause Parenscript name mangling rules to kick in. I assume this is a bug, and the intention is to always warn about embedded dot/bracket characters, but I'm not sure.

An example might help clarify. In the following SLIME-REPL session, I'd expect "foo.bar" and "foobar[3]" to also generate warnings.

FUN-WITH-PS> (ps::encode-js-identifier "foo.bar")
"FOO.BAR"
FUN-WITH-PS> (ps::encode-js-identifier "foo.bar?")
WARNING:
   Symbol foo.bar? contains one of '.[]' - this compound naming convention is no longer supported by Parenscript!
"foo.barwhat"
FUN-WITH-PS> (ps::encode-js-identifier "foobar[3]")
"FOOBAR[3]"
FUN-WITH-PS> (ps::encode-js-identifier "foo-bar[3]")
WARNING:
   Symbol foo-bar[3] contains one of '.[]' - this compound naming convention is no longer supported by Parenscript!
"fooBar[3]"

Here is the relevant portion of ps::encode-js-identifier.

(let ((cache (make-hash-table :test 'equal)))
  (defun encode-js-identifier (identifier)
    "Given a string, produces to a valid JavaScript identifier by
following transformation heuristics case conversion. For example,
paren-script becomes parenScript, *some-global* becomes SOMEGLOBAL."
    (or (gethash identifier cache)
        (setf (gethash identifier cache)
              (cond ((some (lambda (c) (find c "-*+!?#@%/=:<>^")) identifier)
                     (let ((lowercase t)
                           (all-uppercase nil))
                       (when (and (not (string= identifier "[]")) ;; HACK
                                  (find-if (lambda (x) (find x '(#\. #\[ #\]))) identifier))
                         (warn "Symbol ~A contains one of '.[]' - this compound naming convention is no longer supported by Parenscript!"
                               identifier))
                         ...

A couple of things to note:

  • the cond test ensures that identifier contains at least one of the characters -*+!?#@%/=:<>^
    • therefore, the (string= identifier "[]") test can never be true
    • therefore, the warning about an identifier containing a period and/or square brackets can only be triggered if it also contains one of the characters -*+!?#@%/=:<>^

Array argument punning

This one is kind of fun. It turns out that if you want to pass an array literal as an argument of a function, you can use an almost identical syntax to JavaScript arrays, just without the commas. For example:

;; Parenscript             /* JavaScript */
(some-fn [a b 1 2])        someFn([a, b, 1, 2]);

This isn't documented in the Parenscript reference. I think what's going on here is that Parenscript thinks it's compiling a function call with four separate arguments, where the first argument happens to be named [a and the last happens to be 2]. We can see this more clearly if we place spaces around the square brackets, in which case we get:

;; Parenscript             /* JavaScript */
(some-fn [ a b 1 2 ])      someFn([, a, b, 1, 2, ]);

Now Parenscript compiles it as a six-argument function where the first argument is [ and the last is ].

This hack works for nested arrays as well:

;; Parenscript               /* JavaScript */
(some-fn [[a b] 2 [c d] e])  someFn([[a, b], 2, [c, d], e]);
(some-fn [[a [b [c]]]]))     someFn([[a, [b, [c]]]]);

This quirk isn't documented in the Parenscript reference, so I assume it's a kind of "happy accident" since [a and 2] aren't actually valid JavaScript identifiers. But maybe it's intentional? Who knows? In any case, you should probably just use the [] macro (see the next section) for creating nested arrays.

[] vs {}

There are a couple of little asymmetries with respect to [] and {} that caught me off guard, summarized in the following examples:

;; Parenscript               /* JavaScript */
(defvar *my-array* [])       var MYARRAY = [];
(defvar *also-array* '[])    var ALSOARRAY = [];

(defvar *my-object* {})      var MYOBJECT = {  };
(defvar *my-string* '{})     var MYSTRING = '{}';

([] :make (nested []))       ['make', [nested, []]];
({} :not (like []))          {}('not', like([]));

Both [] and '[] in Parenscript are compiled to the empty JavaScript array literal: []. So you might expect {} and '{} to behave similarly. While Parenscript does compile {} to the empty JavaScript object literal { }, the quoted form '{} is instead compiled down to a string '{}'. This is because Parenscript special-cases '[] to expand to (array), whereas there is no special handling for '{}. Here are the relevant bits from Parenscript's quote in special-operators.lisp:

(define-expression-operator quote (x)
  (flet ((quote% (expr) (when expr `',expr)))
    (compile-expression
     (typecase x
       ...
       ((or null (eql [])) '(array))
       ...
       (symbol (symbol-to-js-string x))
       ... ))))

The unquoted forms [] and {} are also handled differently by the Parenscript compiler, though both compile down to the corresponding JavaScript literal syntax. Parenscript simply defines {} as a symbol-macro that expands to (create), whereas [] is treated as an identifier, as far as I can tell. Of course, [] isn't a valid JavaScript identifier, and treating it like one allows for nonsense like:

;; Parenscript          /* JavaScript */
(let (([] 'huh))        (function () {
   [])                     var [] = 'huh';
                           return [];
                        })();

(defvar [] 'whoops)     var [] = 'whoops';

Thanks to ES6's destructuring assignment, neither of these causes a JavaScript SyntaxError in modern browsers; the assignment is just ignored, as far as I can tell. Replacing [] with {} in the above two examples results in the following:

;; Parenscript          /* JavaScript */
(let (({} 'huh))        (function () {
   {})                     var {}1 = 'huh';
                           return {}1;
                        })();
                        /* JavaScript error:
                             Uncaught SyntaxError: Missing initializer in destructuring declaration */

;; Does not compile.
;; Lisp error:
;;   The value
;;     (CREATE)
;;   is not of type
;;     SYMBOL
;;   [Condition of type TYPE-ERROR]
(defvar {} 'whoops)

Note the trailing 1 in the generated JavaScript when let-binding {}. This is due to Parenscript's maybe-rename-lexical-var, which finds {} in *symbol-macro-env*, and replaces it with a gensym.

(defun maybe-rename-lexical-var (x symbols-in-bindings)
  (when (or (member x *enclosing-lexicals*)
            (member x *enclosing-function-arguments*)
            (when (boundp '*used-up-names*)
              (member x *used-up-names*))
            (lookup-macro-def x *symbol-macro-env*)
            (member x symbols-in-bindings))
    (ps-gensym (symbol-name x))))

Defining a symbol-macro a-la {} seems like a cleaner solution, so presumably there is some reason why [] is handled differently. Possibly because Parenscript defines a macro named []? Is it "safe" (i.e. portable) to define both a macro and symbol-macro with same name in Common Lisp? None of SBCL, CCL, or CLISP complain if I do. A quick skim of the relevant sections in the HyperSpec doesn't seem to explicitly rule it out, and indeed this seems analogous to defining a function and a variable with the same name, which is fair game in Common Lisp.

The final asymmetry I'll note between [] and {} is that while Parenscript defines a macro named [] to help with creating nested arrays, there is no corresponding {} macro to create nested object literals. That is, while the form ([] 1 (2 foo) 3) compiles to [1, [2, foo], 3], the form ({} :a (:b c)) compiles to {}('a', 'b'(c)), not { 'a': { 'b': c } }, as you might hope. Of course, it's possible to define your own {} macro for creating nested objects, and that's just what I did in Example 3.1, above.

Takeaways

Despite a few small bugs and rough edges, Parenscript delivers on its promise of providing a Common Lisp-ish syntax for writing JavaScript, complete with the full power of macros for your JavaScripting enjoyment. Alas, it's only subset of Common Lisp, so it feels more like writing JavaScript with a lispy syntax than it feels like writing Common Lisp, but that's still pretty cool! I would consider using Parenscript again for small side-projects with traditional server-side-rendered web interfaces where I was already using Common Lisp on the server-side. I probably wouldn't try to use Parenscript for creating a snazzy, user-interaction-heavy SPA / PWA with React. Then again, as you can probably tell from this website, I wouldn't try creating such a webapp at all!

In keeping with the stream-of-consciousness nature of this post, I'll now ramble off a list of Parenscript pros and cons.

Pros

  • Macros
  • Trivial to call third-party JavaScript libraries from Parenscript without requiring any boilerplate.
  • Parenscript generates very readable JavaScript, with no runtime dependencies. Most of the time, you can guess more-or-less what JavaScript will be produced for a given Parenscript expression.
  • Parenscript implementation is relatively small and easy to understand. Only ~4k total lines of code, of which ~700 lines are in a file that just exports a bunch of JavaScript symbols, and another ~300 lines are in Parenscript's implementation of the loop macro. So ~3k lines of very readable Common Lisp for the meat of the transpiler.
  • For users who already know and love Common Lisp but don't know much JavaScipt, Parenscript allows you to stay in the comfort zone and avoid JavaScript fatigue.
  • Development moves slowly, so stuff rarely breaks.

Cons

  • Parenscript doesn't have cons! (that pun was too good to pass up).
  • Still no support for cool ES6+ stuff like async/await, generators, etc.
  • Though doable, doesn't cater to users who aren't using Common Lisp on the server.
  • Parenscript is a not-super-popular open-source project, with all the pitfalls that usually entails.
  • Development moves slowly, so stuff rarely gets fixed.

Created: 2018-05-04

Last modified: 2024-04-11