Building with Parenscript and Preact
I read an article about Parenscript a while back, and the way he described it makes it seem downright romantic.
Helpful? Olaf is clearly a master of understatement. Try beautiful. [...] It contains syntactic sweeteners for React classes, methods, properties, and most important, DOM tree generation. Olaf had thereby done away with React's grossest feature, the JSX minilanguage, in about 10 lines of code. [...] It will not only educate but inspire you.
Damn... I guess I gotta try this out.
I have a thing for tools that are off the beaten path but are super powerful. I call them "secret weapons" since everyone sees "Lisp" or "Pascal" or whatever and immediately turns their brain off while you're doubling your productivity. I figured it might be fun to put something together with Parenscript to see what the fuss is all about. I saw an article from Holger Sindbaek about how he's making 10k a month off a Solitaire game, so I figured I might try to get in on some of that action; plus I have fond memories as a kid of opening FreeCell and watching the king's head turn left and right to follow the mouse.
The Stack
I decided from the start that I wanted Lisp-style interactive development. Parenscript itself is (basically) just a function that takes code and spits out a JS string, so there's not really a development environment component to it. I found trident-mode which is a pretty barebones tool that makes the HTML page long-poll Emacs for JS strings and then evals them. I wanted to do something similar to React's "fast refresh" which swaps out components while preserving state. Basically the frontend equivalent of "C-M-x'ing that sexp" and having your running program automatically update. Every time I'm in a hot-reloading environment like this I like to pretend I'm Andy Gavin with my own PS2 toolchain.
Since I want to side-step the modern frontend stack a bit, I found Preact as an alternative implementation of React in 3kb, and it has a blog post describing how to hook into its hot updating system. I quickly whipped up a macro which implements a JSX-like syntax, and a defcomponent
macro for creating React components, so I can write something like this:
(defcomponent -counter (((counter set-counter) (use-state 0))) ()
(psx
(:div ()
"The button has been clicked "
(:span ((style (create "color" "red")))
(chain counter (to-string)))
" times."
(:button ((onclick (lambda () (set-counter (1+ counter)))))
"Increment"))))
and have it compile into:
var s = $RefreshSig$();
function Counter() {
s();
var _db1107 = preactHooks.useState(0);
var counter = _db1107[0];
var setCounter = _db1107[1];
__PS_MV_REG = [];
return [preact.h('div', Object.assign({ }), ['The button has been clicked '], [preact.h('span', Object.assign({ style : { 'color' : 'red' } }), [counter.toString()])], [' times.'], [preact.h('button', Object.assign({ onclick : function () {
__PS_MV_REG = [];
return setCounter(counter + 1);
} }), ['Increment'])])];
};
s(Counter, 'useState{[counter, setCounter](0)}', false, function () {
return [];
});
$RefreshReg$(Counter, 'Counter');
flushUpdates();
When you execute that chunk of code, it (re)defines a component, interfaces with Preact's prefresh package and then flushes updates, effectively performing a hot replacement of the component.
And it's true, it only took about 50 lines of macro code to implement. Combine that with trident-mode to push the snippets to the frontend for evaluation, and you have a full fledged interactive hot reloading environment! I don't want to get all Reddit about it, but:
Once I had this in hand, I got to work implementing the actual card game.
Implementation
I implemented draggable and droppable cards using GSAP (since that's what Holger uses), wrapped in use-effect
. It's a pretty lightweight way to animate and tween everything which is really cool. Since the cards are all positioned by this system, I can tweak values in the editor and they all animate into their new places, which is fun and just feels powerful for some reason.
I ended up relying on packing information into the HTML id
property, since it provides a convenient way to point to a specific card ("1_heart" is the ace of hearts) and perform some side-effectful action on it without getting into ref hell. I'm not sure if this is the best practice but it seems to work ok.
As far as implementing the game itself, the logic is all pretty straightforward to write, although I had to crack open the Windows 95 VM a couple times to compare the behavior to the classic FreeCell. My thing even uses the same random number generator as the Windows one (courtsey of Rosetta Code), so the game numbers are the same between the two.
Once the game was working properly, I wanted to include a win screen kind of like the original Solitaire which had the bouncing trailing cards. Since I had GSAP at my disposal to do all sorts of transforms, I made it so the cards get into big sine waves on the top and bottom of the page, and a "You win!" pops up in the middle. I think it looks pretty cool, and only took a few lines of code.
I will say this about React, the restrictions it puts on you are annoying, but once you've wired up the animations and stuff properly, everything just kind of magically works. I added an undo/redo feature which keeps track of the previous state whenever you perform a set-board-state
and the cards perfectly float into place and the columns perfectly expand/contract according to my rules... very nice. You can even undo out of the win screen and it all reverts back properly, and back in again if you redo.
I grabbed Adrian Kennard's card SVGs which are public domain, which is very generous of him. I want to maybe touch up the number and letter fonts on the cards a bit myself but I'll do it later.
The last thing I had to do was plug my Parenscript stuff into the build system so it could "link" it all together in a minified package. It was pretty trivial to write a Vite plugin to process importing `.paren` files, and then at that point I could simply netlify build; netlify deploy --prod
to push the whole thing up to Netlify.
The game worked, but there was occasionally a small delay of a second or two before the JS started running when nothing appeared on the page; from what I understand, the usual way to solve that these days is by doing server-side React rendering and then re-hydrating it on the frontend once your code starts executing.
I found a link to this in the Vite docs, which is a pretty rudimentary way to do pre-rendering using Vue-- it basically renders the Vue stuff to a string, and then does a find-and-replace on a special comment in HTML sources after the build finished. Seems pretty hacky to me, but it seems to be the way things are done, so I whipped up my own version which does something similar, albeit in a more grotesquely ugly way.
And that's pretty much it! Reactive layout, smooth animation, incremental Lisp-style development, and pre-rendering using just Parenscript, GSAP and Preact, all about 50kb gzipped.
The Good
I loved the flexibility of being able to include new syntax when it was appropriate. The game state is represented using an object then when I need to update, I perform a structured-clone
on and then pipe through various modification functions, and then into set-board-state
:
(-> (board-state)
(delete-from-column col)
(set-freecell num card)
(auto-foundation)
(set-board-state))
Another thing that came in pretty handy (with caveats I'll mention later) is Parenscript's implementation of loop
. I always find myself in other languages wishing I had CL's loop
; It just blows everything else out of the water, it is so supremely useful. If I ever make my own dream language that's a mashup of everything I like from other languages, that's definitely going in there. DCGs from Prolog as well, every time I have to write some code that vaguely resembles parsing or decoding, I wish I was in Prolog... but I digress.
Lyn mentions it too in his article but Paredit makes things massively easier to work with. I always thought JS gets pretty ugly real quick with all the braces and ellipsis and arrows piling up.
The Bad
I've used React for a couple quickie projects before but never really had to get down and dirty with it. Once I started making some non-trivial stuff I immediately started running into limitations and weird shit.
For one, a component will always remount if it changes position in the DOM. If your component is storing state inside itself, this means that if for whatever reason it changes location in the DOM, that state is just thrown away. And that really sucks, because I want my deck of 52 cards to be draggable, droppable with animation, and some other things that they need to store state for, so my "solution" was to simply position each card absolutely, and deal with the layout myself. It's actually not too big of a deal since card cascades are pretty simple, but still.
React (and Preact) compare arrays shallowly when in the dependencies array of use-effect
. This means that if you pass an array as a prop and then give it to use-effect
it will simply re-run the effect every single time... apparently the go-to workaround is to pass JSON.stringify(the_array)
to the list instead. That's the kind of bizarre JS stuff I wish I could escape from, which leads me to my next section:
The Ugly
Its bad! You can't say (apply #'+ (list 1 2 3)). Its the thinnest, most brittle veneer on top of JS and it makes all sorts of implications that it can't keep
—commonslip
Parenscript is fundamentally flawed. It's a super-lightweight papering-over of JavaScript, and you become aware of that harsh reality immediately. It makes an attempt to look like the surface syntax of Common Lisp while maintaining none of its semantics, so you kind of have to juggle in your mind the JavaScript you want to get, the Parenscript you have to write, and the Common Lisp that would do something else if executed as CL. It's especially odd since JS has no cons cells, and Parenscript doesn't try to provide them-- (cons 1 nil)
just becomes cons(1, null);
. So it looks superficially a lot like CL, but isn't like it semantically at all. On top of that, Parenscript has a few bugs: it will just miscompile things sometimes, specifically its loop
construct, and it has a bad habit of not declaring variables when you use let
, which doesn't work in "use strict"
mode.
Conclusion
A few disorganized thoughts:
- One thing I've always thought about macros in Common Lisp is that you often don't need them because the language is already so rich on its own. A lot of the times I find myself wanting to use a macro is when I'm using other languages that don't have them. In this case, worlds collide since the actual implementation language is pretty spartan, but it can be extended with the full power of Lisp. Which means that usually when I'd wish for macros I'd sigh wistfully and start copy-pasting, but now I can actually have them. It's pretty cool, and combined with JavaScript's dynamic nature, and my home-grown interactive development environment, it does feel kinda like you're writing Lisp... until the illusion is inevitably broken by semantic differences. But I actually do like it a lot anyway, it's a lot more fun than writing straight JS, that's for sure.
- The JavaScript ecosystem rightfully gets a lot of flack for having insane amounts of complexity and churn; new frameworks, new compiler passes, new sugars on top of JavaScript to make it usable (TypeScript), it's a mess. In some ways I see the Common Lisp ecosystem as being nearly the exact opposite. Libraries instead of frameworks, that sit undisturbed on GitHub for years but still usable. Despite the fact that the language was specified in the 90s and hasn't evolved since then, it still feels extremely fresh because it's so moldable and dynamic. One of my goals with this project was to approach frontend development with the Lisp mentality: carefully choose simple building blocks like Preact and trident-mode, understand them, and combine them in straightforward ways.
- Although I do appreciate the effort behind Parenscript, it really is more of a syntactical wrapper than a new language. I'm interested in JSCL, which aims to properly support Common Lisp on top of JS. One dream project I have is to build a dynamic game engine on something like that that will run in browser and allow for Jak and Daxter or Minecraft style hot-swapping at runtime. This demo blew my mind recently and it's exactly the kind of thing I want to make. I played with ECL a while back which can compile to Emscripten, and got very close to getting SDL running in it, but it wasn't quite ready at the time.
Check out the game here: https://online-freecell.com
Source code is all here: https://github.com/SuperDisk/cardgames
By the way, I'm looking for a job, so if you or anyone you know is hiring and needs a really good developer, please let me know ;)