vkz / tilda

Opinionated threading macro for Racket

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Motivation

Skip to features and examples

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.

Installation

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.

Features

(~> 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.

#:as id

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 ~))

#:when predicate expr/hole

short circuit threading when (predicate ~) is true and return the value of expr/hole:

(~> 42
    (random)
    #:when even? ~
    (add1))

#:unless predicate expr/hole

short circuit threading unless (predicate ~) is true and return the value of expr/hole:

(~> 42
    (random)
    #:unless odd? ~
    (add1))

#:with pat expr/hole

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")

#:do (expr/hole …)

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

About

Opinionated threading macro for Racket


Languages

Language:Racket 97.4%Language:Makefile 2.6%