I like programming in Common Lisp. The interactive development style is a nice experience. No matter how painful it is to have moving pieces all the time, the programming loop is much nicer. I know that programming is getting old-fashioned with all the AI assisting tools, so I try to enjoy it while it lasts.

ParenScript is a lisp flavored JavaScript, thus brings all the pains of JavaScript to the joy of lisp. It is a masochist endeavor to be honest. ClojureScript feels better, because you can use more of Clojure on it. But that is only because there is more supporting code hiding the JavaScript. I wanted to then improve ParenScript at that level, because on my last endeavor it felt hopeless to work with it.

What do I need?

I need some utility functions and macros that help me develop a web front-end. I still don’t know how to package it independently so for the moment it is everything in one name-space inside my front-end toy project. That is the package ps-prelude.

In a lisp file I declare that package.

1(defpackage ps-prelude
2  (:use :cl :parenscript))
3(in-package :ps-prelude)
4(setf *js-target-version* "1.9")

Some syntax improvements

For the sake of readability on the generated code. I want to have the for of syntax, which was not to hard to get working by extending ParenScript.

1(ps::define-statement-operator for-of ((var iterable) &rest body)
2  `(ps-js::for-of ,(ps::compile-expression var)
3     ,(ps::compile-expression iterable)
4     ,(ps::compile-loop-body (list var) body)))
5
6(ps::defprinter ps-js::for-of (var object body-block)
7                "for (const "(ps::ps-print var)" of "(ps::ps-print object)") "
8                (ps::ps-print body-block))

The spread operator/syntax is a nice to have in JavaScript, when I do lisp I prefer apply and concatenate. Yet, because I can, here it is.

1(ps::define-expression-operator spread (object)
2  `(ps-js::spread ,(ps::compile-expression object)))
3
4(ps::defprinter ps-js::spread (object)
5                "..." (ps::ps-print object))

The threading macros are a must have, they make reading lisp a lot easier as you can see the data flow in sequence. I simply imitate the macros from the arrows package. Here, I only copied the code but using the defpsmacro. It is so awesome, that I can use functions which are not part of the exported API by using the double colon, lisp has unrivaled extensibility.

 1(defpsmacro -> (initial-form &rest forms)
 2  (arrows::expand-arrow initial-form forms #'arrows::insert-first))
 3
 4(defpsmacro ->> (initial-form &rest forms)
 5  (arrows::expand-arrow initial-form forms #'arrows::insert-last))
 6
 7(defpsmacro -<> (initial-form &rest forms)
 8  (reduce (arrows::diamond-inserter #'arrows::insert-first)
 9          forms
10          :initial-value initial-form))

Lastly, to write tests one last macro. It takes the lisp form, prints it to a string and then sends it to should-print which is a function on the JavaScript side. Here, you see the true power of lisp being able to work on its self. This step takes place in lisp, before ParenScript then transforms it to JavaScript.

1(defpsmacro should (form)
2  (let ((text-form (format nil "~s" form)))
3    `(should-print ,form ,text-form)))

On the ParenScript side, which becomes JavaScript I define.

 1(in-package :ps-prelude)
 2
 3(defvar *__PS_MV_REG*) ;; Work around parenscript multiple value return
 4
 5(defun should-print (test text-form)
 6  (if test
 7      (list "i.fas.fa-check.text-green-500")
 8      (list "div.bg-red-200.border.p-1"
 9            (list "i.fas.fa-xmark.text-red-600.pr-2")
10            "Failure! " text-form)))

should-print has many implementation details. Later, I’ll explain my cl-who analogous system to create DOM elements. Only understand that if the test which is a ParenScript form is true, then I’ll create a green FontAwesome check-mark and when it fails it’ll tell you inside a red box and show you which form failed.

Working on the DOM

Next are functions on the ParenScript side, all inside the :ps-prelude name-space, which turns into the JavaScript file.

First, an extra test for a JavaScript object. This function is super useful, as almost everything is an object, yet I want to narrow it down to the key-value pair object.

1(defun is-object (value)
2  (and (not (= value nil))
3       (objectp value)
4       (not (arrayp value))
5       (not (instanceof value -element))))

Logging to the console is a debugging necessity, this utility function helps a lot as I can wrap any value with it to see it on the console without needing to refactor the code. The trick is: it returns the value it printed first.

1(defun logger (level obj &rest objects)
2  (apply (getprop console level) obj objects)
3  obj)

Next function helps create an DOM Element from a string defined by the tag-re syntax. Those are the strings you saw previously on the should-print function. It converts the string div#app.text-red.margin-auto into a div Element, with id = "app" and a class = "text-red margin-auto".

 1(defparameter tag-re (regex "([^\\s\.#]+)(?:#([^\\s\.#]+))?(?:\.([^\\s#]+))?"))
 2
 3(defun dom-tag (info-tag)
 4  (destructuring-bind (_ tag id class-name) ((@ info-tag match) tag-re)
 5    (declare (ignore _))
 6    (let ((el (chain document (create-element tag))))
 7      (when id (setf (@ el id) id))
 8      (when class-name
 9        (setf (@ el class-name)
10              ((@ class-name replace-all) "." " ")))
11      el)))

The last functions are my take on a cl-who analogous function system. dom-el takes nested list representing in a lisp(data driven) way the DOM Elements. In those lists, the first entry describes the Element with id and class. If the second entry is an key-value pair object, that defines the properties/attributes of the Element. All other entries in the list are the content which can be other Elements, because the function is recursive.

 1(defun attr-trans (key)
 2  (case key
 3    ("class" "className")
 4    ("for" "htmlFor")
 5    (t key)))
 6
 7(defun fill-attrs (el attrs)
 8  (when (is-object attrs)
 9    (for-in (key attrs)
10      (cond ((chain key (starts-with "data-"))
11             (chain el (set-attribute key (getprop attrs key))))
12            ((string= key "style")
13             (chain -object
14                    (assign (getprop el key)
15                            (getprop attrs key))))
16            (t (setf (getprop el (attr-trans key)) (getprop attrs key)))))
17    t))
18
19(defun dom-el (&rest params)
20  (if (arrayp (aref params 0))
21      (apply dom-el (aref params 0))
22      (let* ((el (dom-tag (aref params 0)))
23             (contents
24               ((chain params slice)
25                (if (fill-attrs el (aref params 1)) 2 1))))
26
27        (for-of (arg contents)
28          (cond ((arrayp arg)
29                 ((@ el append-child) (dom-el arg)))
30                ((instanceof arg -element)
31                 ((@ el append-child) arg))
32                ((and arg (stringp arg))
33                 ((@ el append-child)
34                  ((@ document create-text-node) arg)))))
35        el)))

Last tooling steps

One of the many components to the joy of lisp is interactive development. You need a way to evaluate you code interactively and in this case it means evaluating the ParenScript code into the browser.

The project trident-mode.el solves that problem. However, it hasn’t received any updates in a long time. It is designed to work with SLIME and since I am a doom-emacs user, I use sly for lisp development. For that reason, I had to patch that package for my developer setup. You can find my fork at https://git.oscarnajera.com/pub/hi/trident-mode.el

My fork only supports sly on the other hand, I didn’t not have the patience to and test environment to try to make it work over both packages.

A second problem is when you evaluate code interactively, sly or slynk for that matter truncates the output. That is reasonable for display, but when you want to then evaluate the output as it is the ParenScript output, it can’t be truncated. To solve that issue you can evaluate the following in your repl during development.

1(pushnew '(SLYNK:*STRING-ELISION-LENGTH* . nil) slynk:*slynk-pprint-bindings* :test #'equal)

What can you do with it?

It turns out already a lot. It is much more ergonomic to program with this utilities than without as in my last attempt. Let me give you a last example of what the DOM rendering and test suite looks like.

This would be the more functions in our utilities toolbox we want to have tests for.

 1(defun to-query (config-object)
 2  (let ((params (new (-U-R-L-search-params config-object))))
 3    (chain params (to-string))))
 4
 5(defun zip-to-object (data)
 6  "DATA is two rows, being key to value. Zip them into object."
 7  ((chain -object from-entries)
 8   ((chain (aref data 0) map)
 9    (lambda (name idx) (list name (aref data 1 idx))))))
10
11(defun deep-equal (a b)
12  (let ((keys (chain -object keys)))
13    (cond ((and a b (arrayp a) (arrayp b))
14           (and
15            (=
16             (chain a length)
17             (chain b length))
18            ((chain a every)
19             (lambda (val idx) (deep-equal val (aref b idx))))))
20          ((and a b (objectp a) (objectp b))
21           (and
22            (=
23             (chain (funcall keys a) length)
24             (chain (funcall keys b) length))
25            ((chain (funcall keys a) every)
26             (lambda (key) (deep-equal (getprop a key) (getprop b key))))))
27          (t (eql a b)))))

An this would run the test suite in this very webpage. I included some failing tests on purpose. The result follows immediately. To read the ParenScript generated code, you’ll have to look into the webpage source.

 1(defun test-suite ()
 2  (list "div" (list "h2.text-3xl" "Test of system")
 3        (list "div" "Element generator: "
 4              (should (deep-equal (list "5" "hi") (list "5" "hi")))
 5              (should (deep-equal (list "5" "hi") (list "6" "hi")))
 6              (should (deep-equal ((@ "hi#mi.no.yes-please" match) tag-re)
 7                                  (list "hi#mi.no.yes-please" "hi" "mi" "no.yes-please")))
 8              (should (string=
 9                       (chain (dom-tag "hi#mi.no.pu-si" ) outer-h-t-m-l)
10                       "<hi id=\"mi\" class=\"no pu-si\"></hi>"))
11              (should (string=
12                       (chain (dom-tag "hi#mi.no.si" ) outer-h-t-m-l)
13                       "<hi id=\"mi\" class=\"no yes\"></hi>")))
14        (list "div" "Utilities: "
15              (should (string= (to-query (create :A "he" 'li "bus"))
16                               "a=he&li=bus"))
17              (should (deep-equal
18                       (zip-to-object '((a b) (c d)))
19                       (create a 'c b 'd)))
20              (should (deep-equal (list 5 2 5) (list 5 2 5))))))
21
22(add-event-listener
23 "load"
24 (lambda (event)
25   (let ((el (chain document (query-selector "#demo-test"))))
26     ((@ el append-child) (dom-el (test-suite))))))

I think this is awesome, and already motivates myself to keep trying to get ParenScript to an usable and enjoyable state.