把 if 推上去,把 for 放下来。这九个字最近又火了一轮,一位资深工程师为此写了篇带代数推导的长文,技术社区一百多分讨论。我把原文、口诀的两级出处和社区正反意见都读了一遍,这条口诀值得讲给每个写代码的人。先卖个关子,这口诀还有一段出典。
口诀说了什么
先拆上半句。假设函数内部要处理可能为空的输入,先判空再干活,这是最普通的写法。口诀说把这个判断推到调用方去,函数只接收确凿的值。关键之处在决策的位置,类型收到的一瞬间已经收窄,函数体里从此没有分支,调用方多写一个判断,换来整条调用链的干净。另一个好处是错误提前,调用方最清楚上下文,判空失败时它有全部信息去补救,深埋在库函数里的空指针最难查。
下半句方向相反。循环尽量往下放,放到过滤与清理之后,用批量函数一次吃一组数据。热循环里没有异常情况要处理,编译器能向量化,缓存友好,批处理只付一次启动开销。两句合起来是一种品味,脏活在边界上做干净,核心逻辑不掺沙子。
源流与代数
这条口诀有明确的出处链。二零二三年十一月,某代码分析工具的作者写了篇短文提出这两条法则,随后被一个数据库项目的工程规范文档收录,规范原文明确写着把 if 语句放回父函数、把非分支逻辑移入辅助函数,并附了原文链接。本次热文的作者从业二十余年,出过两本领域建模专著,他的增量在于推导,用函数式组合子证明过滤与映射的交换定律,用类型理论解释为什么改写合法,又拿数据库查询优化的谓词下推做类比。习语、代数、数据库三段连接,是这篇长文的真正贡献。口诀先在工程师手里转了两年,代数证明随后补上,工程直觉与数学背书合流,生命力就长了。
边界与争论
作者自己交待了口诀的边界,这部分反而最有价值。循环里的判断只有当条件与循环无关时才能外提,逐元素的条件只能挪到边界并编码进类型。过滤与映射的先后交换后复杂度同为线性,省的只是对被丢弃元素的函数调用。批处理改变的是成本结构,函数从吃一个变成吃一批,前提是调用方能凑出批来。
社区争论同样有营养。支持方指出编译器早有同名优化,条件外提与循环不变量外移是成熟 pass,主流编译器在安全前提下自动做。反方批评长文没有基准数据,特定语言里别名与可变状态会挡住这些改写,JVM 上就有反例。也有人晒出编译器自动做这些优化的证据,提醒手写改写前先看生成的机器码,别替编译器白忙。还有人怀疑文章出自模型之手,被老读者用作者二十年的博客档案反驳了。教条与实测的拉扯,恰是这类口诀的正确打开方式。
解决了什么需求
初级到中级工程师最缺的就是这种可迁移的结构判断。学框架会过时,学这条口诀不会,它背后是决策上移、数据成批、类型收窄三个通用思路,任何语言都用得上。读法建议分两步,先读二零二三年的原始短文,两分钟建立直觉,再读本次长文的代数部分,看口诀为什么成立。带团队的人可以把它收进评审清单,函数里重复的判空、循环里反复检查同一条件,都是口诀直接命中的代码味道。评审时问一句这个判断能不能上移、这圈循环能不能晚点跑,两句问话就是这条口诀的全部用法。成本两句话,收益一整年。
一句话收尾,好口诀像好棋谱,先记住,再理解,最后知道什么时候别按棋谱走。顺序别乱。
来源
- 长文原文 https://debasishg.github.io/blog/push-ifs-up-and-fors-down/
- 原始短文(二零二三) https://matklad.github.io/2023/11/15/push-ifs-up-and-fors-down.html
- 工程规范文档 https://github.com/tigerbeetle/tigerbeetle/blob/main/docs/TIGER_STYLE.md
- 社区讨论 https://news.ycombinator.com/item?id=49997073