summaryrefslogtreecommitdiff
path: root/content/article
diff options
context:
space:
mode:
Diffstat (limited to 'content/article')
-rw-r--r--content/article/clj8.md172
-rw-r--r--content/article/tech-witchhunt.md13
2 files changed, 171 insertions, 14 deletions
diff --git a/content/article/clj8.md b/content/article/clj8.md
index aa2442c1..a8f58758 100644
--- a/content/article/clj8.md
+++ b/content/article/clj8.md
@@ -1,5 +1,5 @@
+++
-title = 'Coffee Break Clojure, Fold'
+title = 'Coffee Break Clojure, Transducer'
subtitle = ''
slug = 'clj8'
tags = ["Clojure","Lisp","编程"]
@@ -7,6 +7,9 @@ categories = '茶歇蔻芥'
date = 2026-10-17T17:45:13+08:00
draft = true
toc = true
+endnotes = [
+ '本文参考了 Rich Hickey 写就的《[Anatomy of a Reducer](https://clojure.org/news/2012/05/15/anatomy-of-reducer)》《[Transducers are coming](https://clojure.org/news/2014/08/06/transducers-are-coming)》《[Reducers](https://clojure.org/news/2012/05/08/reducers)》和 Clojure 参考文档里的 [Transducers](https://clojure.org/reference/transducers) 一节。'
+]
+++
在第三期《[只用圆括号怎么够?](/article/clj3/#reduce)》里,我讲到了 `reduce` 函数,先来简单复习一下:`reduce` 最少接收两个参数,形如 `(reduce f coll)`,其中 `f` 是接收两个参数的函数,形如 `(f x y)`;`reduce` 遍历 `coll` 中的每个元素,现对前两个元素求 `(f coll1 coll2)`,然后对第三个参数求 `(f (f coll1 coll2) coll3)`,以此类推。<!--more-->
@@ -96,5 +99,172 @@ Fold 中的 `f` 本身也可以是 Fold。还记得 `into` 吗?
在 Clojure 中,能够被放在 `reduce` 里使用的函数就叫作归约函数(reducing function)。这个概念与函数式编程和 Lambda 演算的高阶函数(Higher-order function)有关,据说微积分中的导数也算一种高阶函数,但我这篇文章不是讲数学的(我也不会数学),所以就点到为止。
+## 归约变化
+总结一下,归约函数就是这样的一类函数:
+
+```
+(f result input) -> new result
+```
+
+这样的函数可以相互嵌套使用,不过一般传给 `reduce` 用。
+
+接下来要引入一种新的函数,我没有找到中文译名,暂且称作变约函数(transducer)。它长这样:
+
+```
+(t (f result input)) -> (new-f result input)
+```
+
+Transducer 接收一个归约函数,返回新的归约函数。
+
+返回函数的函数可能有些抽象,但是在 Clojure 中很常见。我举几个例子解释 Transducer 的用法。
+
+我们已经知道 `+` 可以用作归约函数,我们也知道 `map` 可以把一个函数作用到一个集合的每一个函数上。
+
+```clojure
+(reduce + [1 2 3])
+;; => 6
+(map + [1 2 3])
+;; => (2 3 4)
+```
+
+但我们不知道的是,`map` 也可以当成一个 Transducer 来用。如果只传入一个归约函数给 `map`,`map` 就会把这个函数变成作用于集合(而非单条数据)的新归约函数。
+
+```clojure
+(inc 1) ;; inc 作用于单个数字,返回 +1 后的结果
+;; => 2
+
+(def inc-all (map inc)) ;; inc-all 作用于集合,返回每个集合元素 +1 后的结果
+(sequence inc-all [1 2 3])
+;; => (2 3 4)
+```
+
+为什么不能直接 `(inc-all [1 2 3])`,而是要用 `sequence` 函数?不妨试试看直接调用 `inc-all` 会发生什么。
+
+```clojure
+(inc-all [1 2 3])
+;; => #object[clojure.core$map$fn__5950$fn__5951 0x72889280 "clojure.core$map$fn__5950$fn__5951@72889280"]
+
+(type (inc-all [1 2 3]))
+;; => clojure.core$map$fn__5950$fn__5951
+```
+
+看起来 `(inc-all [1 2 3])` 返回的不是序列,而是定义在 `clojure.core/map` 下的函数(`fn`)?要理解为什么,就要先理解 Rich Hickey 引入 Transducer 的时候在想什么。
+
+## 将具体类型与转换算法解耦
+
+Clojure 的哲学是简洁,这点并不只是体现在语法(syntax)上,还体现在语义(semantics)和语言的设计上。Rich Hickey 在《[Simple Made Easy](https://www.infoq.com/presentations/Simple-Made-Easy/)》里讲到,简单(simple)的东西就是不和其他东西交织(complect)在一起的东西。
+
+Java 类不简单,因为它既是命名空间,又是数据结构,还定义了数据上的操作——类(Class)把三个东西交织在了一起。反观 Clojure,命名空间是单独的概念。
+
+标照 Rich 本人对简单的定义,准库里的 `map`、`filter`、`reduce` 等操作序列的函数,都不够简单,因为他们不仅涉及做什么操作(what to do),还涉及了怎么做(how to do it)——预设了惰性,预设了数据操作的顺序(序列都是从前往后线性遍历的),预设了数据的表示方式(返回的是序列)。
+
+如果 `map`、`filter`、`mapcat` 等函数,能够与数据类型(序列)**解耦**呢?也就是说,如果 `map` 只定义做什么操作(what to do) ,把怎么做(how to do it)交给其他组件呢?
+
+比方说,`map` 可以这样实现:
+
+```clojure
+(defn map [f]
+ (fn [f1]
+ (fn [result input]
+ (f1 result (f input)))))
+```
+
+这个 `map` 是构造 Transducer 的函数[^1],它接收函数 `f`,返回了一个返回函数的函数。
+
+最内层的 `(fn [result input])` 就是典型的 `(f x y)` 形式的归约函数。中间的 `(fn [f1])` 接收 `f1` 这个归约函数,返回了最内层的归约函数。在外层的 `mapping` 这个函数定义,返回的就是中间的这个 Transducer。
+
+这个 `mapping` 定义的操作和 `map` 是一样的,要怎么用呢?这是 Rich 举的例子,下面两种写法输出的结果一样:
+
+```clojure
+;; 下面这个函数先是自增 [1 2 3 4] 得到 [2 3 4 5]
+;; 然后再对 [2 3 4 5] 进行累加
+(reduce + 0 (map inc [1 2 3 4]))
+
+;; 下面这个函数用 (map inc) 这个 Transducer
+;; 把 + 这个归约函数,转换成了新的归约函数
+;; 这个新的归约函数的作用是:先给单个元素 +1,然后再累加
+(reduce ((map inc) +) 0 [1 2 3 4])
+```
+
+也就是说,我们将 `(map inc)` 和 `+` 组合,**构造**了新的**数据转换算法**。这里的数据转换算法,就是对做什么(what to do)的定义。
+
+`reduce` 则定义了怎么做(how to do it),因为 `reduce` 是对序列的操作,而且是具有惰性的,返回值是惰性序列。所以把 `reduce` 的怎么做(惰性地做、顺序地做、当作序列做)与 `((map inc) +)` 的做什么(“元素自增后把所有元素累加起来”)组合,就得到了完整的数据操作。
+
+回到一开始的问题,为什么一定要用 `sequence`,不直接用 `inc-all`?因为 `inc-all` 只是抽象的数据转换算法(“自增集合里所有的元素”),但不知道这个集合是什么类型、要不要惰性、最后要返回什么类型。
+
+将 `sequence`(输入和输出数据为序列、惰性、顺序操作)与 `inc-all`(“自增集合里所有的元素”)这些要素组合起来,才能得到完整的数据操作:“自增**序列**里所有的元素**逐一**自增,返回**惰性**序列。”
+
+```clojure
+(sequence inc-all [1 2 3])
+;; => (2 3 4)
+```
+
+如果不想用序列和惰性序列,可以换成向量或者集合:
+
+```clojure
+(into [] inc-all [1 2 3])
+;; => [2 3 4]
+
+(into #{} inc-all [1 2 3])
+;; => #{4 3 2}
+```
+
+如果没有用 Transducer 把 `inc` 包装成 `inc-all`,我们就必须先把向量变成序列,然后再把序列变回向量。因为 `map` 和许多函数都只操作序列和惰性序列。
+
+```clojure
+(map inc [1 2 3])
+;; => (2 3 4) ; <- 这里得到的是序列,而非向量
+
+(vec (map inc [1 2 3])) ; <- 必须手动转换成向量!
+;; => [2 3 4]
+
+(into [] (map inc) [1 2 3]) ; <! Transducer 不关心数据类型
+;; => [2 3 4]
+```
+
+上一篇提到的 *channel* 也可以接收 Transducer,携带 Transuder 的 *channel* 会对经过它的所有数据做变换。
+
+```clojure
+(def growing-channel (chan 1 (map inc)))
+;; => #'user/growing-channel
+(put! growing-channel 1)
+;; => true
+(poll! growing-channel)
+;; => 2
+```
+
+Transducer 的好处就是,由于他只定义了抽象的「做什么」,而不关系具体的「怎么做」,所以可以非常轻易地用在任何地方,不像 `map` 和 `filter` 只能作用于序列(或者要把向量、集等其他数据类型转换为序列)。
+
+## 组合 Transducer
+
+一般来说,要做 Clojure 里做一系列数据操作,会组合使用很多序列函数。
+
+```clojure
+(->> [1 2 3 4 5 6 7 8]
+ (filter odd?)
+ (map #(* 2 %))
+ (reduce +))
+;; => 32
+```
+
+有了 Transducer,我们可以把这一系列操作抽象出来,定义成一个独立的、可复用的操作。
+
+```clojure
+(def double-odd-numbers
+ (comp
+ (filter odd?)
+ (map #(* 2 %))))
+
+; 可以用来累加
+(transduce double-odd-numbers + [1 2 3 4 5 6 7 8])
+;; => 32
+
+; 可以不加,返回集合
+(into #{} double-odd-numbers [1 2 3 4 5 6 7 8])
+;; => #{6 2 14 10}
+
+; 如果你想,还能交给 channel
+(def double-odd-chan (chan 1 double-odd-numbers))
+```
diff --git a/content/article/tech-witchhunt.md b/content/article/tech-witchhunt.md
deleted file mode 100644
index 65ac3ae5..00000000
--- a/content/article/tech-witchhunt.md
+++ /dev/null
@@ -1,13 +0,0 @@
-+++
-title = '技术猎巫'
-subtitle = ''
-slug = ''
-tags = []
-categories = ''
-date = 2026-09-28T08:59:01+08:00
-draft = true
-toc = true
-+++
-
-猎巫者至少比一点也不关心社区里发生了什么的无知者好…… 对吗?
-