本节摘要:推倒重写是函数式迁移最著名的失败方式。本节给出反面教材之后的正路:三阶段渐进改造——先立约定(新代码守规矩)、再提纯核心(高频变更模块先动)、最后择域深耕(核心域换语言或深挖)。每阶段给退出判据、周期预估与风险清单,并以一个订单系统的迁移实例串全程。
某金融科技团队在技术动人的号召下启动"核心系统 Haskell 重写",三十人投入十四个月,进度过半时发现:旧系统还在并行维护(双跑成本翻倍)、新系统对十几个遗留协议的兼容超出预期、Haskell 招聘让团队半年只补进两人。项目最终止损回撤,一年投入归零。复盘的结论值得抄进每份迁移方案:重写失败不是因为 Haskell,而是因为"大爆炸式切换"——切换周期越长,双跑成本与需求漂移越致命。函数式迁移的正路与它恰好相反:小步、并行、随时可停。
第一阶段不写一行函数式代码,只做两件事:定规矩与建雷达。定规矩指发布团队约定(下一节给模板),核心条款通常只有三条:新代码的模型不可变、新代码的规则层纯函数、副作用收口到指定层。建雷达指摸清家底——用第 2.1 节的六类副作用清单扫描存量代码,产出一张"模块纯度地图":每个核心模块标注副作用密度、变更频率(近半年提交数)、测试覆盖。这张地图是后续一切决策的输入,产出它的扫描脚本值得认真写:
# 纯度雷达:静态扫描三类高危信号,产出模块级汇总 import ast, collections def scan_file(path): tree = ast.parse(open(path, encoding="utf-8").read()) hits = collections.Counter() for node in ast.walk(tree): if isinstance(node, ast.Global): # 信号一:全局写 hits["global"] += 1 if isinstance(node, ast.Attribute) and isinstance(node.value, ast.Name): if node.attr in {"append", "update", "pop", "sort"}: # 信号二:原地变更 hits["mutation"] += 1 if isinstance(node, (ast.Import, ast.Attribute)): if "time" in ast.dump(node) or "random" in ast.dump(node): # 信号三:时钟随机 hits["nondeterminism"] += 1 return hits # 对每个模块汇总:副作用密度 = 三类信号之和 / 函数数 # 再叠加变更频率(从版本控制历史取提交数)与覆盖率,生成优先级矩阵
阶段退出判据:约定发布并全员签字确认;纯度地图覆盖全部核心模块;评审清单(三条规矩的检查项)进入流程。此阶段零风险,纯收益是全团队第一次对代码库的副作用分布有共同认知。
第二阶段按纯度地图选切入点,判据公式简单粗暴:变更频率高 × 副作用密度高 × 测试覆盖相对好 = 最优先。变更频率高保证投入有回报,副作用密度高说明改善空间大,测试覆盖好提供安全网。切进去之后用第 3.4 节的四步法(提纯、拆意图、组管道、收边界)逐模块改造,每模块独立发布、独立可回滚。
以订单系统的实路径为例:纯度地图显示"促销计算"模块半年改版十一次、副作用密度全库第二、单测覆盖百分之六十——三高,首选切入。六周改造后:规则函数全部提纯、时钟与序号注入、快照采集上线(顺带装好第 7.2 节的排错体系)。阶段二的纪律:一次只动一个模块、改完即发布、发布后观察两周再选下一个——小步跨步换来的是随时止损的能力。常见的执行偏差是把阶段二做成"顺手大重构",一次提交混入功能变更与提纯变更,出问题时无法归因,务必用提交纪律防住。
阶段退出判据:高频变更模块全部完成提纯;测试覆盖与用例数达到第 7.1 节"三指标同改善"标准;团队在评审中能自然使用"纯度"词汇并达成共识。
前两阶段在任何语言内即可完成。第三阶段才回答"要不要换语言":把最核心、规则最密、正确性最敏感的域(计价引擎、风控内核、结算逻辑)迁到函数式语言(Scala、Clojure,或激进者直接 Haskell),与主流主库互操作。进入阶段三的前置条件全部是组织性的而非技术性的:团队有至少两人有目标语言实战经验、招聘管线验证过、双栈运维(构建、监控、告警)跑通过试点。不满足就停在阶段二——阶段二的收益(可测、可复放、可并行)已经覆盖函数式价值的大头,语言迁移是锦上添花而非必选项。

切入顺序有了判据公式,落地时还需要一张可打分的表格防止"凭感觉选模块"。给四个维度各设三档分值:变更频率(半年提交数:高三分、中两分、低一分)、副作用密度(纯度雷达的三类信号之和除以函数数:高三分、中两分、低一分)、测试安全网(覆盖率与用例质量:好三分、一般两分、差一分)、模块规模(预计改造工作量:一周内三分、两周内两分、更长一分)。总分十二分制,优先处理十分以上的模块,四分以下永远不排期。
计分卡的价值不在精确,在让排序可讨论:团队对"先改哪个"有分歧时,把各模块的打分摆到桌面上,分歧立刻从"你偏爱哪个模块"变成"哪个维度的权重该调"——后者是可协商的技术问题,前者是鸡同鸭讲。某订单系统团队用这张卡排出了促销计算、库存扣减、地址服务三个模块的顺序,全程没有出现"我觉得"式的争论。计分卡每季度复核一次,模块的变更频率变了,排序就跟着变。
三阶段各自的风险与预案。阶段一风险最小,唯一要防的是"规矩过重"——条款超过一页纸,执行率必然崩,规矩宁少勿多。阶段二的两大风险:提纯中引入业务回归(预案:改造前后用快照做行为对拍,不一致即回滚)与工期失控(预案:模块级时间盒,超时未完成就收缩范围而不是延期)。阶段三的独有风险是双栈人才断层(核心域只有一人会写目标语言时,此人离职即灾难;预案:双人在岗制度与文档强制)。给整个迁移装一个总闸:任何时点发现"迁移导致交付速度下降超过两成且持续一个月",暂停迁移、复盘原因——迁移永远不该让业务交付买单。
💡 关键直觉:迁移的对象不是代码是风险敞口。变更频率高的模块每天都在产生新的"状态类 bug 风险",先提纯它,等于先堵住漏水的管子;没漏的管子(低频稳定模块)永远排在最后,甚至不用排。
单兵与战役的打法齐了,最后一环是组织:一个同时存在函数式与命令式风格的团队,如何立规矩而不打风格战争。