强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

函数式编程艺术 / 01 函数式编程导论

01 函数式编程导论

“函数式编程是一种编程范式,它将计算视为数学函数的求值,并避免改变状态和可变数据。”


1.1 函数式编程的历史

函数式编程(Functional Programming, FP)的根基可以追溯到 1930 年代,比电子计算机的出现还要早。

1.1.1 关键时间线

年份事件人物
1930Lambda 演算(Lambda Calculus)提出Alonzo Church
1936图灵机提出,与 Lambda 演算等价Alan Turing
1958LISP 语言诞生,首个 FP 语言John McCarthy
1973ML 语言诞生,引入类型推断Robin Milner
1987Haskell 标准化委员会成立多位学者
1990Haskell 1.0 发布Haskell Committee
1995JavaScript 诞生,浏览器中的 FPBrendan Eich
2003Erlang 在电信领域大规模应用Ericsson
2007Clojure 诞生,JVM 上的 LispRich Hickey
2010Scala 开始流行,JVM 上的 FP+OOPMartin Odersky
2015ES6 引入箭头函数、解构,JS FP 化ECMA
2020sRust、Kotlin、Swift 大量采用 FP 特性社区

1.1.2 两大流派

函数式编程有两个主要流派:

流派代表语言特点
纯函数式Haskell, Elm无副作用,引用透明,强类型系统
多范式函数式JavaScript, Python, Rust, ClojureFP 与 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/catchEither, 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 推荐编辑器插件

编辑器HaskellJavaScriptPythonRustClojure
VS CodeHaskellESLint + PrettierPylancerust-analyzerCalva
Neovimhaskell-toolslspconfiglspconfigrust-toolsconjure
Emacshaskell-modejs2-modeelpyrusticcider

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 概念

扩展阅读

  1. 《Haskell 趣学指南》 — Miran Lipovača(在线免费)
  2. 《Structure and Interpretation of Computer Programs》 — Abelson & Sussman
  3. Lambda Calculus - Computerphile — 视频讲解
  4. Functional Programming - Wikipedia — 综述
  5. 《范畴论入门》 — Steve Awodey(进阶数学基础)
  6. Mostly Adequate Guide to FP — JavaScript FP 入门

下一章02 纯函数与副作用 — 深入理解函数式编程的核心概念