I'm a third-year Computer Science and Mathematics student at Northeastern University interested in programming languages and developer tooling.
(λx.x x) (λx.x x)
Rackjure: Using Clojure's Thread Macro in Racket
The thread macro of Rackjure provides incredible convenience.
Edit (2023–05–21): This macro is available through the
threadinglibrary in Racket.
One of the common complaints of Racket and other Lisp/Scheme languages are the absurd amount of parentheses. Critics claim that readability can be diminished with deeper nesting of function calls. It’s a valid complaint — although Racketeers generally observe the “shape” of a program by its indentation to determine and understand the semantics of the code.
English-speaking natives, along with a large fraction of the world’s population, generally read from left-to-right. As such, we may find it more natural when code follows suit. In a fair share of functional programming languages, we find, however, that we read from right-to-left with nested function calls.
Let’s consider an example with working with web-servers in Racket. When we receive a
request struct from our endpoint, we want to extract the data from the body as JSON and transform it into a Racket hash-table.
(require json) (require web-server/servlet) (define (request-body-json req) (request? . -> . hash?) (string->jsexpr (bytes->string/utf-8 (request-post-data/raw req))))
Here’s what the
request-body-json function does:
- Receives a
- Retrieves the raw bytes of the request body (assuming it is a
- Transforms those bytes into a UTF–8 encoded string.
- Finally, converts that string (presumably in JSON format) into a Racket hashtable.
We’ve only applied three functions, and our code already seems deeply nested. We could
compose1 (docs) the functions to reduce the amount of parentheses.
(define (request-body-json req) (request? . -> . hash?) ((compose1 string->jsexpr bytes->string/utf-8 request-post-data/raw) req))
compose1 returns the functions composed together (recall function composition from mathematics). While it does reduce the amount of parentheses, we still have to read the function application from right-to-left.
Clojure solved this problem with their thread macros:
->>. In Racket, these are accessible through rackjure.
$ raco pkg install rackjure
These macros were renamed to
~>> respectively to avoid shadowing the
-> we use in Racket contracts. They provide convenience and readibility. We can rewrite our
request-body-json function as follows:
(require json) (require web-server/servlet) (require rackjure/threading) (define (request-body-json req) (request? . -> . hash?) (~> req request-post-data/raw bytes->string/utf-8 string->jsexpr))
Alternatively, in one line:
(define (request-body-json req) (request? . -> . hash?) (~> req request-post-data/raw bytes->string/utf-8 string->jsexpr))
In either case, we’re reading a more natural direction: top-to-bottom and left-to-right. This seems to provide greater readibility to our code.
So, what is the macro doing? The first argument to the macro is an expression that can be evaluated. The remaining arguments are forms. In our
~> macro, for each form, the previous form (or the starting expression for the first form) is inserted as the second item — that is, the first argument.
(~> req (request-post-data/raw #|inserted here|#) (bytes->string/utf-8 #|inserted here|#) (string->jsexpr #|inserted here|#))
A better example is illustrated in the Racket documentation. Here, we have an expression without the
~> threading macro, that returns the number of bytes of a byte string in bytes.
(string->bytes/utf-8 (number->string (bytes-length #"foobar") 16))
With the threading operator, this would look like:
(~> #"foobar" (bytes-length #|inserted here|#) (number->string #|inserted here|# 16) (string->bytes/utf-8 #|inserted here|#))
~>> threading macro, the previous form is inserted as the last item. Consider this example without the macro:
> (map add1 (filter even? (build-list 20 identity))) '(1 3 5 7 9 11 13 15 17 19)
If we were to use the
~> threading macro, previous forms would be inserted in the wrong location:
(~> (build-list 20 identity) (filter #|inserted here|# even?) (map #|inserted here|# add1))
~>> threading macro solves this issue, inserting them as the last item.
> (~>> (build-list 20 identity) (filter even? #|inserted here|#) (map add1 #|inserted here|#)) '(1 3 5 7 9 11 13 15 17 19)
The threading macros are incredibly convenient to use around the codebase to improve readibility. I personally use them in this site and a backend side-project of mine I’ve been working on. The threading macros are one of the things I admired from Clojure, and I’m pleased they’ve been ported to Racket.