tilda package implements the so called threading macro that simply lets you
compose function invocations left-to-right from the first one to compute, then the
next one that uses the result from the first, and so on. Contrast this with the
typical way you would have to nest your function calls in a Lisp of your choice.
;; typically you would have to write something like this, where assuming eager ;; execution semantics range would compute first, then filter, then map, and ;; finally apply. (apply + (map add1 (filter odd? (range 1 6)))) ;; threading macro lets you express the same computation in a more natural order (~> 6 (range 1 ~) (filter odd? ~) (map add1 ~) (apply + ~))
In the above example the latter expands exactly into the former expression.
Popularized by Clojure threading macros typically come in three flavors
propagates result as the first argument in the following clause,
->> threads the
result as the last argument and there’s usually the third macro that lets you
assign the hole-marker - identifier to be replaced in each clause.
-> has already been taken for contract declarations, Racket usually uses
tilda to name the same macros. That is to readily admit this isn’t the first
Racket package to provide threading macros. If you want battle tested threading
library I suggest installing Alexis King’s threading. I believe Greg
Hendershott’s rackjure also ships them and comes with a lot of goodies
popularized by Clojure. You can’t go wrong with either of them.
This implementation is merely a reflection of my taste when it comes to threading as well as a decent exercise in Racket macrology. If you are new to Racket, rolling out your own pre- and post-threading macros is a really good exercise, one that you could push even further, as I have, and take Racket’s rich macro-system for a test drive.
Speaking of taste:
- I find naked function-id clauses, although sometimes cleaner, mostly inconsintent and really prefer to have every clause wrapped in parenthesis,
->>macros are awkward to use when some of the clauses need to take the first argument, while others expect the last; if you start with
->, then find you need to switch to post-threading you could simply use
->>as a clause in its own right, sadly the same trick doesn’t work the other way; this is when you would normally consider explicitly naming your hole-marker; I say this ought to be the default;
- sometimes you want to perform intermediate computation to use in the clauses further along; at this point you have no choice but to split the threading; I think we could allow binding declarations between clauses.
So, the threading macro in
tilda does exactly those things.
I sincerely invite veteran Racketeers to suggest improvements to my macro code. To a large extend this is an exercise, after all.
tilda package although appears seperate here is part of the
that is in heavy development atm and has not been released, yet. Once
in good enough shape to be published
tilda may or may not remain a separate
package. It is, for the time being, to solicit advice from my betters in the
Racket community. It is for that same reason I’ve not published it to the Racket
package repository, plus I’d hate to cause fragmentation where you, the user, have
tough time picking among the libraries that do more or less the same thing.
Simplest thing you can do is clone this repo and install it locally:
cd tilda raco pkg install -u
which will link the cloned directory from your local Racket installation. This
will also let you hack on the library and have your changes propagate to wherever
tilda (most of the time). Assuming you’ve forked the repo you can
also easily send pull requests via Github interface.
You can also do the entire clone, install, link dance in one go by passing
raco. Please consult the docs here.
Knowing that in no way this README can replace proper Racket documentation I kindly ask the reader to make do for the time being as I learn how to scribble my way to perfection. Apologies.
(~> expr clause ...) clause = thread-option | (expr ...) | (pre-expr ... hole post-expr ...) hole = ~ |~id thread-option = #:with pat expr/hole | #:do (expr/hole ...) | #:as id | #:when expr/hole expr/hole | #:unless expr/hole expr/hole expr/hole = (pre-expr ... hole post-expr ...) | (expr ...) | expr
Every clause must be wrapped in parenthesis and explicit hole-marker needs to be supplied unless you want to ignore the result of the previous clause, which you can do. Omitting a hole amounts to starting to thread from the current clause, however any bindings created with keyword thread-options would persist for the dynamic extent of the entire ~> computation.
(require prelude/tilda rackunit) (check-eq? (~> 'foo (symbol->string ~) (format ":~a" ~str) (string->symbol ~)) ':foo) (check-eq? (~> 'foo (symbol->string ~) (format ":~a" ~str) ;; threading can be split by expr that ignores the result (list 42) (car ~)) 42)
Notice, that any unbound identifier that starts with tilda can be a hole-marker, so you can use either ~ or e.g. ~key interchangably, with the latter simply hinting to the reader of your code what sort of thing it’s supposed to be, making hole-markers essentially self-documenting.
You can interleave clauses with thread-options that let you perform and bind
intermediate computations so that you may avoid having to split your thread just
let-bind something. This is of course nothing new to someone who’s used
syntax-parse and friends:
(check-equal? (~> 'foo (symbol->string ~) #:with bar "-bar" #:with baz "-baz" (string-append ~foo bar baz) (format ":~a" ~str) (string->symbol ~) #:do ((define l (list 1 2)) (set! l (cons 0 l))) (cons ~sym l)) '(:foo-bar-baz 0 1 2)) (check-equal? '(0 1 2) (~> 0 #:do ((define foo ~)) (add1 ~) #:do ((define bar ~)) (add1 ~) (list foo bar ~))) ;; note that bound ~id is not treated as a hole so isn't replaced (check-equal? '(6 1) (let ((~foo 1)) (list (~> 2 ;; with LHS takes a match pattern #:with (list a b) (list ~foo ~) (+ ~foo ~ a b)) ~foo))) (check-equal? '(5 6) (~> 6 #:as num #:when (even? ~) (set! num (sub1 num)) (list num ~)))
#:with keyword allows a
match pattern in its LHS.
~> is implicitly wrapped in an escape continuation bound to <~, so you can cut your thread short at any time and return any intermediate result:
(check-eq? 6 (~> 6 #:unless (odd? ~) (<~ ~) (range 1 ~))) ;; #:as and short-circuit with or (check-equal? (list 6 (range 1 6)) (~> 6 #:as upper-limit (range 1 ~) #:as seq (filter odd? ~) (findf even? ~) (or ~num (<~ (list upper-limit seq))) (* 2 ~)))
define~> and lambda~> macros
define~> lets you define functions whose body effectively threads whatever
formal argument you specify as a hole-marker. Otherwise the grammar for its formal
parameters is exactly that of Racket’s
define. Note, however, that you may not
use ~ as a formal parameter, that is because hole-markers are required to be
unbound but of course the whole point of function parameters is to bind them in
;; pick a parameter and prepend its name with ~ to thread its value in the body, ;; you can use that argument in the body as usual, however it won't be treated as ;; a hole-marker but as a bound identifier whose value will be used as expected (define~> ((foo~> . ~arg) b #:c [c 3]) (list* b c ~) #:as all (last ~) #:when (even? ~) (<~ 'even) (+ ~ (car all))) (check-eq? ((foo~> 0 1) 2 #:c 3) 3) (check-eq? ((foo~> 0 1) 2) 3) (check-eq? ((foo~> 0 2) 3) 'even) ;; binding ~ however is not allowed (check-exn #rx"attempt to bind hole-marker" (thunk (convert-compile-time-error (lambda~> ~ (car ~))))) (check-eq? ((λ~> (a b . ~rest) (map add1 ~) (list* ~) (last ~)) 1 2 3 4) 5) (check-eq? ((λ~> ~args (cdr ~) (last ~)) 1 2 3) 3)
Being unable to use ~ as a formal parameter above is somewhat unsatisfying. I could remedy that if I knew how to temporarily disable its binding for the dynamic extent of the body, which, I believe, should be possible by manipulating its set of scopes. If you happen to know how to do this in Racket, do tell.