diff options
| author | eltrac <hi@eltr.ac> | 2026-09-29 16:24:58 +0800 |
|---|---|---|
| committer | eltrac <hi@eltr.ac> | 2026-09-29 16:24:58 +0800 |
| commit | 5ba80c77a9459614afcb3e8373fd5108a4fab0ca (patch) | |
| tree | 6cf8c4d4042f72925186d92f86b220b97e62b447 | |
| parent | b1b80820c21484ddd4fffc134d8f2104cf6b0c65 (diff) | |
[draft] updates
| -rw-r--r-- | content/article/clj4.md | 139 | ||||
| -rw-r--r-- | content/article/tech-witchhunt.md | 13 | ||||
| -rw-r--r-- | content/weekly/100.md | 2 |
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 |
