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
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
(current-line-count number?) will equal
13 the next time you evaluate it.
Here’s the cool parts:
- There is no thread dedicated to polling the file. The living build will only check if there was a change on the current thread when you ask to see the value.
- The procedure passed to
#:build!does run on its own short-lived thread to avoid blocking the main thread, or other builds.
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
#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 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.
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:
(current-line-count)returns a cell.
((current-line-count))returns the cell value.
(((current-line-count)))waits for and returns the number of lines in the file.
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.
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
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:
(current-home-page)is the cell used for monitoring the home page.
((current-home-page))is the procedure that waits on the value representing the latest home page.
(((current-home-page)))is the procedure that, when applied, combines the latest home page with the latest version of its dependencies.
((((current-home-page))))is a structure that represents a deliverable web page.
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
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
(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
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!