Creating Living, Reactive Builds in Racket

Last updated: home

A while back I introduced Unlike Assets (UA), a Webpack-inspired build system for Racket that helps you build creative projects. Unlike Assets powers Polyglot, and can power any alternatives.

Recently I upgraded UA to handle living builds using kinda-ferpy, which is a library I wrote to make Racket values behave like spreadsheet cells. UA’s new module is unlike-assets/reactive.

Here’s a demo:

If for whatever reason you cannot watch the video or cannot be bothered to watch someone fumble on a keyboard, then keep reading.

A Simple Living Build

Consider this module:

#lang racket

(require unlike-assets/reactive)

(define (start-living-line-build! key)
  (start-living-build!
   key
   #:sample! (λ () (file-or-directory-modify-seconds key))
   #:suppress? =
   #:build! (λ (mtime) (length (file->lines key)))))

This defines a procedure that starts living builds that check when a file was modified, and produces the number of lines in that file as an artifact. While paths are expected in this context, UA builds use string keys (hence key) to name assets more flexibly.

Given that you have a file.txt containing 3 lines of text, then the following session holds:

(define current-line-count (start-living-line-build! "file.txt"))
(current-line-count number?) ; 3

If you go off and add 10 lines to file.txt, then (current-line-count number?) will equal 13 the next time you evaluate it.

Here’s the cool parts:

It’s also possible to reactively start live builds when a user asks for a key. Here I add a sys formal to start-living-line-build! and pass the entire procedure to make-u/a-build-system.

#lang racket

(require unlike-assets/reactive)

(define (start-living-line-build! key sys)
  (start-living-build! key
                       #:sample! (λ () (file-or-directory-modify-seconds key))
                       #:suppress? =
                       #:build! (λ (mtime) (length (file->lines key)))))

(define u/a (make-u/a-build-system start-living-line-build!))

make-u/a-build-system sets up a build system that consults start-living-line-build! to create a living build in response to the user’s request for a build by key. That build will henceforth use that key for the life of the system. If you want a key to be a URI or a scheme of your own choice, that’s fine. They just happen to be path strings relative to the current directory here.

With this module, the following sessions holds:

; Always equal to the current number of lines in index.html
(u/a "index.html" number?)

; Same, but for file.txt.
(u/a "file.txt" number?)

; This will show a hash of all encountered builds
; e.g. #hash(("index.html" . #<procedure:live-build>) ("file.txt" . #<procedure:live-build>))
(u/a)

Okay, but what’s sys?

The added sys formal is actually bound to the same procedure as u/a in the above example. You can use it to recursively request the values of other living builds when constructing an asset. This is powerful. I could leverage a pattern like the below to treat the file as a list of keys to act as build input.

#:build!
(λ (mtime)
  (define lines (file->lines key))
  (for ([l (in-list lines)])
    ((sys l)))
  (length lines))

That way, the build will request and build dependencies without blocking. I’m leaving out some details on what it means to wait for those dependencies. That’s another article.

Why use number?

A living build represents layers of deferred work. If you run (current-line-count) on it’s own, it will just return a kinda-ferpy cell whose value is a procedure. You retrieve the value of a cell by applying it like a procedure:

Put another way:

It looks like a stereotype of Lisp, but I like it. Of all the ways complexity appears, I prefer that the build looks like repeated applications to construct increasingly concrete values. Because that’s what a build process is.

But to answer the question: (current-line-count number?) just means “drill down until you get a number and give me that.”

But why have the extra procedures at all?!

Each representation has a use.

(current-line-count) can be used for monitoring. You can depend on that cell to subscribe to new builds.

Using kinda-ferpy, you can hook up a logger to a specific cell this way.

(require kinda-ferpy)

(define %cell (current-line-count))
(define cell-mon
  (% #:dependency %cell
    (log-info "A line count is changing")))

That’s fun because it means that you can learn about a build by hooking into the relevant cell. Since these cells behave like spreadsheet cells, any change in %cell’s value will trigger the log.

((current-line-count)) can be used for scheduling. This procedure acts like a promise so I can defer the work of waiting for the value.

(define wait ((current-line-count)))

; do something else...

(wait) ; okay, I want the line count now.

(((current-line-count))) is of course useful for the actual value of the build, but it might not always stop 3 levels deep.

Regarding this Website

I have not yet released the source code for this website. That will change soon, since it’s a great example of how to use unlike-assets.

For now, let’s say I am building a web page using a living build like (current-home-page). When I change my main stylesheet, my home page updates to reflect the latest version of that stylesheet’s cache-busted name (like a89be3dd.css). It turns out that if I want to check on the latest value of other living builds properly, I need the value of the web-page build to be… you guessed it, another procedure.

This is the actual breakdown that I use for this website:

In general, yes, this can mean asking “And how many bracket pairs go here?!” But predicates like number? solve that.

I can build my website using sessions like this:

> (define current-home-page (u/a "index.html"))
> (define iface (current-home-page live-web-asset?))

; This saves the production ready index.html to disk.
> (apply (live-web-asset-save! iface))

; This returns an HTTP response of the page for use in a development web server.
> (live-web-asset-response iface)

; This saves the production ready index.html to disk.
> (apply (live-web-asset-save! iface))

Every time I evaluate (apply (live-web-asset-save! iface)), I save the latest production-ready home page to disk. If I write a build server that responds to GET index.html with (live-web-asset-response (u/a "index.html" live-web-asset?)), then the server will always show the latest version of the page while I work.

Since I can kick off builds for dependencies and wait for them later, I can crawl my web pages for resources that don’t yet exist and make them exist, just by asking for a finished index.html.

When I’m happy with what I have, I can save the entire website to disk by looping over the visited builds caused by my asking for index.html.

(for ([live-build (in-hash-values (u/a))])
((live-web-asset-save! (live-build live-web-asset?))))

Isn’t that fun?

I want it!

I’ve covered how to use unlike-assets/reactive to create living builds in Racket. I hope it brings you joy and fortune. Here’s the source code. You can install the latest version of unlike-assets using raco pkg install unlike-assets.

If this was valuable to you and you want to support the project, please consider supporting my work. A subscription gets you a say over development, access to a private chat room, and timely support.

Whether the project works for you or not, please send feedback to moc.drarhvpwegegas@egas. I’d love to hear from you!