tilda
package implements the so called threading macro that lets
you compose function invocations left-to-right from the first one to
compute, then the next 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 assuming eager execution:
(apply + (map add1 (filter odd? (range 1 6))))
;; vs
(~> 6
(range 1 ~)
(filter odd? ~)
(map add1 ~)
(apply + ~))
Popularized by Clojure the macro typically comes in three flavors:
->
or “thread first” propagates the value as the first argument in the following clause,->>
or “thread last” threads the result as the last argument,as->
generalizes the above forms by explicitly binding threaded values to a user picked identifier.
Since ->
has already been taken for contract declarations, Racket
usually picks tilda-arrow in place of dash-arrow. That is to
readily admit this isn’t the first Racket package to provide threading
macros. If you want a battle-tested threading library I suggest
installing Alexis King’s threading or Greg Hendershott’s rackjure
which also comes with other Clojure inspired goodies. You can’t go
wrong with either of them.
Present implementation reflects my idiosyncratic aesthetic and requirements. If you are new to Racket, rolling out your own threading macros makes for an exceptional exercise in macrology.
Install it from Racket Packages:
cd tilda
raco pkg install -u
Or clone and install from the local check-out:
git clone https://github.com/vkz/tilda.git
cd tilda
raco pkg install -u
You can also do the entire clone, install, link dance in one go by
passing --clone
to raco
. Please consult the Racket docs here.
(~> expr clause ...) clause = keyword-clause | (expr ...) | (pre-expr ... hole post-expr ...) hole = ~ | ~id | ~id? keyword-clause = #:with pat expr/hole | #:do (expr/hole ...) | #:as id | #:when predicate expr/hole | #:unless predicate expr/hole expr/hole = (pre-expr ... hole post-expr ...) | (expr ...) | expr
Typically the position where to insert (or “thread” through) the value is marked with ~. Clauses without a marker are treated as “thread first”, that is the threaded value will be inserted as the first argument.
Any unbound identifier that starts with tilda can be a hole-marker, so you can use either ~ or ~num to mark the position to thread through. The latter communicates what kind of value is being threaded and makes for a more readable code.
Hole-markers of the form ~id? that end in ? are treated as
predicates to be checked before threading continues. Unless (id? ~)
is true, computation short circuits returning #f as the result of
the entire threading form. Keyword-clauses #:when
and #:unless
allow for a more expressive way to guard and short-circuit
computation.
Every threading form is implicitly wrapped in an escape continuation, which can be triggered with <~. That is, (<~ 42) anywhere will escape making 42 the result of the entire threading form.
keyword-clauses let you change the semantics right in the middle of threading. Any bindings they introduce are available in subsequent clauses.
bind the value being threaded to id
and suspend threading for the
next clause, restart threading in subsequent clauses. Such behavior
effectively restarts threading with a new value and accommodates forms
like if
, cond
, let
etc to compute said value:
(~> (random 42)
#:as v
(cond
((even? v) v)
((odd? v) (add1 v)))
(/ 2)
(format "Half of ~s is ~s" v ~))
short circuit threading when (predicate ~) is true and return the value of expr/hole:
(~> 42
(random)
#:when even? ~
(add1))
short circuit threading unless (predicate ~) is true and return the value of expr/hole:
(~> 42
(random)
#:unless odd? ~
(add1))
pattern-match on an expression with a hole, continue to thread with pattern variables bound in the following clauses:
(~> "foo bar"
(string-split)
#:with (list foo bar) ~
(list* bar foo ~foobar))
;; =>
'("bar" "foo" "foo" "bar")
introduce implicit begin
block to compute and bind interim values or
perform side effects:
(check-equal? (~> 0
#:do ((define foo ~) (printf "got ~s \n" foo))
(add1 ~)
#:do ((define bar ~) (printf "got ~s \n" bar))
(add1 ~)
(list foo bar ~))
'(0 1 2))
;; =>
got 0
got 1