.TH threading 7 .SH NAME threading \- Functional Threading “Macros” .SH AUTHOR Artyom Bologov .SH SYNOPSIS Threading macros make Lisp-family languages much more readable. Other languages too, potentially! Except… other languages don’t have macros. How do we go about enabling threading “macros” there? .SH TEXT .P I love Common Lisp. But my dayjob is in Clojure (and TypeScript, ugh.) I can’t help but notice the convenience of threading macros. .SH Threading Readability .P Compare these two pieces of code: .P .in +4n .EX ;; 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))))) .EE .in .P — Threading vs. non-threading Lisp code .P 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. .P 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 in pure Lambda Calculus? .SH Threading Combinators .P 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: .P .in +4n .EX piping 3 : pipe (* 2) : pipe 1+ piped . ;; or, abbreviated --> 3 : -> (* 2) : -> 1+ >-- . ;; or, with colons expanded to parens --> 3 (-> (* 2) (-> 1+ >--)) . .EE .in .P — Hypothetical threading syntax .B Note on Lamber syntax .EX .P The language I’m using in this post is .UR https://github.com/aartaka/lamber Lamber .UE , my Lambda Calculus compiling language. It features a minimalist syntax with only functions, values, \fBif\fP-s, and operators like \fBwisp (7)\fP Wisp’s colon nesting operator (section inline-colon) and terminating period (similar to Lua’s \fBend\fP.) .EE .P 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. .P .in +4n .EX def piping fn (x f) f x . ;; also known as T combinator alias piping T . .EE .in .P — Simple piping wrapper .P Now to the workhorse \fBpipe\fP function: .P .in +4n .EX def pipe fn (f g x) g : f x . ;; Also known as Queer Bird Combinator alias pipe Q . .EE .in .P — pipe combinator .P The way this magic works is: .P * We take a function to pipe and close over it .P * Then we take a “continuation” to apply to this function .P * And then we do the closed-over action on the value and “continue” the computation .P So \fBpipe (* 2) : pipe 1+ piped\fP means .P * closing over \fB(* 2)\fP and .P * taking a function closed over \fB1+\fP .P * and then applying this \fB1+\fP function to result of \fB(* 2)\fP applied to the data. .P Nice reversal, huh? But what does this \fBpiped\fP thing does? It acts as a piping terminator, essentially returning what’s passed to it: .P .in +4n .EX def piped fn (x) x . ;; or alias piped identity . .EE .in .P — Simple piped definition/alias .P We accept second function into \fBpipe\fP. 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 \fBidentity\fP. .P Not sure if I explain it well enough. So here’s an expansion process: .P .in +4n .EX 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 . .EE .in .P — Meticulous expansion of piping ensemble .P This threading is still relatively wordy and noisy, even when using \fB->\fP 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! .P 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. ================================================================================ .P It is a nice coincidence that Marvin Borner came to the same .UR https://bruijn.marvinborner.de/samples/aoc_2022_01_solve.bruijn shape for his “→” infix operator .UE , and it seems it’s called Q/queer combinator! .SH COPYRIGHT .UR https://creativecommons.org/licenses/by/4.0 CC-BY 4.0 .UE 2022-2026 by Artyom Bologov (aartaka,) .UR https://codeberg.org/aartaka/pages/commit/a91befa with one commit remixing Claude-generated code .UE . Any and all opinions listed here are my own and not representative of my employers; future, past and present.