Skip to content

🔥渐进式函数式:如何在现有项目中引入 FP 思维?

——从纯函数工具函数开始,逐步替换命令式逻辑

“不要推倒重来,
而是用函数式‘渗透’你的代码库。”

一、现实困境:团队、工期、技术债

你热爱函数式:

  • 纯函数
  • 不可变性
  • 组合性
  • 无副作用

但项目是:

  • 大量 for 循环
  • this 满天飞
  • 直接修改对象
  • 副作用遍地

推倒重写?不可能。

正确姿势:渐进式改造。

二、策略 1:从“纯函数工具函数”开始

目标:替换 utils/ 目录中的命令式函数

命令式写法

js
// utils.js
function addTag(tags, newTag) {
  tags.push(newTag); // 直接修改
  return tags;
}

函数式改造

js
// utils.js
export const addTag = (tags, newTag) => [...tags, newTag];
  • 输入 → 输出
  • 无副作用
  • 可缓存、可测试

优势:

  • 零风险:不改变调用方
  • 立竿见影:减少 bug
  • 团队易接受:“不就是写个表达式吗?”

三、策略 2:用 map/filter/reduce 替代 for 循环

传统循环

js
const activeUsers = [];
for (let i = 0; i < users.length; i++) {
  if (users[i].active) {
    activeUsers.push({...users[i], lastSeen: formatTime(users[i].lastSeen)});
  }
}

函数式重构

js
const activeUsers = users
  .filter(u => u.active)
  .map(u => ({ ...u, lastSeen: formatTime(u.lastSeen) }));
  • 更短
  • 更清晰
  • 无索引错误
  • 可组合

这是最容易推广的 FP 实践。

四、策略 3:组件化“状态转换”函数

场景:React 状态更新

混乱的 setState

js
handleUpdate = () => {
  this.setState(prev => {
    prev.user.tags.push('premium');
    prev.user.profile.updated = Date.now();
    return prev;
  });
};

提取为纯函数

js
// transformations.js
export const addTag = (user, tag) => ({
  ...user,
  tags: [...user.tags, tag]
});

export const markUpdated = user => ({
  ...user,
  profile: { ...user.profile, updated: Date.now() }
});

export const upgradeToPremium = user => 
  pipe(
    addTag('premium'),
    markUpdated
  )(user);
js
// Component
handleUpdate = () => {
  this.setState(prev => ({
    user: upgradeToPremium(prev.user)
  }));
};
  • 状态逻辑可测试
  • 可复用
  • 易于调试

五、策略 4:引入不可变工具库(按需)

小改动:使用 immer

js
import produce from 'immer';

// 命令式写法,函数式结果
const newState = produce(state, draft => {
  draft.users[0].tags.push('pro');
  draft.settings.theme = 'dark';
});
  • 团队接受度高(写法熟悉)
  • 结果不可变
  • 零学习成本

大项目:引入 immutable.jszustand

  • 中央状态管理
  • 结构共享优化性能

六、策略 5:编写“函数式风格”的自定义 Hook

示例:表单处理

js
// hooks/useForm.js
export const useForm = (initial, validators) => {
  const [values, setValues] = useState(initial);
  const [errors, setErrors] = useState({});

  const handleChange = (name, value) => {
    setValues(prev => ({ ...prev, name: value }));
    
    if (validators[name]) {
      const error = validators[name](value);
      setErrors(prev => ({ ...prev, [name]: error }));
    }
  };

  const reset = () => {
    setValues(initial);
    setErrors({});
  };

  return { values, errors, handleChange, reset };
};
  • 状态逻辑封装
  • 纯配置驱动
  • 多组件复用

七、策略 6:用 fp-tsramda 渐进增强

步骤:

  1. 先用 lodash/fpramdamapfilter
  2. 引入 pipecompose 构建数据流
  3. 使用 OptionEither 处理可能失败的操作
js
import { pipe } from 'ramda';
import { map, filter, prop } from 'ramda';

const getActiveUserNames = pipe(
  filter(prop('active')),
  map(prop('name'))
);

getActiveUserNames(users); // ['Alice', 'Bob']

从“工具”入手,再学“思想”。

八、沟通策略:如何说服团队?

1. 用“可读性”而非“数学”说服

  • “这个 pipe 比 5 层嵌套 if 清晰多了”
  • “这个 mapfor 循环少 10 行代码”

2. 用“可测试性”打动架构师

  • 纯函数 → 单元测试简单
  • 状态转换 → 易于断言

3. 从小处着手,展示 ROI

  • 修复一个因副作用引起的 bug
  • immer 避免一次 shouldComponentUpdate 误判

九、避免的陷阱

陷阱建议
过度工程不要为简单逻辑写 Monad
命名晦涩getUserById 而非 findUserKleisli
团队脱节配对编程,代码评审中解释
性能下降大数据量用 lazy 或命令式优化

十、总结:渐进式函数式的 5 个步骤

步骤行动
1️⃣从工具函数开始:写纯函数,不改状态
2️⃣替换循环:用 map/filter/reduce
3️⃣封装状态逻辑:提取转换函数
4️⃣引入不可变性:用 immerimmutable
5️⃣组合与抽象pipe、自定义 Hook、Either

不要追求“100% 纯函数”,
而要追求“更少的 bug,更高的可维护性”。

函数式不是一场革命,
而是一场静默的渗透

当你某天发现:
“我们已经用 pipe 写了 80% 的业务逻辑”,
你就知道——
FP 已经赢了。