核心思想
把计算看作数学函数的求值,避免可变状态和副作用。
关键概念
一、纯函数(Pure Function)
定义:
- 相同的输入永远得到相同的输出;
- 没有副作用(不修改外部状态、不做 I/O、不修改入参)。
// 纯函数
function add(a, b) { return a + b; }
// 不纯:修改了外部状态
let total = 0;
function addToTotal(x) { total += x; return total; }
// 不纯:依赖外部状态
let tax = 0.1;
function price(p) { return p * (1 + tax); }好处:
- 可测试:给定输入,断言输出即可,无需搭建环境;
- 可推理:不需要追踪外部状态;
- 可缓存:相同输入可缓存结果(记忆化);
- 并行安全:没有共享状态,天然适合并发。
二、不可变性(Immutability)
数据一旦创建就不修改。需要"改变"时,创建一个新的副本。
// 可变
arr.push(4);
// 不可变
const newArr = [...arr, 4];好处:避免意外修改,简化推理,便于实现"撤销/重做"和时间旅行调试。
代价:频繁创建新对象可能产生 GC 压力。现代语言通过持久化数据结构(结构共享)来优化——新对象与旧对象共享未修改的部分。
三、一等函数与高阶函数
一等函数:函数可以像值一样传递、赋值给变量、作为参数、作为返回值。
高阶函数:接受函数作为参数,或返回函数的函数。
常见的:map、filter、reduce。
const doubled = [1, 2, 3].map(x => x * 2);
const evens = [1, 2, 3, 4].filter(x => x % 2 === 0);
const sum = [1, 2, 3].reduce((a, b) => a + b, 0);这些替代了显式的循环,让"做什么"更清晰(而非"怎么做")。
四、函数组合(Composition)
把小函数组合成更复杂的函数。
const compose = (f, g) => (x) => f(g(x));
const toUpper = s => s.toUpperCase();
const exclaim = s => s + '!';
const shout = compose(exclaim, toUpper);
shout('hello'); // 'HELLO!'并非全是好处
一、学习曲线
对于习惯了命令式编程的人,纯函数式的思维方式需要适应。
二、性能考虑
- 不可变数据导致的拷贝开销(虽有结构共享优化);
- 大量小函数调用的开销(虽有内联优化)。
三、现实世界需要副作用
程序终究要读写文件、访问网络、打印输出。
函数式编程的做法不是"禁止"副作用,而是把副作用隔离到边界(如 Haskell 的 IO Monad,或把副作用集中在系统的外层)。
现代语言中的实践
完全不需要转向 Haskell——多数主流语言都吸收了函数式的特性:
- JavaScript:
map/filter/reduce、箭头函数、Promise 链; - Python:
map/filter、列表推导式、functools、lambda; - Java 8+:Stream API、lambda、Optional;
- C#:LINQ;
- Rust:迭代器、闭包、模式匹配、代数数据类型。
实用建议:
- 优先写纯函数(尤其是业务逻辑的核心部分);
- 把副作用隔离在边界;
- 用
map/filter/reduce替代简单的循环; - 避免过度设计——不必为了"纯"而把简单问题复杂化。
一个平衡
函数式是一种工具,不是宗教。
在业务规则、数据转换、并发处理等场景,函数式的思想能显著降低复杂度。但在涉及硬件、I/O、性能极致优化的场景,命令式往往更直接。
好的代码通常是多种范式的合理混合。
说明:本文为编程范式的知识整理。