Aug. 13, 2024

Claude AI has a mode where it can generate something called "artifacts". One of the things you can do with this is generate simple single page web applications. It generates the web app and then mounts it in an iframe so you can quickly test it and give feedback. This gives you a fast iterative process using the AI to refine the web app incrementally.

This is pretty cool but I would much prefer a web app written in ClojureScript with Reagent forms instead of JavaScript or React. ClojureScript is more concise and I find it leads to less bugs and is faster to work with.

Note: this post is available as a YouTube video.

Claude AI app artifact generation

There is a version of ClojureScript that runs entirely in the browser called Scittle, created by Michiel Borkent who also created the babashka suite of Clojure utilities. Unfortunately Claude can only use libraries that are available on cdnjs when generating web apps, and Scittle was not available. So I raised a PR with cdnjs and it's now available to use in Claude generated artifacts.

What all this means is you can now prompt Claude to generate small ClojureScript apps and it will generate clean ClojureScript code. I've set up a basic repository with a ClojureScript + Reagent prompt and example HTML file you can give to Claude to get it started.

The best way to use this repository is to create a new project in Claude, and copy the prompt and the example in as the default project prompt. A project is Claude's way of letting you use the same prompt multiple times.

As a simple example, you can generate a basic compound interest calculator using the prompt from the repo and adding the following text to the end:

Please generate a simple compound interest calculator.

You can test the resulting app and see the code here. The code it produces is a single page HTML app with inline ClojureScript and I've shared the cljs part here:

(require
  '[reagent.core :as r]
  '[reagent.dom :as rdom])

(def state (r/atom {:principal 1000
                    :rate 5
                    :years 10}))

(defn calculate-compound-interest [principal rate years]
  (for [year (range 1 (inc years))]
    {:year year
     :balance (* principal (Math/pow (+ 1 (/ rate 100)) year))}))

(defn input-field [label key type]
  [:div
   [:label label]
   [:input {:type type
        :value (get @state key)
        :on-change #(swap! state assoc key (js/parseFloat (.. % -target -value)))}]])

(defn result-table []
  (let [{:keys [principal rate years]} @state
    results (calculate-compound-interest principal rate years)]
    [:table
     [:thead
      [:tr
       [:th "Year"]
       [:th "Balance"]]]
     [:tbody
      (for [{:keys [year balance]} results]
    ^{:key year}
    [:tr
     [:td year]
     [:td (str "$" (.toFixed balance 2))]])]]))

(defn compound-interest-calculator []
  [:div
   [:h1 "Compound Interest Calculator"]
   [input-field "Initial Principal ($): " :principal "number"]
   [input-field "Annual Interest Rate (%): " :rate "number"]
   [input-field "Investment Duration (years): " :years "number"]
   [result-table]])

(rdom/render [compound-interest-calculator] (.getElementById js/document "app"))

I hope this will be useful to people who want to build with LLMs and ClojureScript. Enjoy!

Sept. 10, 2023

Today I put together a small test repo to check how much space is saved when replacing React with Preact in a ClojureScript project.

I used npm init shadowfront prtest to get a basic project up and running. This creates a simple one page Reagent app with a button you can click.

(ns prtest.core
  (:require
    [reagent.core :as r]
    [reagent.dom :as rdom]))

(defonce state (r/atom {}))

(defn component-main [_state]
  [:div
   [:h1 "prtest"]
   [:p "Welcome to the app!"]
   [:button {:on-click #(js/alert "Hello world!")}
            "click me"]])

(defn start {:dev/after-load true} []
  (rdom/render [component-main state]
               (js/document.getElementById "app")))

(defn init []
  (start))

I made a build to check the size of the resulting js binary. Then I uninstalled react and react-dom and installed preact@8 and preact-compat. Then I updated shadow-cljs.edn to add the following clause into the :app build:

:js-options {:resolve {"react" {:target :npm :require "preact-compat"}
                       "react-dom" {:target :npm :require "preact-compat"}}

This asks shadow-cljs to alias those React modules to the Preact compatibility layer throughout the whole stack.

I ran make to build the project before and after the change and got the following results:

  1. With React = 292k
    $ du -hs build/js/main.js 
    292K    build/js/main.js
  1. With Preact = 172k
    $ du -hs build/js/main.js 
    172K    build/js/main.js

A 41% size reduction (120k) for a simple one page app seems pretty good. Most of the remaining 172k would be ClojureScript core and libraries such as Reagent.

Update: shadow-cljs lets us generate a build report. Here's a build report with React and then Preact:

React ClojureScript build report

Package Weight %
react-dom @ npm: 18.2.0 128.22 KB 45.8 %
org.clojure/clojurescript @ mvn: 1.11.60 115.71 KB 41.4 %
reagent @ mvn: 1.1.0 22.93 KB 8.2 %
react @ npm: 18.2.0 6.49 KB 2.3 %
scheduler @ npm: 0.23.0 3.96 KB 1.4 %
org.clojure/google-closure-library @ mvn: 0.0-20230227-c7c0a541 1.12 KB 0.4 %
Generated Files 932 0.3 %
src 490 0.2 %

Preact ClojureScript build report

Package Weight %
org.clojure/clojurescript @ mvn: 1.11.60 115.61 KB 71.4 %
reagent @ mvn: 1.1.0 22.94 KB 14.2 %
preact-compat @ npm: 3.19.0 9.24 KB 5.7 %
preact @ npm: 8.5.3 8.18 KB 5.1 %
preact-context @ npm: 1.1.4 2.55 KB 1.6 %
org.clojure/google-closure-library @ mvn: 0.0-20230227-c7c0a541 1.12 KB 0.7 %
Generated Files 931 0.6 %
prop-types @ npm: 15.8.1 801 0.5 %
src 490 0.3 %

March 26, 2023

tl;dr: you can generate very small (less than 1k) JS artifacts from ClojureScript with some tradeoffs. I worked out a list of rules to follow and made the cljs-ultralight library to help with this.

Photograph of a glider in the air

Most of the web apps I build are rich front-end UIs with a lot of interactivity. Quite often they are generating audio in real time and performing other complicated multimedia activites. This is where ClojureScript and shadow-cljs really shine. All of the leverage of a powerful LISP with its many developer-friendly affordances (editor integration, hot-loading, interop, repl) brought to bear, allowing me to quickly iterate and build with fewer bugs.

On many projects I find myself also needing a small amount of JavaScript on a mostly static page. An example would be a static content page that has a form with a submit button that needs to be disabled until particular fields are filled. It seems a bit excessive to send a 100s of kilobyte JS file with the full power of Clojure's immutable datastructures and other language features just to change an attribute on one button.

In the past I resorted a tiny bit of vanilla JS to solve this problem. I have now discovered I can use ClojureScript carefully to get most of what is nice about the Clojure developer experience and still get a very small JS artifact.

Here's an example from the Jsfxr Pro accounts page. What this code does is check whether the user has changed a checkbox on the accounts page, and shows the "save" (submit) button if there are any changes.

(ns sfxrpro.ui.account)

(defn start {:dev/after-load true} []
  (let [input (.querySelector js/document "input#update-email")
        submit-button (.querySelector js/document "button[type='submit']")
        initial-value (-> input .-checked)]
    (aset submit-button "style" "display" "none")
    (aset input "onchange"
          (fn [ev]
            (let [checked (-> ev .-target .-checked)]
              (aset submit-button "style" "display"
                    (if (coercive-= checked initial-value)
                      "none"
                      "block")))))))

(defn main! []
  (start))

This code compiled to around 500 bytes. It has since been updated to do a bunch of different more complicated stuff and today it compiles to 900 bytes. I'll talk about some of the special weirdness and language tradeoffs in a second, but first here is the shadow-cljs config I used.

{:builds {:app {:target :browser
                :output-dir "public/js"
                :asset-path "/js"
                :modules {:main {:init-fn sfxrpro.ui/main!}}
                :devtools {:watch-dir "public"}
                :release {:output-dir "build/public/js"}}
          :ui {:target :browser
               :output-dir "public/js"
               :asset-path "/js"
               :modules {:account {:init-fn sfxrpro.ui.account/main!}}
               :devtools {:watch-dir "public"}
               :release {:output-dir "build/public/js"}}}

The first build target :app is for the main feature-rich app which does all the complicated stuff. I am fine with this being a larger artifact as it does a lot of things for the user including realtime generation of audio samples.

The second target :ui creates a file called account.js which is just 900 bytes. It gets loaded on the accounts page which is statically rendered. The reason for two completely separate build targets is otherwise the compiler will smoosh all of the code together and bloat your artifact size. It is easiest just to keep both codebases completely separate.

When compiling I found it useful to have a terminal open watching the file size of account.js so I could see real time when the size ballooned out and figure out which code was making that happen.

So what tricks do we have to use in the code to get the artifact size down? Here is a brief list of rules to follow to stay small. If you break any of these rules your artifact size will balloon.

  1. Do not use any native Clojure data types. Don't use vec or hash-map or list for example. Instead you have to use native JavaScript data structures at all times like #js {:question 42} and #js [1 2 3]. That also means you will have to use aget and aset instead of get and assoc. It means we are dropping immutablity and other data type features.
  2. Do not use Clojure's = operator. I know that sounds mad but what you can use instead is ClojureScript's coercive-=. This function does a native-style surface level JavaScript comparison. This means you have to give up the value based equality comparison you can use on deeply nested datastructures in Clojure.
  3. Do not use certain built-ins like partial. Other clever built-ins like juxt are probably going to be bad for file size too. As far as I can tell it's anything that uses immutable Clojure types under the hood. For the specific case of partial you can use #(...) instead to do what you need.
  4. Use js/console.log instead of print.
  5. Use (.map some-js-array some-func) instead of (map some-func some-js-array)

Generally as far as possible you should stick with native JavaScript calls and data types.

If all of this sounds onerous remember that the idea here is to only do this in situations where you have a small codebase giving the user some small amount of interactivity on a web page. So that's the tradeoff. You still get LISP syntax, editor integration, hot-loading, repl, and lots of other nice Cloure stuff, but you have to forgo immutable datastructures and language features like partial.

I have created a small library called cljs-ultralight to help with the UI side of things. It uses browser calls and returns JS data types. You can use it to perform common UI operations like attaching event handlers and selecting elements, without incurring too much overhead.

The library applied-science.js-interop also works with these techniques. Require it like this: [applied-science.js-interop :as j] and you can use j/assoc! and j/get and friends. Note if you use j/get-in or other functions that take a list argument, you should instead pass a JavaScript array which works well.

Also note that @borkdude has a couple of very interesting projects under way in this space. Check out squint and cherry for more details.

Jan. 30, 2023

My first app of the year is out, hooray! \o/ It's a simple app to sync pocket operator devices. It outputs a sync signal from your phone which you can plug into your pocket operator's left input to drive it using a 2.5mm male-to-male stereo audio cable. It works well with the p0k3t0 Sync Splitter.

You can get it for Android and iPhone:

PO Sync connected to a phone

This was a fun app to build. I made it because somebody left this review on one of my other apps on Google Play:

Using this for the PO sync feature. I like that most; everything else is okay... I think a great idea would be to make an app with just the PO sync feature and a tempo slider or wheel, plus an on/off

So I knew there was at least one person who wanted this app. It was simple to implement and I got to use my favourite programming languge, ClojureScript. I love it when people need software that I know I can put together quickly. You can get the source code here:

https://github.com/chr15m/PocketSync

2023 is going to be the year of pocket operator apps for Dopeloop and I. I hope to make at least 4 new music apps. I'll post back here when I release them (and also to newsletter + Dopeloop subscribers).

Dec. 30, 2022

Mastodon is a real breeze to develop for. I was able to use nbb (Clojure scripting on Node.js) to post an image using the API in a few minutes. Here's how.

Step 1: Create a new Mastodon app.

Go to Preferences -> Development -> New Application, or visit /settings/applications/new on your Mastodon server.

screenshot.png

Step 2: Store the access token

Once that is done copy the "Access token" and your server's URL. Put them in an env file:

export MASTO_ACCESS_TOK=...
export MASTO_SERVER=...

Get these into your current environment (or use direnv etc.):

. ./env

Step 3: Set up nbb & libraries

echo {} > package.json
npm i nbb masto

Step 4: Create the code

In a file called post-image.cljs put the following code:

(ns post-image
  (:require
    [promesa.core :as p]
    [applied-science.js-interop :as j]
    ["fs" :refer [readFileSync]]
    ["process" :refer [env]]
    ["masto" :refer [login]]))

(p/let [masto (login #js {:url (j/get env :MASTO_SERVER) :accessToken (j/get env :MASTO_ACCESS_TOK)})
        attachment (j/call-in masto [:v2 :mediaAttachments :create] #js {:file (readFileSync (last argv))
                                                                         :description "Test image"})
        status (j/call-in masto [:v1 :statuses :create] #js {:status "Hello this is a test!"
                                                             :visibility "public"
                                                             :mediaIds #js [(j/get attachment :id)]})]
  (js/console.log status))

Step 5: Test it.

npx nbb post-image.cljs some-image.png

Congratulations, you now have an nbb script which can post images to Mastodon. Enjoy!