函数式编程艺术 / 01 函数式编程导论
01 函数式编程导论
“函数式编程是一种编程范式,它将计算视为数学函数的求值,并避免改变状态和可变数据。”
1.1 函数式编程的历史
函数式编程(Functional Programming, FP)的根基可以追溯到 1930 年代,比电子计算机的出现还要早。
1.1.1 关键时间线
| 年份 | 事件 | 人物 |
|---|---|---|
| 1930 | Lambda 演算(Lambda Calculus)提出 | Alonzo Church |
| 1936 | 图灵机提出,与 Lambda 演算等价 | Alan Turing |
| 1958 | LISP 语言诞生,首个 FP 语言 | John McCarthy |
| 1973 | ML 语言诞生,引入类型推断 | Robin Milner |
| 1987 | Haskell 标准化委员会成立 | 多位学者 |
| 1990 | Haskell 1.0 发布 | Haskell Committee |
| 1995 | JavaScript 诞生,浏览器中的 FP | Brendan Eich |
| 2003 | Erlang 在电信领域大规模应用 | Ericsson |
| 2007 | Clojure 诞生,JVM 上的 Lisp | Rich Hickey |
| 2010 | Scala 开始流行,JVM 上的 FP+OOP | Martin Odersky |
| 2015 | ES6 引入箭头函数、解构,JS FP 化 | ECMA |
| 2020s | Rust、Kotlin、Swift 大量采用 FP 特性 | 社区 |
1.1.2 两大流派
函数式编程有两个主要流派:
| 流派 | 代表语言 | 特点 |
|---|---|---|
| 纯函数式 | Haskell, Elm | 无副作用,引用透明,强类型系统 |
| 多范式函数式 | JavaScript, Python, Rust, Clojure | FP 与 OOP/命令式混合,实用导向 |
1.2 Lambda 演算
Lambda 演算是函数式编程的数学基础,由 Alonzo Church 在 1930 年代提出。
1.2.1 基本语法
Lambda 演算只有三种表达式:
<表达式> ::= <变量> -- x, y, z
| λ<变量>.<表达式> -- λx. x(函数定义)
| <表达式><表达式> -- f x(函数应用)
1.2.2 在各语言中的对应
Lambda 演算的 λ 抽象对应各语言的匿名函数:
Haskell:
-- Lambda 演算: λx. x + 1
\x -> x + 1
-- 命名函数
addOne x = x + 1
JavaScript:
// Lambda 演算: λx. x + 1
const addOne = x => x + 1;
// 等价的匿名函数
const addOneAnon = function(x) { return x + 1; };
Python:
# Lambda 演算: λx. x + 1
add_one = lambda x: x + 1
# 等价的命名函数
def add_one(x):
return x + 1
Rust:
// Lambda 演算: λx. x + 1
let add_one = |x: i32| x + 1;
// 函数指针
fn add_one_fn(x: i32) -> i32 {
x + 1
}
Clojure:
;; Lambda 演算: λx. x + 1
(fn [x] (+ x 1))
;; 简写
#(+ % 1)
1.2.3 Church 编码
Church 编码展示了如何仅用函数表示数据:
-- Church 编码:用函数表示自然数
-- 0 = λf. λx. x
-- 1 = λf. λx. f x
-- 2 = λf. λx. f (f x)
type Church = (a -> a) -> a -> a
churchZero :: Church
churchZero = \f x -> x
churchSucc :: Church -> Church
churchSucc n = \f x -> f (n f x)
churchToInt :: Church -> Int
churchToInt n = n (+1) 0
-- 2 的 Church 编码
two :: Church
two = \f x -> f (f x)
-- 加法
churchAdd :: Church -> Church -> Church
churchAdd m n = \f x -> m f (n f x)
1.3 函数式编程的核心特征
1.3.1 五大核心原则
| 原则 | 说明 | 重要性 |
|---|---|---|
| 纯函数 | 相同输入永远产生相同输出,无副作用 | ★★★★★ |
| 不可变性 | 数据一旦创建不可修改 | ★★★★★ |
| 函数是一等公民 | 函数可作为值传递、返回、存储 | ★★★★★ |
| 声明式风格 | 描述"做什么"而非"怎么做" | ★★★★☆ |
| 类型系统 | 用类型约束保证正确性 | ★★★★☆ |
1.3.2 函数式 vs 命令式
命令式风格(Imperative):
// 计算偶数的平方和
const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
let result = 0;
for (let i = 0; i < numbers.length; i++) {
if (numbers[i] % 2 === 0) {
result += numbers[i] * numbers[i];
}
}
// result = 220
函数式风格(Functional):
// 同样的计算,函数式风格
const result = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
.filter(n => n % 2 === 0)
.map(n => n * n)
.reduce((sum, n) => sum + n, 0);
// result = 220
对比表:
| 维度 | 命令式 | 函数式 |
|---|---|---|
| 关注点 | 如何做(How) | 做什么(What) |
| 状态 | 可变状态 | 不可变数据 |
| 控制流 | 循环、条件 | 递归、高阶函数 |
| 副作用 | 大量副作用 | 隔离副作用 |
| 可变性 | 变量可变 | 值不可变 |
| 并发 | 需要锁 | 天然适合并发 |
1.3.3 多语言对比示例
同一个问题——“统计文本中每个单词出现的次数”:
Haskell:
import qualified Data.Map as Map
import Data.Char (toLower)
wordCount :: String -> Map.Map String Int
wordCount = foldl (\acc word -> Map.insertWith (+) word 1 acc) Map.empty
. words
. map toLower
-- wordCount "hello world hello" == Map.fromList [("hello",2),("world",1)]
JavaScript:
const wordCount = text =>
text.toLowerCase().split(/\s+/).reduce((counts, word) => {
counts[word] = (counts[word] || 0) + 1;
return counts;
}, {});
Python:
from collections import Counter
from functools import reduce
def word_count(text):
return Counter(text.lower().split())
# 或者纯函数式版本
def word_count_fp(text):
words = text.lower().split()
return reduce(
lambda acc, w: {**acc, w: acc.get(w, 0) + 1},
words,
{}
)
Rust:
use std::collections::HashMap;
fn word_count(text: &str) -> HashMap<String, usize> {
text.to_lowercase()
.split_whitespace()
.fold(HashMap::new(), |mut acc, word| {
*acc.entry(word.to_string()).or_insert(0) += 1;
acc
})
}
Clojure:
(defn word-count [text]
(->> (clojure.string/split (clojure.string/lower-case text) #"\s+")
(frequencies)))
;; (word-count "hello world hello")
;; => {"hello" 2, "world" 1}
1.4 与命令式编程的详细对比
1.4.1 控制流对比
| 命令式 | 函数式等价 |
|---|---|
for 循环 | map, reduce, 递归 |
while 循环 | 尾递归, unfold |
if/else | 模式匹配, 三元表达式 |
switch/case | 模式匹配, 穷尽性检查 |
try/catch | Either, Result, Option |
| 可变变量 | fold, scan, let 绑定 |
1.4.2 状态管理对比
命令式(可变状态):
class BankAccount {
constructor(balance) {
this.balance = balance; // 可变状态
}
deposit(amount) {
this.balance += amount; // 修改内部状态
return this;
}
withdraw(amount) {
if (amount > this.balance) throw new Error('Insufficient funds');
this.balance -= amount;
return this;
}
}
函数式(不可变状态):
const BankAccount = (balance) => ({
balance,
deposit: (amount) => BankAccount(balance + amount), // 返回新对象
withdraw: (amount) => {
if (amount > balance) throw new Error('Insufficient funds');
return BankAccount(balance - amount);
}
});
1.4.3 并发安全性
| 场景 | 命令式 | 函数式 |
|---|---|---|
| 多线程读 | 需要锁/同步 | 天然安全(不可变) |
| 多线程写 | 需要互斥锁 | 返回新数据,无需锁 |
| 死锁风险 | 存在 | 无 |
| 竞态条件 | 存在 | 极少 |
1.5 函数式编程的适用场景
1.5.1 特别适合 FP 的场景
| 场景 | 原因 | 典型技术 |
|---|---|---|
| 数据处理/ETL | 管道化、无副作用、易测试 | MapReduce, Spark |
| 编译器/解释器 | AST 变换天然递归 | 模式匹配, 代数数据类型 |
| 金融计算 | 不可变性保证审计追踪 | 纯函数, 不可变数据 |
| 并发服务 | 无共享状态 | Actor 模型, CSP |
| UI 状态管理 | 单向数据流 | Redux, Elm Architecture |
| 科学计算 | 引用透明便于优化 | 惰性求值, 向量化 |
| 区块链/智能合约 | 确定性执行 | 纯函数, 不可变账本 |
1.5.2 不太适合 FP 的场景
| 场景 | 挑战 | 替代方案 |
|---|---|---|
| 硬实时系统 | GC 暂停不可预测 | Rust(零成本抽象) |
| 极致性能优化 | 不可变数据有开销 | 命令式内核 + FP 外壳 |
| 大量 I/O 操作 | IO Monad 可能复杂 | 混合风格,隔离副作用 |
| 小型脚本 | FP 设计增加复杂度 | 简单命令式即可 |
1.5.3 业务场景案例
案例:电商订单处理
// 函数式风格的订单处理管道
const processOrder = order =>
validateOrder(order)
.then(applyDiscount)
.then(calculateTax)
.then(reserveInventory)
.then(createPayment)
.then(sendConfirmation);
// 每一步都是纯函数,易于测试和组合
const validateOrder = order =>
order.items.length > 0
? Right(order)
: Left('Order must have at least one item');
const applyDiscount = order =>
Right({
...order,
total: order.total * (1 - getDiscountRate(order.customer))
});
1.6 常见误解
1.6.1 误解与澄清
| 误解 | 澄清 |
|---|---|
| “FP 不能处理副作用” | FP 将副作用隔离到边界,使用 Monad 等结构管理 |
| “FP 性能很差” | 惰性求值、结构共享等技术可优化性能 |
| “FP 太学术化” | Haskell/Elm 已用于生产,Rust/Go 借鉴 FP 概念 |
| “FP 代码难以理解” | 学习后反而更简洁、更可预测 |
| “纯函数不能做任何有用的事” | 纯函数通过组合可以表达任意计算 |
| “FP 只是 map/filter/reduce” | FP 涵盖类型系统、并发、错误处理等广泛领域 |
1.6.2 FP 不是银弹
函数式编程并不意味着:
- ❌ 完全抛弃命令式代码
- ❌ 所有代码都必须是纯函数
- ❌ 必须使用 Monad、范畴论等高级概念
- ❌ 不可变数据一定比可变数据好
实际上:
- ✅ 在合适的地方使用 FP 概念
- ✅ 渐进式地引入 FP 特性
- ✅ 实用主义优先于教条主义
1.7 开发环境准备
1.7.1 各语言环境搭建
Haskell(GHC):
# 安装 GHCup(Haskell 工具链管理器)
curl --proto '=https' --tlsv1.2 -sSf https://get-ghcup.haskell.org | sh
ghcup install ghc 9.6.3
ghcup install cabal 3.10.2.0
ghcup install stack 2.13.1
JavaScript(Node.js):
# 推荐使用 nvm 管理 Node 版本
nvm install --lts
npm install -g ramda lodash
Python:
# 推荐使用 pyenv
pyenv install 3.12.0
pip install toolz cytoolz pyrsistent
Rust:
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
cargo new fp-exercises
Clojure:
# 安装 Clojure CLI
curl -L -O https://github.com/clojure/brew-install/releases/latest/download/posix-install.sh
chmod +x posix-install.sh
sudo ./posix-install.sh
1.7.2 推荐编辑器插件
| 编辑器 | Haskell | JavaScript | Python | Rust | Clojure |
|---|---|---|---|---|---|
| VS Code | Haskell | ESLint + Prettier | Pylance | rust-analyzer | Calva |
| Neovim | haskell-tools | lspconfig | lspconfig | rust-tools | conjure |
| Emacs | haskell-mode | js2-mode | elpy | rustic | cider |
1.8 本教程约定
1.8.1 代码风格
- 代码块标注语言标识(
haskell,javascript,python,rust,clojure) - 术语首次出现时附英文,如"纯函数(Pure Function)"
- 可运行的示例会标注输入和预期输出
1.8.2 符号说明
| 符号 | 含义 |
|---|---|
→ | 类型签名中的函数箭头 |
=> | JavaScript/Rust 箭头函数 |
∘ | 函数组合 |
∀ | 全称量词(对于所有) |
≡ | 恒等(等价于) |
⊥ | 底值(bottom,未定义/错误) |
1.9 小结
| 要点 | 说明 |
|---|---|
| FP 起源 | Lambda 演算(1930s),比计算机更早 |
| 核心思想 | 纯函数 + 不可变数据 + 声明式风格 |
| 实用价值 | 更好的可测试性、并发安全性、可组合性 |
| 适用性 | 数据处理、并发、UI 状态管理等领域优势明显 |
| 渐进采用 | 可以在现有项目中逐步引入 FP 概念 |
扩展阅读
- 《Haskell 趣学指南》 — Miran Lipovača(在线免费)
- 《Structure and Interpretation of Computer Programs》 — Abelson & Sussman
- Lambda Calculus - Computerphile — 视频讲解
- Functional Programming - Wikipedia — 综述
- 《范畴论入门》 — Steve Awodey(进阶数学基础)
- Mostly Adequate Guide to FP — JavaScript FP 入门
下一章:02 纯函数与副作用 — 深入理解函数式编程的核心概念