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.
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")
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.
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)))
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)
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.