Conversations with a six-year-old on functional programming

Conversations with a Six-Year-Old on Functional Programming

一场跨越年龄的技术对话

“爸爸,什么是函数式编程?”

这个问题来自我家六岁的儿子。他看到我在电脑前写代码,好奇地凑过来问。我当时正在用 JavaScript 实现一个数据处理流程,屏幕上满是 map、filter、reduce 这样的函数式方法。

我本可以简单地回答“等你长大就知道了”,但作为一个 IT 技术博主,我突然意识到:**函数式编程(Functional Programming,FP)的核心概念,其实可以用孩子都能理解的方式解释清楚**。因为 FP 的本质就是将复杂问题拆解为简单、可预测的小步骤——这恰恰是孩子们学习世界的方式。

这篇文章,我想用这种“最简单的类比”,带你重新理解函数式编程,同时给出实用的 JavaScript/Node.js 代码示例。我们会从最基础的概念出发,逐步深入到实际工程应用。


二、技术核心分析

2.1 Pure Functions:每一次都是确定的结果

我跟儿子玩积木的时候发现了一个有趣的现象:当我把三块红色积木叠在一起,无论什么时候、无论在哪里叠,得到的永远是三块红色积木的垂直堆叠。这就是**纯函数(Pure Function)**的核心特征——**相同的输入,永远得到相同的输出,且没有任何副作用(Side Effect)**。

**什么是副作用?** 副作用就像是积木会自己乱动、变色或者消失。编程中,副作用包括:修改全局变量、发送网络请求、写入文件系统、修改传入的参数本身。

// 这是一个纯函数

function add(a, b) {

return a + b;

}

// 这不是纯函数(有副作用)

let total = 0;

function addToTotal(a) {

total += a; // 修改了外部变量

return total;

}

纯函数为什么重要?因为它们**可测试、可缓存、可并行执行**。在复杂的业务逻辑中,纯函数让代码行为变得可预测,大大降低了调试难度。

2.2 Immutability:不动原物,创造新物

六岁的孩子会本能地理解这个概念:我给你一块橡皮泥,你想把它变成其他形状,不能捏原有的那块,而是要用新橡皮泥捏一个新的。

这就是**不可变性(Immutability)**的核心思想:**不修改原有数据,而是创建新的数据副本**。

// 可变的方式(不推荐)

const numbers = [1, 2, 3, 4, 5];

numbers.push(6); // 修改了原数组

// 不可变的方式(推荐)

const numbers = [1, 2, 3, 4, 5];

const newNumbers = [...numbers, 6]; // 创建新数组,原数组不变

不可变性的优势在于:

2.3 Higher-Order Functions:把函数当作积木

“爸爸,我可以把这个小房子放在大城堡上吗?”

当然可以!**高阶函数(Higher-Order Functions)**就是允许我们把**函数当作参数传递,或者返回函数**的函数。它们就像万能积木,可以和其他积木组合。

// 基础:把函数当作参数

function map(array, transformFn) {

const result = [];

for (const item of array) {

result.push(transformFn(item));

}

return result;

}

// 使用

const doubled = map([1, 2, 3], x => x * 2);

// 结果: [2, 4, 6]

JavaScript 自带的 mapfilterreduce 就是最常用的高阶函数。它们将遍历逻辑和数据处理逻辑分离,让代码更具通用性。

2.4 Function Composition:组合的力量

给孩子一堆积木,他们能搭出无限多的事物——因为他们学会了**组合**。两块积木可以组成一个窗户,四扇窗户组成一栋房子。

**函数组合(Function Composition)**是 FP 的精髓:将多个简单函数组合成复杂功能。

// 简单的单一职责函数

const addOne = x => x + 1;

const double = x => x * 2;

const square = x => x * x;

// 手动的组合

const process = x => square(double(addOne(x)));

// 使用 compose(从右到左执行)

const compose = (...fns) => x => fns.reduceRight((v, f) => f(v), x);

const process = compose(square, double, addOne);

console.log(process(3)); // 过程:3 -> 4 -> 8 -> 64

函数组合让代码变得**声明式(Declarative)**——我们描述“做什么”而非“怎么做”。当需求变更时,只需调整组合方式,无需改动内部实现。


三、实战代码示例

下面我们通过一个完整的数据处理流程,展示如何在 Node.js 中应用函数式编程思想。

3.1 场景:用户订单数据分析

假设我们需要处理一批订单数据,完成以下任务:

1. 筛选出金额大于 100 的订单

2. 按用户分组

3. 计算每个用户的总消费

4. 按消费金额降序排列

**传统命令式写法:**

// 数据源

const orders = [

{ id: 1, userId: 'A', amount: 150, product: 'Keyboard' },

{ id: 2, userId: 'B', amount: 80, product: 'Mouse' },

{ id: 3, userId: 'A', amount: 250, product: 'Monitor' },

{ id: 4, userId: 'C', amount: 120, product: 'Headset' },

{ id: 5, userId: 'B', amount: 90, product: 'USB Hub' },

{ id: 6, userId: 'A', amount: 30, product: 'Cable' },

];

// 命令式处理

const filtered = [];

for (const order of orders) {

if (order.amount > 100) {

filtered.push(order);

}

}

const grouped = {};

for (const order of filtered) {

if (!grouped[order.userId]) {

grouped[order.userId] = [];

}

grouped[order.userId].push(order);

}

const result = [];

for (const userId in grouped) {

const total = grouped[userId].reduce((sum, o) => sum + o.amount, 0);

result.push({ userId, total });

}

result.sort((a, b) => b.total - a.total);

console.log(result);

**函数式写法:**

const { flow, groupBy, mapValues, sortBy, reverse, pipe } = require('lodash/fp');

const orders = [

{ id: 1, userId: 'A', amount: 150, product: 'Keyboard' },

{ id: 2, userId: 'B', amount: 80, product: 'Mouse' },

{ id: 3, userId: 'A', amount: 250, product: 'Monitor' },

{ id: 4, userId: 'C', amount: 120, product: 'Headset' },

{ id: 5, userId: 'B', amount: 90, product: 'USB Hub' },

{ id: 6, userId: 'A', amount: 30, product: 'Cable' },

];

// 纯函数式的处理流程

const processOrders = flow(

orders => orders.filter(o => o.amount > 100), // 筛选

groupBy('userId'), // 分组

mapValues(groupedOrders =>

groupedOrders.reduce((sum, o) => sum + o.amount, 0)

), // 计算总金额

Object.entries, // 转数组

map(([userId, total]) => ({ userId, total })), // 格式化

sortBy('total'), // 排序

reverse // 降序

);

const result = processOrders(orders);

console.log(result);

// 输出: [

// { userId: 'A', total: 400 },

// { userId: 'C', total: 120 }

// ]

3.2 自定义函数组合工具

为了更好地理解 FP 原理,我们自己实现一个函数组合工具库:

// compose.js - 函数组合工具

// 从右到左组合(数学惯例)

const compose = (...fns) => {

if (fns.length === 0) return x => x;

if (fns.length === 1) return fns[0];

return (initialValue) => fns.reduceRight(

(acc, fn) => fn(acc),

initialValue

);

};

// 从左到右组合(更符合阅读习惯)

const pipe = (...fns) => {

if (fns.length === 0) return x => x;

if (fns.length === 1) return fns[0];

return (initialValue) => fns.reduce(

(acc, fn) => fn(acc),

initialValue

);

};

// 偏函数工具

const partial = (fn, ...presetArgs) => {

return (...laterArgs) => fn(...presetArgs, ...laterArgs);

};

// 柯里化工具

const curry = (fn) => {

return function curried(...args) {

if (args.length >= fn.length) {

return fn.apply(this, args);

}

return function(...args2) {

return curried.apply(this, args.concat(args2));

};

};

};

// 使用示例

const multiply = (a, b) => a * b;

const add = (a, b) => a + b;

const square = x => x * x;

// 普通调用

const result1 = multiply(add(2, 3), square(4));

console.log(普通调用: ${result1}); // 20

// 使用 pipe 和偏函数

const process = pipe(

partial(add, 2), // 先加2

partial(multiply, 3), // 再乘3

square // 最后平方

);

const result2 = process(5);

console.log(Pipe 结果: ${result2}); // (5+2)*3 = 21, 21*21 = 441

// 使用 curry

const curriedMultiply = curry(multiply);

const double = curriedMultiply(2);

const triple = curriedMultiply(3);

console.log(Curry: double(5)=${double(5)}, triple(4)=${triple(4)});

// 链式组合

const complexProcess = pipe(

x => x + 10,

x => x * 2,

x => x - 5,

x => x / 3,

Math.round

);

console.log(复杂流程: ${complexProcess(20)}); // ((20+10)*2-5)/3 ≈ 12

3.3 不可变数据操作实战

使用原生 JavaScript 实现不可变数据操作:

// immutable.js - 不可变数据操作

// 浅拷贝 + 修改

const updateObject = (obj, key, value) => ({

...obj,

[key]: value

});

// 深层更新(处理嵌套对象)

const updateNested = (obj, path, value) => {

const [first, ...rest] = path;

if (rest.length === 0) {

return updateObject(obj, first, value);

}

return updateObject(obj, first, updateNested(obj[first], rest, value));

};

// 数组的不可变操作

const updateArray = (arr, index, newValue) => [

...arr.slice(0, index),

newValue,

...arr.slice(index + 1)

];

const addToArray = (arr, item) => [...arr, item];

const removeFromArray = (arr, index) => [

...arr.slice(0, index),

...arr.slice(index + 1)

];

const insertInArray = (arr, index, item) => [

...arr.slice(0, index),

item,

...arr.slice(index)

];

// 实战示例

const user = {

name: 'Alice',

age: 30,

address: {

city: 'Beijing',

district: 'Chaoyang'

},

scores: [85, 92, 78]

};

// 更新顶层属性

const user1 = updateObject(user, 'age', 31);

console.log('原对象未被修改:', user.age); // 30

console.log('新对象已更新:', user1.age); // 31

// 深度更新嵌套属性

const user2 = updateNested(user, ['address', 'district'], 'Haidian');

console.log('深度更新:', user2.address.district); // Haidian

console.log('原对象未变:', user.address.district); // Chaoyang

// 数组操作

const user3 = updateObject(user, 'scores', addToArray(user.scores, 95));

console.log('添加分数:', user3.scores); // [85, 92, 78, 95]

const user4 = updateObject(user, 'scores', insertInArray(user.scores, 1, 88));

console.log('插入分数:', user4.scores); // [85, 88, 92, 78]

module.exports = {

updateObject,

updateNested,

updateArray,

addToArray,

removeFromArray,

insertInArray

};


四、工具横评对比表

在 JavaScript 函数式编程生态中,有几个核心工具库值得关注。以下是详细对比:

工具名称核心功能适用场景优点缺点
---------------------------------------
**Lodash/fp**函数式工具集通用数据处理生态成熟、性能优秀、自动柯里化版本可用、TypeScript 支持好包体积较大(~72KB)、FP 版本 API 与主库不一致
**Ramda**纯函数式工具集深度函数式编程所有函数自动柯里化、数据最后参数、真正的函数式设计、丰富的函数列表性能略逊于 Lodash、API 学习曲线陡峭、中文文档较少
**Immutable.js**不可变数据结构状态管理、大型应用原生支持不可变操作、结构共享(性能优化)、与 React/Redux 深度集成学习成本高、与普通 JS 对象互转繁琐、API 较复杂
**Immer**不可变更新语法糖React 状态更新用可变语法写不可变逻辑、极低学习成本、完美集成 Redux Toolkit只处理 POJO、不支持自定义类、处理不了循环引用
** Sanctuary**类型安全的 FP强类型需求项目代数数据类型、类型错误提前暴露、完整的 Maybe/ Either 类型依赖复杂、运行时代码体积大、性能开销明显
**FPO**FP-OOP 混合渐进式迁移保留面向对象写法的同时获得 FP 优势、易于团队接受不够“纯粹”、在 FP 和 OOP 间妥协
**fnuc**轻量级 FP极简需求体积小(~2KB)、API 简洁、无依赖功能有限、社区活跃度低

选型建议

**小型项目 / 快速开发**:Lodash/fp 是最安全的选择,文档丰富,遇到问题容易找到解决方案。

**中型项目 / 追求纯 FP**:Ramda 提供了更纯粹的函数式体验,适合希望深入 FP 思想的团队。

**大型前端应用 / 状态管理**:

**类型敏感项目**:如果项目使用 TypeScript 且希望获得编译时类型检查,Ramda + @types/ramda 或 Sanctuary 都是不错的选择。


五、总结

回到开头的对话。六岁的儿子听完我的解释后,似懂非懂地点点头,然后问我:“那我可以把所有积木都组合在一起,搭一个超级大的城堡吗?”

我笑了。是的,这就是函数式编程的终极目标——**用最简单的积木,搭建最复杂的世界**。

核心要点回顾

1. **纯函数(Pure Functions)** 是 FP 的基石:相同的输入,相同的输出,无副作用。它们让代码可测试、可预测。

2. **不可变性(Immutability)** 简化了状态管理:不修改原数据,创建新数据。配合结构共享,性能和可维护性可以兼得。

3. **高阶函数(Higher-Order Functions)** 解放了抽象能力:函数可以像数据一样传递和组合,让代码复用达到新高度。

4. **函数组合(Composition)** 是 FP 的精髓:通过组合简单函数构建复杂逻辑,代码声明式、可读性强、易于维护。

实践建议

未来展望

函数式编程已经深度融入现代 JavaScript 生态:

正如我跟儿子说的那样:函数式编程不是要把代码变成天书,而是让复杂的逻辑变得像搭积木一样直观和可控。当你能用简单的积木搭出整个世界,你就掌握了函数式编程的精髓。


*本文所有代码示例均可在 Node.js 环境中直接运行,依赖项已标注。如需完整代码仓库,请访问相关 GitHub 仓库。*

← 返回文章列表