Building with Parenscript and Preact: Difference between revisions

no edit summary
No edit summary
No edit summary
 
(8 intermediate revisions by the same user not shown)
Line 7: Line 7:
Damn... I guess I gotta try this out.
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.
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 [https://www.indiehackers.com/post/how-i-grew-a-simple-solitaire-game-to-10k-mrr-28e352c308 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.


[[File:King2.apng]]
[[File:King2.apng]]
Line 13: Line 13:
= The Stack =
= 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 [https://github.com/johnmastro/trident-mode.el 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 [https://nickfa.ro/w/images/8qgw3f.jpg "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 [http://www.codersnotes.com/notes/disassembling-jak/ with my own PS2 toolchain].
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 [https://github.com/johnmastro/trident-mode.el 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 [https://nickfa.ro/w/images/8qgw3f.jpg "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 [http://www.codersnotes.com/notes/disassembling-jak/ 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 [https://dev.to/jovidecroock/prefresh-fast-refresh-for-preact-26kg 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 <code>defcomponent</code> macro for creating React components, so I can write something like this:
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 [https://dev.to/jovidecroock/prefresh-fast-refresh-for-preact-26kg 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 <code>defcomponent</code> macro for creating React components, so I can write something like this:


<syntaxhighlight lang="lisp">(defcomponent -ui-button () (props)
<syntaxhighlight lang="lisp">
  (with-defaults (on-click (disabled false) children) props
(defcomponent -counter (((counter set-counter) (use-state 0))) ()
    (psx
  (psx
    (:div ((on-click on-click)
  (:div ()
            (class-name (class-names "ui-button" (when disabled "disabled")))
        "The button has been clicked "
            (style (create "color"
        (:span ((style (create "color" "red")))
                          (if disabled
                (chain counter (to-string)))
                            "grey"
        " times."
                            "black"))))
        (:button ((onclick (lambda () (set-counter (1+ counter)))))
          children))))</syntaxhighlight>
                  "Increment"))))
</syntaxhighlight>
 
[[File:Clicked.png]]


and have it compile into:
and have it compile into:
Line 32: Line 35:
<syntaxhighlight lang="javascript">
<syntaxhighlight lang="javascript">
var s = $RefreshSig$();
var s = $RefreshSig$();
function UiButton(props) {
function Counter() {
     s();
     s();
     var disabled304 = 'undefined' === typeof props.disabled ? false : props.disabled;
     var _db1107 = preactHooks.useState(0);
    var counter = _db1107[0];
    var setCounter = _db1107[1];
     __PS_MV_REG = [];
     __PS_MV_REG = [];
     return [preact.h('div', Object.assign({ onClick : props.onClick,
     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 () {
                                            className : classNames('ui-button', disabled304 ? 'disabled' : null),
        __PS_MV_REG = [];
                                            style : { 'color' : disabled304 ? 'grey' : 'black' }
        return setCounter(counter + 1);
                                          }), [props.children])];
    } }), ['Increment'])])];
};
};
s(UiButton, '', false, function () {
s(Counter, 'useState{[counter, setCounter](0)}', false, function () {
     return [];
     return [];
});
});
$RefreshReg$(UiButton, 'UiButton');
$RefreshReg$(Counter, 'Counter');
flushUpdates();
flushUpdates();
</syntaxhighlight>
</syntaxhighlight>
Line 52: Line 57:
[[File:Lisp-preact.mp4|1000px]]
[[File:Lisp-preact.mp4|1000px]]


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:
And it's true, it only took about [https://github.com/SuperDisk/cardgames/blob/dbf74fc105fde68cb4962a8b7ff7dcfb231db34f/preact.lisp#L53 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:


[[File:8q5nqk.jpg]]
[[File:8q5nqk.jpg]]
Line 115: Line 120:
</blockquote>
</blockquote>


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-- <code>(cons 1 nil)</code> just becomes <code>cons(1, null);</code>. 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 <code>loop</code> construct, and it has a bad habit of not declaring variables when you use <code>let</code>, which doesn't work in <code>use strict"</code> mode.
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-- <code>(cons 1 nil)</code> just becomes <code>cons(1, null);</code>. 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 <code>loop</code> construct, and it has a bad habit of not declaring variables when you use <code>let</code>, which doesn't work in <code>"use strict"</code> mode.


= Conclusion =
= Conclusion =


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.
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 [https://github.com/jscl-project/jscl 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. [https://www.youtube.com/watch?v=72y2EC5fkcE 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, [mailto:yux50000@hotmail.com please let me know ;)]