summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authoreltrac <hi@eltr.ac>2026-09-29 16:24:58 +0800
committereltrac <hi@eltr.ac>2026-09-29 16:24:58 +0800
commit5ba80c77a9459614afcb3e8373fd5108a4fab0ca (patch)
tree6cf8c4d4042f72925186d92f86b220b97e62b447
parentb1b80820c21484ddd4fffc134d8f2104cf6b0c65 (diff)
[draft] updates
-rw-r--r--content/article/clj4.md139
-rw-r--r--content/article/tech-witchhunt.md13
-rw-r--r--content/weekly/100.md2
3 files changed, 153 insertions, 1 deletions
diff --git a/content/article/clj4.md b/content/article/clj4.md
new file mode 100644
index 00000000..b5637a13
--- /dev/null
+++ b/content/article/clj4.md
@@ -0,0 +1,139 @@
++++
+title = 'Coffee Break Clojure, Vol.4'
+subtitle = 'Functioanlly Lazy'
+slug = 'clj4'
+tags = ["Lisp","编程"]
+categories = '茶歇蔻芥'
+date = 2026-09-29T14:07:57+08:00
+draft = true
+toc = true
++++
+
+今天的文章衔接[上一期](/article/clj3/)有关集合(collection)和序列(sequence)的知识,来讨论一种特殊的集合,叫惰性序列(lazy seq)。我们还会接触到函数式编程(functional programing)。
+
+其实在另一门名为 [Haskell](https://www.haskell.org/) 的语言里,也能见到惰性,而 Haskell 是纯函数式的编程语言(不像 Clojure 有很多面向对象的成分)。函数式和惰性非常契合,可以说没了惰性,函数式就不够务实,会消耗更多的计算资源。<!--more-->
+
+## 编程范式
+
+编程范式(programming paradigm)可以理解为编程方法论。用我的方法解释的话,一套编程范式提供了一系列高度抽象的逻辑组件、通用的程序结构,比方说 `for` 循环结构就是[结构化编程](https://zh.wikipedia.org/wiki/%E7%BB%93%E6%9E%84%E5%8C%96%E7%BC%96%E7%A8%8B)(structrual programing)这一范式提出的。
+
+结构化编程的主要主张是 `goto` 语句有害论,由 Dijkstra 在 1968 年提出。这在当时颇有争议,不过如今已成公理,如今的新来者可能根本不会听说 `goto` 语句。我也从来没写过,我了解到 `goto` 的时候,我的 C 语言老师就严肃地告诉我们不该用 `goto`。
+
+据 Robert C. Martin 在《[架构整洁之道](/library/2026/clean-architecture/#编程范式设计原则和组件原则)》里总结,结构化编程本质是上限制了**程序控制权的直接转移**,即不允许用 `goto` 语句直接变更程序的执行位置。要转移程序的执行位置,只能用**块结构**[^1]表示条件判断、循环体等等。块结构使得程序可以被清晰地划分为可理解的部分,而不是一条一条分散的代码。
+
+面向对象编程(object-oriented programing,后称 OOP)在块结构之上提出了新的组织程序的方式,也就是类。程序员经常需要定义一种数据类型,然后定义对这种数据类型的操作(封装);对不同的类型和不同的输入而言,相同的操作可能有不同的行为(多态);数据类型的某些操作是共通的,可以复用(继承)。OOP 引入了更高层的概念和结构。据 Martin 所言,OOP 本质上是限制了对程序控制权的**间接**转移,因为 OOP 用方法(method)代替了函数指针(function pointer)。[^2]
+
+Martin 认为编程范式并没有引入新的东西,反而是提出了更多的限制,比如限制 `goto` 语句和限制函数指针。函数式编程(后称 FP)范式引入的限制是:禁止赋值语句。是的,==不允许给变量赋值==,所以在前四篇里我们一次都没有见到 Clojure 的赋值语句,只见到了定义符号的语句。
+
+为什么?因为给变量赋值就意味着我们在维护一个**可变的状态**,而状态在 FP 看来是个坏东西:
+
+1. 如果系统有多个状态,复现 Bug 时,需要复原系统中所有的状态。
+2. 有多个读者和写者时,为保证数据一致性和完整性,需要加锁,而维护锁很痛苦[^3]。更何况锁本身也是一个新的状态!
+ - 说到这里,容许我插一嘴,我前不久修复了一个非常诡异的 Bug,最后我们发现问题出在系统里某个地方加了锁。这个锁是外部的某个库引入的,我们完全不知道那个锁的存在。当时系统出现了大量的超时错误,我们以为是网络阻塞,结果竟然是大量请求等待锁导致的。
+3. 管理状态如何在系统的各个部分之间同步和转移,定义状态如何变更,检验状态的有效性等等,都会极大提升系统的复杂度。
+
+这其实也在逐渐成为共识,我们看到很多编程语言(比如 Rust)的变量默认不可变(immutable)。状态更少会让复杂系统更容易维护,而是 FP 则致力于消除可变状态。
+
+不过整体而言,FP 并不主流。不少人对 Haskell 的第一印象是「学术语言」,尽管 Haskell 的标语是「Enjoy long-term maintainable software you can rely on」(享受你可以依靠的长期可维护的软件),显然是出于工程考虑写下的标语。
+
+在过去,FP 受到的不认可往往源自于它的性能问题。由于 FP 系统不维护状态,而是在函数的相互调用之间传递参数,循环结构也往往用递归(recursion)表示,这就导致处理大量数据时,函数栈帧会占用大量内存,而且可能出现栈溢出问题。不过这个问题现在已就有了应用广泛的解决方案,叫作尾部调用优化(Tail Call Optimization,TCO)——简单来说,只要递归函数调用发生在函数尾部,编译器或解释器就可以复用上一个递归调用的栈帧,把递归调用变成循环结构,但语言层面仍然是递归写法。
+
+## 深入函数式
+
+FP 不仅禁止赋值语句,还避免副作用(side effectse)。FP 把函数想象成和数学函数一样的东西,输入值,然后输出值,这其间不该发生别的事情,比如不该修改系统中其他的状态。
+
+那 FP 程序怎么和外部设备交互呢?照其定义,I/O 操作也是副作用,我还不能把数据写入磁盘了吗?其实,一般只有**纯**函数式语言才会想要完全消除副作用,Haskell 是通过引入 [IO 单子](https://zh.wikipedia.org/wiki/%E5%8D%95%E5%AD%90_(%E5%87%BD%E6%95%B0%E5%BC%8F%E7%BC%96%E7%A8%8B)#IO%E5%8D%95%E5%AD%90%EF%BC%88Haskell%EF%BC%89)的概念消除副作用的。Haskell 把对外部状态的访问和操作当作显式的作用,而非副作用。至于 Clojure,它并不是**纯**函数式语言。副作用是允许的,只是不鼓励。Scheme 也容忍副作用,因此 Scheme 更喜欢用 *procedure*(过程)这个词表示 *function*,因为有副作用的不是纯函数。
+
+与副作用相关的概念是引用透明性([referential transparency](https://en.wikipedia.org/wiki/Referential_transparency)),可以说避免副作用就是为了实现引用透明性。以下面这段 Clojure 代码为例:
+
+```clojure
+(+ (- 3 1) 4)
+;; => 6
+
+;; 上述代码应该和下面这段代码等价
+(+ 2 4)
+;; => 6
+
+;; 上述代码应该和下面这段代码等价
+6
+;; => 6
+```
+
+==函数调用可以显式地替换为它的返回值==,这就是引用透明性。对数学运算来说这理所应当,FP 要求的就是把这种「理所应当」也应用在其他计算机程序上。没有引用透明性的函数是什么样的?
+
+```js
+var x = 0
+function add(a, b) {
+ x = x + 1
+ return a + b
+}
+
+add(add(x, x), x)
+// => 1
+```
+
+上述 `add()` 是个闭包函数(因为它访问了外部作用域的 `x` 变量),每执行一次 `add()` 都悄悄地给 `x` 加 `1`,然后再返回 `a + b`。
+
+执行 `add(add(x, x), x)` 的结果是 `1`,因为调用最外层的 `add()` 之前,我们先要得到参数的值。第一个参数是函数调用,求出值为 `0`,同时 `x` 的状态变成了 `1`[^4];然后将 `0` 作为第一个参数传入,此时求 `x` 的值,发现是 `1`,将 `1` 作为第二个参数传入,也就是 `add(0, 1)`。最后函数返回了 `1`,`x` 的值变成了 `2`。
+
+假设现在 `x` 的值还是 `0`,我们把 `add(x, x)` 换成理应等价的 `0`,发现 `add(0, x)` 得到的是 `0` 而不是 `1`。这个程序没有引用透明性,而且 `x` 的状态不可控,`add()` 的结果难以预测。
+
+不难意识到避免副作用对系统的长期可维护性的好处。如果我们非要一个 `x` “变量”作为计数器,记录调用 `add()` 函数的次数,不妨用递归实现。
+
+```js
+const numbers = [2, 4, 3, 7, 2, 7, 5, 10, 3]
+
+function addWithCounter(numbers, counter, total, index) {
+ if (numbers.length - 1 < index) {
+ return {counter: counter, total: total}
+ }
+ return addWithCounter(numbers, counter+1, total + numbers[index], index+1)
+}
+
+addWithCounter(numbers, 0, 0, 0)
+// => {counter: 9, total: 43}
+```
+
+上面这段代码是函数式的。`const` 是常量声明,而不是赋值。在 `addWithCounter()` 中我们只是返回计算后的值,将某些“状态”作为函数参数递归地传递,没有可变状态。我们在不使用循环结构的情况下遍历累加了数组,并且维护了一个 `counter`(计数器)。
+
+在 Clojure 中,可以用 `loop` 构造递归,无需声明函数:
+
+```clojure
+(defn addWithCounter [numbers]
+ (loop [counter 0, total 0, index 0]
+ (if (< (count numbers) (inc index))
+ {:counter counter :total total}
+ (recur (inc counter) (+ total (numbers index)) (inc index)))))
+
+(addWithCounter [1 2 3])
+;; => {:counter 3, :total 6}
+```
+
+不过更 *Idiomatic* 的写法是:
+
+```clojure
+;; reductions 类似于 reduce
+;; 但返回的是每个步骤的 lazy seq,而不是最终结果
+;; (= (reduce f coll) (last (reductions f coll)))
+(defn addWithCounter [numbers]
+ (let [steps (reductions + numbers)]
+ {:counter (count steps)
+ :total (last steps)}))
+
+(addWithCounter [1 2 3])
+;; => {:counter 3, :total 6}
+```
+
+`reductions` 函数返回的是惰性序列,其实上一篇提到的 `map` 返回的也是惰性序列。这究竟是什么东西?
+
+## 初识惰性
+
+## Footnotes
+
+[^1]: 比如 C 语言中 `{}` 包裹的块。
+
+[^2]: 不过我对这个说法存疑,因为 C++ 作为 OOP 语言,甚至有方法指针(method pointerp)这种东西。
+
+[^3]: Go 语言的 `channel` 其实用 FP 以外的方式解决了这个问题
+
+[^4]: 为什么这个 `add(x, x)` 没有受到 `x = 1` 的影响?因为函数的参数是作为**值**传入的,不是变量引用。`add()` 接收到的就是两个 `0` 而不是两个 `x`。
diff --git a/content/article/tech-witchhunt.md b/content/article/tech-witchhunt.md
new file mode 100644
index 00000000..65ac3ae5
--- /dev/null
+++ b/content/article/tech-witchhunt.md
@@ -0,0 +1,13 @@
++++
+title = '技术猎巫'
+subtitle = ''
+slug = ''
+tags = []
+categories = ''
+date = 2026-09-28T08:59:01+08:00
+draft = true
+toc = true
++++
+
+猎巫者至少比一点也不关心社区里发生了什么的无知者好…… 对吗?
+
diff --git a/content/weekly/100.md b/content/weekly/100.md
index 5562797f..6f32ca55 100644
--- a/content/weekly/100.md
+++ b/content/weekly/100.md
@@ -1,7 +1,7 @@
+++
title = "大脑充血 Vol.100"
subtitle = "小游戏特辑"
-tags = ["游戏"]
+tags = ["小报","游戏"]
date = 2026-09-28 08:44:16
draft = false
toc = true