副作用是脆弱根因
核心摘要
本集回答如何根治脆弱、纠缠的代码,判断是:用纯函数消除副作用,以不可变数据和类型约束搭建可组合管道。
干货提炼
主题一:副作用是脆弱根因
- 函数里执行扣款这类副作用会直接造成测试和复用困难:`buyCoffee` 中的 `cc.charge` 让测试不得不造 mock、做依赖注入;想复用买咖啡逻辑,只能循环 12 次,产生 12 笔支付和 12 笔手续费。
- 函数式解法把“创建扣款”和“执行扣款”分开:函数只返回咖啡和一个 `charge` 数据值,不真正扣款;之后用一个批量函数把 12 张“收据”合并成一次支付。系统因此变成“纯核心 + 薄副作用层”。
主题二:引用透明
- 引用透明意味着表达式求值不悄悄改变外部状态:String 拼接生成新字符串,原变量不变;StringBuilder 的 `append` 会原地改对象,所以前者引用透明,后者会破坏引用透明。
- 不透明状态会让同一表达式在两处得到不同结果:纯函数像密封黑盒,不同模块之间没有共享可变状态,理解一个函数不需要模拟整个系统状态。
- 抛出异常也属于破坏引用透明的副作用:它像“go to”一样中断执行管道,跳转到调用栈深处的 catch 块;本集留下的挑战是把错误表示成不抛异常的数据值,就像把信用卡扣款变成收据数据。
主题三:尾递归迭代
- 不用可变计数器也能循环:用名为 `go` 或 `loop` 的局部辅助函数,把新计算状态作为参数传给下一次调用,而不是修改 `var` 索引。
- 尾调用消除避免栈溢出:当递归调用位于函数最后一条指令时,Scala 编译器会丢掉旧栈帧、复用同一块内存,把纯递归翻译成与 while 循环同等高效的底层代码;既保持不可变,又不用付出性能代价。
主题四:高阶与多态
- 函数作为一等值可以传来传去:`formatResult` 接收“绝对值函数”或“阶乘函数”作为参数,甚至可以直接传匿名函数,逻辑本身变成可携带的数据。
- 多态函数把具体类型抽走,让公式尽可能通用:查找函数从 `Array[String]` 泛化成 `Array[A]`,机制不变;`partial1` 谜题里,只凭 A、B、C 三个未知类型和函数签名,编译器逼迫出唯一正确实现。
- 类型越抽象,实现越被锁定:`partial1` 中拿到 A 和 B 后,唯一能做的就是交给已有的 A→B→C 函数,因此“类型决定实现”,几乎没有写错 bug 的空间。
主题五:不可变数据共享
- 函数式列表用 Nil 和 Cons 构造:`Cons(1, Cons(2, Cons(3, Nil)))` 表示 `[1,2,3]`;遍历用 `case Cons(h,t)` 模式匹配,编译器把 head 和 tail 直接解构到局部变量,替代 getter 样板。
- 不可变列表追加元素不用复制整个结构:新 `Cons` 的 tail 直接指向原列表,新旧列表共享原有元素;因为不可变,引用可以安全跨线程共享,无需防御性复制,大型系统反而降低内存开销。
- fold 处理集合替代手写循环:`foldRight` 和 `foldLeft` 把传入的逻辑应用到列表的每个元素,等价于用逻辑替换每个 `Cons`,把嵌套结构折叠成单个结果。
高光金句
- ❝ you get the safety of immutable state without paying any performance penalty ❞
- ❝ The types dictate the implementation so strictly that you literally cannot write a bug ❞
- ❝ functional programming replaces it with interlocking, sterile, highly predictable pipelines ❞
提及资源
- 本集暂无提及外部资源