# 📎 Functional Threading “Macros” By Artyom Bologov (An almost monochrome clean thumbnail. On it, “T H R E A D I N G” is written in large bright letters. The letters are connected with one single line, going right through every one of them and ending with an arrow head cycling back to the letters. In the corners, there are attributions to “Artyom Bologov” and “aartaka.me” in a handwriting that seems to shift from one letter to another, making it less readable.) I love Common Lisp. But my dayjob is in Clojure (and TypeScript, ugh.) I can’t help but notice the convenience of threading macros. ## Threading Readability (#readability) Compare these two pieces of code: =================================== lisp =================================== ;; 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 vs. non-threading Lisp code ==================== 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? ## Threading Combinators (#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: =================================== lamber =================================== piping 3 : pipe (* 2) : pipe 1+ piped . ;; or, abbreviated --> 3 : -> (* 2) : -> 1+ >-- . ;; or, with colons expanded to parens --> 3 (-> (* 2) (-> 1+ >--)) . ==================== Hypothetical threading syntax ==================== Note on Lamber syntax { The language I’m using in this post is Lamber , my Lambda Calculus compiling language. It features a minimalist syntax with only functions, values, `if`-s, and operators like Wisp’s colon nesting operator and terminating period (similar to Lua’s `end`.) } 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. =================================== lamber =================================== def piping fn (x f) f x . ;; also known as T combinator alias piping T . ==================== Simple piping wrapper ==================== Now to the workhorse `pipe` function: =================================== lamber =================================== def pipe fn (f g x) g : f x . ==================== pipe combinator ==================== The way this magic works is: – We take a function to pipe and close over it – Then we take a “continuation” to apply to this function – And then we do the closed-over action on the value and “continue” the computation So `pipe (* 2) : pipe 1+ piped` means – closing over `(* 2)` and – taking a function closed over `1+` – and then applying this `1+` function to result of `(* 2)` applied to the data. Nice reversal, huh? But what does this `piped` thing does? It acts as a piping terminator, essentially returning what’s passed to it: =================================== lamber =================================== def piped fn (x) x . ;; or alias piped identity . ==================== Simple piped definition/alias ==================== We accept second function into `pipe`. And then apply it to the result of the first one. And the best way to stop the computation is to simply return the data passed into this second function. Thus `identity`. Not sure if I explain it well enough. So here’s an expansion process: =================================== lamber =================================== 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 . ==================== Meticulous expansion of piping ensemble ==================== This threading is still relatively wordy and noisy, even when using `->` aliases. But that’s mostly due to Lamber’s minimalism and colon reliance. Other languages might even introduce special operators behaving this way. And it’ll work just fine without colons and nesting! 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. Copyright 2022-2025 Artyom Bologov (aartaka). Any and all opinions listed here are my own and not representative of my employers; future, past and present.