函数式编程13:Result、Either 与异常的失败边界
订单第七行的数量是 bad。返回 empty 可以阻止继续报价,却不能告诉导入界面哪个字段出了问题。若数量能解析为四,但模拟库存只有三,又是另一种失败:输入格式正确,业务拒绝本次请求。两者都与代码内部意外抛出的 IllegalStateException 不同。 本章用 Java 21 手写 Result<E,A>,把成功值与结构化错误分开,并演示短路组合。异常只在字符串解析这个狭窄边界被翻译,业务回调中的程序缺陷继续向外传播。这里的 Result 名字是教学选择;Scala Either 常用 Left 表示错误、Right 表示成功,表达的两分支结构相同,但具体库方法仍应按契约使用。 系列导读与能力自测 错误需要保留哪些信息 错误类型取决于消费它的程序。导入界面至少需要行号、字段名与稳定代码;库存拒绝则需要请求量与可用量。把所有信息塞进一条字符串,展示容易,程序化处理却会依赖自然语言格式。 123sealed interface Problem permits InputError, Rejection {}record InputError(...
函数式编程12:Maybe、Option 与 Optional 的缺席语义
订单可能没有优惠券。计算优惠券名称长度时,调用者需要区分“没有优惠券”和“有一张名称为空的优惠券”;继续套一层 try/catch 并不能建立这种区别。缺席如果是预期结果,应出现在返回值结构中,而不是让所有使用者从某次空指针异常倒推它。 Maybe 是一种常见的抽象形状:结果可以是一个 A,也可以没有 A。Scala 用 Option、Some、None 表达这类结构,JDK 提供 Optional。本章先手写一个明确拒绝 null 的 Maybe,再观察 JDK 的方法行为。相似的数据形状并不意味着它们对 null、默认值和对象身份采用同一契约。 系列导读与能力自测 缺席增加了一个分支 若有限域 A 只有红、绿、蓝三个值,Maybe<A> 有四种情况:Some(红)、Some(绿)、Some(蓝)、None。它不是为每个值额外增加一个“是否为空”的可变标志,而是选择有值或无值其中一个构造分支。有值分支携带数据,无值分支不携带 A。 这个表达适用于查询不到可选优惠券、尚未设置昵称等场景。若失败时需要告诉用户“格式不合法”或“优惠券已过期”,单独一个 None 不够用。...
函数式编程11:模式匹配、状态转换与结构折叠
已付款订单收到了第二次付款请求。程序能识别 Paid 分支,不代表它应该再次付款;所有 case 都写全,也不代表它已经实现了重复请求策略。模式匹配解决数据的分类与拆解,业务转换决定每个分支允许发生什么,这两项需要不同的验证。 本章在 Java 21 中定义一个三状态、两事件的教学程序。普通函数返回 Accepted 或 Rejected,另一个结构折叠函数把同样的状态转换成文本或数字。编译变异实验为层次新增一个状态,检查旧 switch 是否真的被编译器拒绝。所有结果局限于内存转换,不包含付款调用或数据库提交。 系列导读与能力自测 从数据结构推导处理分支 状态包含 Draft、Paid(receipt)、Shipped(receipt, tracking)。事件包含 Pay(receipt) 与 Ship(tracking)。Pay 是一个请求,Paid 是已被模型接受的结果。把二者写成不同类型,可以避免直接把输入事件当成业务事实传给后续程序。 1234567sealed interface State permits Draft, Paid, Shipped {...
函数式编程39:有界并发的本地订单导入工程
本章把金额计算、输入校验、不可变状态、效果描述、资源作用域和有界并发放到同一条可运行路径中。程序读取真实本地文件,按订单聚合商品行,查询模拟折扣报价,计算应付金额,产生已处理订单,并在整批成功后一次保存结果与命令回执。 它是进程内教学工具,不连接生产服务。输入、故障和调度关系由测试确定;文件确实创建、读取并关闭,效果确实执行,取消也等待任务结束。保存使用 Ref 中的内存快照,不把内存成功报告为数据库提交。完整实现自包含在本章目录,没有依赖其他章节的 Main 类。 从基线保留什么 导读与能力自测的金额样本是两件单价十二元五角的商品,加一件三元二角的商品,九折后应付二十五元三角八分。本章保留相同计算顺序:先汇总订单行金额,再应用折扣率,最后以 HALF_UP 舍入到两位小数。 这里的 discount 表示减免比例,0.10 对应九折,而不是直接乘以 0.10。零折扣得到原金额,全额折扣得到零。模拟报价端返回折扣率,因此它不是文件中单价的来源。商品单价来自输入,外部步骤决定折扣,这两个来源在模型中明确分开。 日期不参与本章结果,程序也不读取系统时钟或随机数。与第 00 章相比,这...
函数式编程38:同一业务程序的两个解释器
当业务流程只需要“查询报价,再保存金额”,代码可以依赖两个操作,而不依赖操作最终由内存状态、数据库还是异步运行时实现。效果参数 F[_] 把返回值所处的计算上下文保留下来,使流程不必在每一步抽出普通值。 本章实现同一段 program 的两个解释器:State 版本把事件与保存记录作为纯状态返回;IO 版本通过 Ref 执行实际的内存状态更新。两者都不是远程报价服务。实验比较业务结果与事件顺序,并额外检查 IO 描述构造、重复执行和报价失败。这比只把类型参数写成 F 更能说明抽象是否承担了实际责任。 先从具体接口开始 假设数量三的报价是三乘一百二十五分,保存成功没有额外返回值。直接实现可以接收 Quote 与 Save 两个函数,先调用报价,再调用保存。只要暂时不需要切换执行上下文,这种普通依赖注入已经足够。 引入 F 的原因是两个解释器对“返回一个值”的含义不同。State 返回的是一个需要初始 World 才能解释的状态转换;IO 返回的是一个交给运行时执行的效果描述。若接口强制 quote 返回 Long,State 就必须提前拿到 World,IO 就必须提前执行,业务流...
函数式编程37:深递归、Trampoline 与 Eval 的栈安全
一个返回值正确的小规模递归,不一定能处理很深的输入。把计算包进函数,也不一定消除了调用栈增长。栈安全讨论的是执行策略:下一步计算发生时,前一步是否仍占着必须等待返回的栈帧。 本章在 Scala 3.3.7、Cats 2.12.0 和 Java 21 上比较普通递归、编译器可优化的尾递归、手写 Trampoline、Cats Eval,以及 Monad 的 tailRecM。统一深度为二十万。实验包含真实的 StackOverflowError 反例,也检查成功路径结果与分配观察,没有把“不爆栈”写成“没有空间成本”。 等待加法的递归栈 从一加到 n 的直接写法很短: 12def unsafe(n: Int): Long = if n == 0 then 0L else n + unsafe(n - 1) 调用 unsafe(3) 时,外层需要等待 unsafe(2) 返回才能加三;下一层需要等待 unsafe(1) 才能加二。未完成的加法隐含保存在调用栈上。代码中的最后一个表达式虽然是加法表达式,但递归调用之后仍有工作,因此不是尾调用。 实验执行 unsafe(200000),...
函数式编程36:用契约回归渐进重构 Java
把一段 Java 方法改成返回 Result,并不自动意味着重构保持了原行为。原方法可能先检查单价再检查数量,空列表可能合法,溢出可能抛出特定异常。新实现即使对正常订单计算正确,也可能在这些边界上改变调用方看到的结果。 本章用一个小型计价函数展示两次可独立验证的改动。第一步把输入保存成快照,暂时保留异常契约;第二步把预期失败转成显式结果,使用边界观察函数比较新旧行为。三个版本同时留在实验中,不假装已经迁移了某个生产服务。 先记录旧方法实际承诺的行为 旧方法接收数量列表和单价分值。单价必须非负,数量必须在一到一百之间。总价使用精确整数乘法和加法,溢出抛 ArithmeticException。空列表返回零。 123456789static long legacy(List<Integer> quantities, long unit) { if (unit < 0) throw new IllegalArgumentException("unit"); long total = 0; for (int q : qua...
函数式编程35:性能实验中的时间、分配与保留空间
循环、管道、不可变数据和惰性求值各自改变了不同的成本。一次计时无法同时回答执行时间、分配流量、历史版本保留空间和缓存命中率。把这些指标混成“函数式代码快不快”,得到的结论很难指导具体修改。 本章给出一个可复现的 Java 21 微实验:相同整数输入上比较循环与 IntStream,分别记录多轮时间和当前线程分配量;另用可达对象图比较复制与共享历史;最后检查严格求值、按需消费和有界缓存。它不是 JMH 基准,不给出生产容量建议。每个结论都限定在实际测量或断言能够支持的范围内。 先确定计算的是同一件事 输入是从零到九千九百九十九的 int 数组。两种实现都选择偶数,计算平方,再用 long 汇总。乘法前先转换成 long,避免把 int 乘法溢出的结果再扩宽。 1234567891011static long loop(int[] xs) { long s = 0; for (int x : xs) if ((x & 1) == 0) s += (long) x * x; return s;}static long pipeline(in...
函数式编程34:Java 容器的组合与生命周期边界
Optional、Stream 和 CompletableFuture 都提供接收函数的方法。相似的方法形状能帮助阅读类型,却不能让它们具有相同的求值、空值或资源语义。把 thenCompose 记成 flatMap 的对应物之后,仍然需要回答:任务什么时候启动,失败以什么形式传播,谁负责关闭文件,重复使用会发生什么。 本章固定 Java 21,用真实临时文件和自有线程池检查这些问题。实验不引入替代 JDK 容器的库,也不尝试给三个 API 颁发统一的 Monad 认证。观察单位分别是返回值、回调执行次数、关闭次数、线程名称和异常原因。 从嵌套类型判断 map 与 flatMap 对于 Optional,如果函数返回 Optional,map 保留外层结构,flatMap 消去函数返回值带来的那一层包装。 12345var m = Optional.of(2);var nested = m.map(x -> Optional.of(x + 1));var flat = m.flatMap(x -> Optional.of(x + 1));assert nested.e...
函数式编程33:状态不变量与对象封装
订单支付后不能取消,是状态转换规则;金额不能为负,是构造约束;同一请求重复提交不能再执行一次,是命令处理规则。把三种规则都放进一个名为 validate 的方法,会让调用者难以判断它究竟检查了什么。函数式领域模型需要分别描述合法数据、允许的转换和重复命令的处理方式。 本章使用只有草稿、已支付、已取消三个状态的订单。它不连接支付平台,也没有数据库事务。这个范围足以比较两种实现:对象在方法内部替换自身持有的状态,纯函数接收旧状态并返回结果。比较的对象是业务可观察行为,不是代码中是否出现 class。 构造约束和转换约束 金额使用非负的 long 分值。构造器拒绝负数、空订单标识和空状态,并复制请求回执映射。这里的回执记录请求键以及对应动作,而不是只有一个“见过的键”集合。同一个键先请求支付、随后请求取消,需要被识别为冲突,不能当作正常重试。 123456789101112enum Phase { DRAFT, PAID, CANCELLED }enum Action { PAY, CANCEL }record Order(String id, ...


