summaryrefslogtreecommitdiff
path: root/content
diff options
context:
space:
mode:
Diffstat (limited to 'content')
-rw-r--r--content/library/2026/functional-design.md66
1 files changed, 61 insertions, 5 deletions
diff --git a/content/library/2026/functional-design.md b/content/library/2026/functional-design.md
index ed81c808..0a83cfde 100644
--- a/content/library/2026/functional-design.md
+++ b/content/library/2026/functional-design.md
@@ -1,7 +1,7 @@
+++
title = 'Functional Design'
-slug = ''
-tags = []
+slug = 'functional-design'
+tags = ['Lisp','编程']
authors = ['Robert C. Martin']
rating = 3.5
date = 2026-05-14T11:35:24+08:00
@@ -27,7 +27,7 @@ toot = ''
状态可变会导致很多问题。首先是时序耦合(Temperal Coupling),即代码执行的先后顺序会影响程序的行为。
-> It is something you are probably quite familiar with. `Open` must be called bfore `close`. `New` must be called before `delete`. `Malloc` must be called before `free`. The list of pairs like this is endless. And in many ways, they are a bane of our existence.
+> It is something you are probably quite familiar with. `Open` must be called before `close`. `New` must be called before `delete`. `Malloc` must be called before `free`. The list of pairs like this is endless. And in many ways, they are a bane of our existence.
你有多少次发现某个程序问题只需要调换两个函数调用的顺序就能解决?人们熟悉的垃圾回收机制(Garbage Collection)就是因为人们管理不好时序耦合、管理不好已经分配的内存,才会被广为接受。
@@ -35,17 +35,73 @@ toot = ''
如果状态不可变,上述问题都不会发生。
-> You can't have race condition if you don't mutate state.
+> You can't have race condition if you don't mutate states.
简而言之,函数式编程就是禁止(或者至少在极大程度上限制)赋值操作的编程。函数式语言里的变量实际上不可变,只能被初始化而不能被赋值,它们是常量。函数式程序不改变状态,状态变更是在一个接一个的递归函数调用中以参数的形式被传递的,过往的状态不会改变,我们只创建新的状态。
+对于不熟悉函数式编程的读者,可以想象结构化编程里的循环结构,以下是 Go 语言的例子:
+
+```go
+func IncrementList(list []int) {
+ var newlist []int
+ for _, num := range list {
+ newnum := num + 1
+ newlist = append(newlist, newnum)
+ }
+ return newlist
+}
+```
+
+上面这个函数接收一个整数型 Slice(可以理解为数组),返回所有元素 +1 过后的新 Slice。显然,每次循环都会给 newlist 进行一次赋值。
+
+以下是用 Clojure 编写的,函数式的例子:
+
+```clojure
+(defn inc-list [int-list]
+ (map inc int-list))
+```
+
+写完了,甚至不需要定义函数,`(map inc int-list)` 本身就足够清晰了。
+
+如你所见,函数式编程往往不使用 `for` 这样的结构遍历集合数据,然后对集合中的每个元素(for each)做操作,而是直接操作集合本身。你也可以这样理解,`for` 循环其实是用一个迭代器(iterator)访问集合中的数据,而 `map` 是通过一个函数和一组数据构造了一个迭代器,并且这个迭代器不是用来访问数据的,而是用来直接得到新的数据(或者说直接访问结果)。
+
+如果你想自增后删掉大于等于 10 的元素,Go 语言的写法需要你在 `for` 循环里写上 `if`,而 Clojure 只需要这样。
+
+```clojure
+(->> int-list
+ (map inc)
+ (remove #(>= % 10)))
+```
+
+这甚至很自然地形成了一个职责链。职责链(chain-of-responsibility)是一种软件设计模式,简单来说就是由一个接一个的处理器处理同一个请求,每个处理器有各自的职责。这种设计模式的好处之一是,处理器可以随时替换、拓展和移除(甚至拦截和过滤),同时符合单一职责原则(SRP)和开闭原则(OCP)。*什么?你说职责链里的处理对象应该实现 `Handler` 接口?可这里的处理对象都是函数啊,它们本来就是同一个类型。*
+
+
+函数式编程中,函数是一等公民,和面向对象编程中的类和接口处于同等地位。
+
+实际上这两种方式在具体的执行层面区别不大,但我们拥有了更具**表现力**(expressiveness)的程序结构。
+
## 在有限图灵机上作弊
-邱奇-图灵论题(Church–Turing thesis)表明递归、λ 演算和图灵机是等价的,即 λ 表达式和图灵机程序(也就是任何用图灵完备的编程语言编写的程序)可以相互转换。然而,我们使用的计算机是有限图灵机(finite Turing machine),函数式程序是看起来像 λ 表达式的程序,但实际上是在图灵机上实现的。为了实现这种编程风格,我们需要作弊。
+邱奇-图灵论题(Church–Turing thesis)表明递归、λ 演算和图灵机是等价的,即 λ 演算(lambda calculus)和图灵机程序(也就是任何用图灵完备的编程语言编写的程序)可以相互转换。然而,我们使用的计算机是有限图灵机(finite Turing machine),函数式程序是看起来像 λ 演算的程序,但实际上是在图灵机上实现的。为了实现这种编程风格,我们需要作弊(cheat)。
首先是尾部调用优化(TCO,Tail Call Optimization),因为函数式编程使用递归而非循环(如果想禁止赋值语句,那就必须递归)。如果递归的次数太多,函数调用栈就会占用大量的内存,比方说,在遍历一个很长的列表的时候。如果递归调用在函数的尾部,函数式编程语言的编译器就会优化这段代码,新的递归不会创建新的栈帧(Stack Frame),而是用 `goto` 语句跳转到函数顶部,复用当前的栈帧。
这实际上还是在使用循环,从执行的层面来看,这个程序和那些使用结构化编程的程序没什么区别,但从源代码来看,递归的方法就是函数式的。所以函数式编程是一种风格,出于各种现实的原因,程序实际上还是顺序执行。
+很多时候,函数式程序的底层都不是纯函数的,我们只是把状态变更隔离开来,让大部分的程序看起来是函数式的。纯函数式不够务实,比方说,I/O 操作一定涉及到状态变化,如果要往屏幕上展示东西,就不得不改变显示器的状态;要解决实际问题,我们也不得不并发,不得不存储状态,不得不面对竞争条件。
+
+这么说来,函数式编程是偏执吗?我们知道自己在作弊,也知道世界不是纯函数的,计算机程序也不需要具有数学美感,采用 OO 不是更务实吗?至少在读到这本书的前四分之一时,我发出了这样的疑问。尽管我很快迷上了 FP 并对啰唆的 OOP 颇有微词,但看起来 FP 既不能在图灵机上自如地运行(不优化的话会严重耗费计算资源),也不能完美地解决实际问题(现实是可变的,也很难避免并发),再者,函数式编程语言中有那么多 Lisp 方言[^1],一堆一堆的括号也吓跑了好多人。
+
+我不知道怎么回答,我只能说,函数式编程确实很好玩。单从开发体验来看,比起一门静态类型的面向对象编程语言,我更喜欢用动态类型的函数式编程语言。
+
+*噢等等,我说了动态类型对吧?*
+
+## 论类型安全
+
+
+
+## FOOL
+## Footnotes
+[^1]: 非 Lisp 的函数式编程语言有 Earlang、Haskell 和 F# 等。此外,很多函数式编程语言并不是「纯函数式」的,它们在某种程度上支持可变性,甚至支持面向对象。