summaryrefslogtreecommitdiff
path: root/content/article/compute-infinity.md
diff options
context:
space:
mode:
authoreltrac <hi@eltr.ac>2026-03-17 21:03:21 +0800
committereltrac <hi@eltr.ac>2026-03-17 21:03:21 +0800
commitff62e46609ca7f63663eaf07e48150ae37dba806 (patch)
tree9c875f1de42d3232918aeae227a8c7b6b7752bc1 /content/article/compute-infinity.md
parent76d0aa6c6356e54d5bc742e0e32777298bae022c (diff)
docs: new draft
Diffstat (limited to 'content/article/compute-infinity.md')
-rw-r--r--content/article/compute-infinity.md128
1 files changed, 128 insertions, 0 deletions
diff --git a/content/article/compute-infinity.md b/content/article/compute-infinity.md
new file mode 100644
index 00000000..bfe667ed
--- /dev/null
+++ b/content/article/compute-infinity.md
@@ -0,0 +1,128 @@
++++
+title = '计算无穷'
+subtitle = 'Clojure 中的惰性序列'
+slug = ''
+tags = ['Lisp']
+categories = '代码炼金术'
+date = 2026-03-17T20:01:03+08:00
+draft = true
+toc = true
++++
+
+由于计算机算法的特性之一是有穷性,一切能被计算机计算的东西都必须是能够在优先步骤内完成的,所以,计算机大概是不能够应对无穷长的数据的。
+
+真的是这样吗?<!--more-->
+
+## 穿针引线
+
+在正式介绍 Clojure 这门语言如何操作无穷之前,我想先介绍两个非常好用的宏,`->` 和 `->>`,因为我会在接下来的演示代码中非常频繁地使用它们。这两个宏的名字分别叫作 thread-first 和 thread-last。它们是用来做什么的呢?
+
+首先,在任何编程语言中(哪怕不是 Lisp),都有可能遇到连续对数据进行计算的情况,一段计算完成之后,需要把结果传递给下一段代码继续操作,一般来说需要嵌套函数。假设我们需要解析一段 Markdown 文本,而我们采取的策略是,使用一个函数解析所有的标题,使用另一个函数解析块引用,再使用一个函数解析列表,以此类推,我们可能会这样写:
+
+```go
+func parseMarkdown(raw string) string {
+ return parseLists(parseBlockquotes(parseHeadings(raw)))
+}
+```
+
+显然,这种代码可读性很差。大多数时候,我们可能会将某一段计算的结果存放在变量里,再把变量的值传递给下一个函数,以此提升可读性。
+
+```go
+func parseMarkdown(raw string) string {
+ result1 := parseHeadings(raw)
+ result2 := parseBlockquotes(result1)
+ result3 := parseLists(result2)
+ return result3
+}
+```
+
+上面这段代码可读性就高很多了。你可能觉得这样很蠢,怎么会有人这样写解析器?没关系,就算不是解析器,也可能会出现类似结构的代码,比如一开始传入一个文件路径,读取这个路径下文件的内容,再把文件内容(假设是 JSON 字符串)解析为数据对象,再提取数据中的某个值。
+
+这种做法看起来并不少见,所以能否简化呢?有没有办法让我们不需要把数据放进变量里,再取出来,再传给下一个参数,而是能够直接把所有的操作都串起来呢?
+
+用 thread-first 和 thread-last 就能做到。
+
+```clojure
+(defn parseMarkdown [raw]
+ (-> raw
+ parseHeadings
+ parseBlockquotes
+ prseLists))
+```
+
+视觉上,这很像类 Unix 系统上常用的管道(Pipes),即 `|` 符号。在输入命令行的时候,可以用 `|` 把两个命令串联起来,使 `|` 之前的命令的结果传递给之后的命令。假设你需要读取 `foo.txt` 这个文件的内容,并查找其中包含 `bar` 的行,就可以这样写。
+
+```sh
+cat foo.txt | grep bar
+```
+
+使用 `->` 可以让代码变得简洁,比如,假如你想要读取一个 JSON 文件的内容,这个文件不大,你想直接加载到内存里并直接转换为 Clojure 的数据结构,就可以这样写:`(-> filename slurp json/read-string)`。
+
+thread-first 和 thread-last 的区别是什么呢?刚刚我们演示的函数都只接受一个参数,而 first 和 last 指的是把上一个函数运算的结果传给下一个函数的第一个参数还是最后一个参数,所以并没有体现出区别。
+
+假设我们要操作的数据是集合,比如列表和向量,而操作这类数据的函数往往在最后一个参数的位置接收集合(比如 `(map f coll)` 的作用是对集合 `coll` 中的每一个元素执行 `f` 操作,然后返回一个惰性序列,集合 `coll` 就是最后一个参数),如果使用 thread-first,也就是 `->` 宏,集合就会我传入第一个参数,也就是被当成函数 `f` 而非 `coll`。
+
+假设我们有一个装满了狗狗的向量,我们要从所有的狗狗中找出听话的好狗狗,并给好狗狗每只狗一块零食吃,就可以这样写:[^1]
+
+```clojure
+(defn treat-good-dogs [dogs]
+ (->> dogs
+ (filter :good)
+ (map give-treat)
+ doall))
+```
+
+如果对上面的代码进行宏展开,就会得到:
+
+```clojure
+(defn treat-good-dogs [dogs]
+ (doall (map give-treat (filter :good dogs))))
+```
+
+不过,为什么在最后加上 `doall` 呢?这是做什么的?
+
+## 惰性的魔力
+
+假设我们有无穷只狗狗,但我们的零食数量是有限的,要怎么办呢?我们当然没办法满足所有的好狗狗,在零食发放完之后就不能继续操作了。这要怎么写呢?你可能会想,需要一个整数型的变量来表示零食的数量,每发出去一块零食就减 `1`,当变量值为 `0`,就停下,终止操作。
+
+用不着这么麻烦,我们可以直接对无穷进行操作,直接在无穷的狗狗里筛选好狗狗,再取出前 `n` 个好狗狗(`n` 为零食的数量),给他们零食就好了。
+
+假设 `dogs` 是一个无穷的序列,也就是说用无穷无尽的狗狗们在等待着发放零食(天哪,光是写下来就觉得可爱爆了)。那么,下面这段代码会陷入无限循环吗?
+
+```clojure
+(->> dogs
+ (filter :good)
+ (map give-treat))
+```
+
+不会,因为 `filter` 和 `map` 返回的是惰性序列(Lazy sequence)。
+
+简单来说,只要我们不主动读取惰性序列中的数据,相关的操作就永远不会被计算。比如 `(filter :good dogs)` 这一步,虽然写法很优雅,可以直接读作「filter good dogs」(筛选好狗狗),但 Clojure 实际上并没有真的去筛选好狗狗,而是承诺:*下次,下次一定,之后我就把这件事情办了。*
+
+下一步 `(map give-treat ...)` 也是一样,Clojure 还没有「给每一只好狗狗一块零食」,而是做出了「我之后就会给每只好狗狗一块零食」的承诺。
+
+这就是为什么之前我们要用到 `doall`,这个函数的效果是执行惰性序列中的所有效果。如果没有这个函数,我们就只是做出了承诺,而没有真的去执行。
+
+当然,现在的情况是,我们得到的惰性序列是无穷长的(无穷的狗狗中好狗狗的数量自然也是无穷),当然不能 `doall`,因为这样做资源会很快被消耗光,我们没有那么多零食。在计算机系统的视角下,我们也没有那么多的内存和处理器资源可以分配。
+
+假设我们有 `n` 个零食,我们可以这样写:
+
+```clojure
+(->> dogs
+ (filter :good)
+ (map give-treat)
+ (take n)
+ doall)
+```
+
+上面这段代码的意思是:
+
+1. 我们有无穷的狗狗
+2. 承诺之后会筛选出其中的好狗狗
+3. 承诺之后会给筛选出的好狗狗零食吃
+4. 取出前 n 个承诺
+5. 执行这些承诺
+
+## 实践惰性
+
+[^1]: 假设「好狗狗」的数据结构长这样 `{:good true}`,这是 Clojure 中映射的写法,可以用关键词作为操作取出其映射的值,比如 `(:good {:good true})` 就会返回 `true`。