1 Threading I
Clojure includes a useful macro, which it calls ->. This macro is known as the “threading” macro because it “threads” a value through a series of expressions. You may also find it useful to compare it to a shell pipeline. For example, in Clojure:
> (-> 5 (* 2) (+ 1)) 11
The precise details of how that works will be elaborated upon in a moment, but notice how the first expression, 5, is first multiplied by two, then incremented by one to produce 11.
This macro is available in Racket via the threading package. The “real” version of ~> is more featureful than the version you will implement here, but future exercises will expand upon this simplified version.
In Racket, the name -> is already used by racket/contract, so the threading macro in Racket is usually named ~> instead. We will use this name for our threading macro.
1.1 Specification
The ~> macro is used to convert a nested expression into a more readable “flattened” pipeline. For example, this expression:
(- (bytes-ref (string->bytes/utf-8 (symbol->string 'abc)) 1) 2)
Can be written in “pipeline” style like this:
(~> 'abc symbol->string string->bytes/utf-8 (bytes-ref 1) (- 2))
The following three rules specify how the above transformation works.
Rule 1. For any expression x, (~> x) is equivalent to x. That is, when ~> only has a single subform, it expands to the subform directly, with no modifications.
This forms the “base case” of the ~> macro. Successive application of the other rules will eventually produce an expression of the form (~> x).
Rule 2. For any expression (f a ...) (where f should evaluate to a function and each a should evaluate to a function argument), (~> x (f a ...) more ...) is equivalent to (~> (f x a ...) more ...).
For example, (~> 5 (+ 1)) expands to (~> (+ 5 1)).
Note that the above example allows an additional sequence of more subforms, which are passed through unchanged. That is, (~> 5 (+ 1) (* 2)) expands into (~> (+ 5 1) (* 2)), which in turn expands into (~> (* (+ 5 1) 2)), finally producing (* (+ 5 1) 2) due to Rule 1.
Here are some evaluation examples that include expansion steps to help better understand how this rule works:
> (~> "hello, " (string-append "world"))
expansion steps:
1. (~> (string-append "hello, " "world"))
2. (string-append "hello, " "world")
"hello, world"
> (~> #\a (list #\z) (list->string))
expansion steps:
1. (~> (list #\a #\z)
(list->string))
2. (~> (list->string (list #\a #\z))
3. (list->string (list #\a #\z))
"az"
> (~> 'abc (symbol->string) (string->bytes/utf-8) (bytes-ref 1) (- 2))
expansion steps:
1. (~> (symbol->string 'abc)
(string->bytes/utf-8)
(bytes-ref 1)
(- 2))
2. (~> (string->bytes/utf-8 (symbol->string 'abc))
(bytes-ref 1)
(- 2))
3. (~> (bytes-ref (string->bytes/utf-8 (symbol->string 'abc)) 1)
(- 2))
4. (~> (- (bytes-ref (string->bytes/utf-8 (symbol->string 'abc)) 1) 2))
5. (- (bytes-ref (string->bytes/utf-8 (symbol->string 'abc)) 1) 2)
96
Rule 3. For any identifier id, (~> x id more ...) is equivalent to (~> x (id) more ...).
For example, (~> 'abc symbol->string) expands to (~> 'abc (symbol->string)).
Put another way, this rule just wraps bare identifiers in parentheses—
> (~> #\a (list #\z) list->string) "az"
> (~> 'abc symbol->string string->bytes/utf-8 (bytes-ref 1) (- 2)) 96
1.2 Grammar
This section describes the expected grammar for the ~> macro without specifying functionality.
syntax
(~> val-expr clause ...)
clause = (fn-expr arg-expr ...) | bare-id