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.
- It's Common Lisp, so there's that.
- You can share Parenscript code between the client and server, if you're running Common Lisp on the server.
- No runtime dependencies. Compiles down to vanilla JS (version 1.3
by default, but you can pick your poison with
*js-target-version*
). - There is no need for boilerplate or type definitions if you want to call JavaScript code.
- 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-catThis 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: ¯\_(ツ)_/¯
.
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 thatidentifier
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
-*+!?#@%/=:<>^
- therefore, the
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.