I love Common Lisp. But my dayjob is in Clojure (and TypeScript, ugh.) I can’t help but notice the convenience of threading macros.
Compare these two pieces of code:
;; No threading (* (1+ (* significand (flt (/ (expt 2 significand))))) (expt 2 (- exponent (flt bias))) sign) ;; Threading (->> significand (expt 2) / flt (* significand) 1+ (* sign (expt 2 (- exponent (flt bias)))))
Threading code is much more readable. It shows the sequence of actions in the order they happen in. It omits the obvious parentheses. It highlights the patterns in function applications.
One problem with threading macros though: they are macros. Lisps are good at sophisticated syntax transformations. Other languages—not so much. So we need other ways to thread functions together. Like... combinators?
The idea is simple: we need several functions that’d pass closures around. Bubbling outward and storing the inner functions for until the outer ones run. (Reversing the applicative inner->outer evaluation order, thus the need for extra closures.) Must look something like:
piping 3 : pipe (* 2) : pipe 1+ piped . ;; or, abbreviated --> 3 : -> (* 2) : -> 1+ >-- . ;; or, with colons expanded to parens --> 3 (-> (* 2) (-> 1+ >--)) .
The language I’m using in this post is
Lamber, my Lambda Calculus compiling language.
It features a minimalist syntax with only functions, values,
First, let’s add a function that’ll initiate the piping. Nothing fancy, just take the initial value and a curried function. And then apply the function to the value.
def piping fn (x f) f x . ;; also known as T combinator alias piping T .
Now to the workhorse
The way this magic works is:
So
Nice reversal, huh?
But what does this
We accept second function into
Not sure if I explain it well enough.
So here’s an expansion process:
This threading is still relatively wordy and noisy, even when using
The implementation in this post is thread-last, which rhymes well with Lamber’s philosophy:
functions should be tail-heavy, putting the data to act on as last argument.
So I only need thread-last.
I leave thread-first combinator (and multi-arg ones) as an exercise to the reader.
def pipe fn (f g x)
g : f x .
We take a function to pipe and close over it
closing over
def piped fn (x) x .
;; or
alias piped identity .
piping 3 : pipe (* 2) : pipe 1+ piped .
piping 3 : pipe (* 2) : pipe 1+ identity .
piping 3 : pipe (* 2) : (fn (f g x) g : f x) 1+ identity .
piping 3 : pipe (* 2) : (fn (g x) g : 1+ x) identity .
piping 3 : pipe (* 2) : (fn (x) identity : 1+ x) .
piping 3 : pipe (* 2) (fn (x) identity : 1+ x) .
piping 3 : (fn (f g x) g : f x) (* 2) (fn (x) identity : 1+ x) .
piping 3 : (fn (g x) g : (* 2) x) (fn (x) identity : 1+ x) .
piping 3 : (fn (x) (fn (x) identity : 1+ x) : (* 2) x) .
(fn (x f) f x) 3 (fn (x) (fn (x) identity : 1+ x) : (* 2) x) .
(fn (f) f 3) (fn (x) (fn (x) identity : 1+ x) : (* 2) x) .
(fn (x) (fn (x) identity : 1+ x) : (* 2) x) 3.
(fn (x) identity : 1+ x) : (* 2) 3 .
(fn (x) identity : 1+ x) : (* 2 3) .
(fn (x) identity : 1+ x) : 6 .
identity : 1+ 6 .
identity 7 .
7 .