函数式线程“宏”
Functional Threading "Macros"

原始链接: https://aartaka.me/threading.html

## Lisp 类语言中的线程 作者是一位 Common Lisp 爱好者,同时使用 Clojure 和 TypeScript,探索在其他语言中重现 Clojure 方便的线程宏。线程宏通过以自然、从左到右的顺序表达一系列函数应用来增强可读性,从而消除过多的括号。 然而,线程宏依赖于 Lisp 中常见的强大宏系统,而在其他地方则不太常见。这促使人们研究*线程组合器*——传递闭包以实现类似效果的函数。作者在他们的语言 Lamber 中实现了这个概念,使用 `piping`、`pipe` 和 `piped` 函数。 `piping` 启动过程,`pipe` 通过闭包将函数链接在一起并按顺序应用它们,而 `piped` 作为终止符,返回最终值。即使使用像 `->` 这样的简写别名,由于 Lamber 的极简设计,生成的语法仍然有些冗长,但作者认为可以在其他语言中用专用运算符更简洁地实现类似的概念。该方法侧重于“thread-last”风格,符合 Lamber 偏爱尾重函数参数的偏好。

黑客新闻 新 | 过去 | 评论 | 提问 | 展示 | 招聘 | 提交 登录 函数式线程“宏” (aartaka.me) 12 分,GarethX 1小时前 | 隐藏 | 过去 | 收藏 | 讨论 考虑申请YC冬季2026批次!申请截止至11月10日 指南 | 常见问题 | 列表 | API | 安全 | 法律 | 申请YC | 联系 搜索:
相关文章

原文

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 #

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 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 #

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+ >--)) .
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.

def piping fn (x f)
  f x .
;; also known as T combinator
alias piping T .
Simple piping wrapper

Now to the workhorse pipe function:

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:

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:

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.

Leave feedback! (via email)

联系我们 contact @ memedata.com