函数式编程32:纯核心与外部端口
三件商品的单价是一百二十五分,总额应为三百七十五分。这个计算不需要文件句柄、线程池或网络客户端。实际服务还要查询单价、取得时间、保存结果;把这些动作都放进金额函数,测试就必须同时控制外部世界,才能回答乘法是否正确。 Functional Core / Imperative Shell 的实用边界是:核心接收已经取得的值,返回决策;外层安排取得输入与执行动作。纯核心并不要求整个应用没有副作用,也不要求每个函数返回 IO。本章先用 Java 21 的三个窄端口,把计算与执行责任分开。 完整实现为 Main.java,result.json 记录编译、运行和断言输出。案例只操作本地临时文件,报价是可替换函数,不访问外部服务。 参数里应该出现哪些事实 Input 保存标识与数量,Decision 保存标识、金额和报价时刻。核心 decide 接收 Input、单价以及 Instant。这三个参数已足以计算本章输出;函数内部不再读取当前时间,也不捕获可变费率表。 123456static Decision decide(Input input, long unit, Instant no...
函数式编程31:性质生成器与缩减反例
一个把结果倒序返回的 map,在空列表和单元素列表上都可能通过测试。输入只有一个订单时,倒序没有可见差异;订单增加到两条且内容不同,恒等映射就改变了业务顺序。测试需要找到能区分实现的输入,还需要把输入缩小到足以解释错误的程度。 本章用 Java 21 实现一个有限的性质测试实验:生成数值与函数参数,检查组合关系,主动制造错误,再保存随机种子与缩减样本。它不替代完整测试框架,也不把一千次成功称为数学证明。重点是让每一项“通过”都能回答输入来自哪里、比较了什么、什么情况下本应失败。 前置知识是 map、flatMap、错误累积与显式状态。完整的 Main.java 包含生成器、缩减器与可失败断言;本次运行证据 保存环境、源文件摘要、命令和实际输出。 先确定观察等价 表达式两边都返回 Optional,还不足以说明它们相等。本实验比较 Optional 内部整数的值;错误列表比较元素及顺序;账户转换比较转换后的余额和失败时是否保持旧余额。对象地址、分配次数和耗时不在本次代数等价关系中。 选择观察方式会改变测试。错误集合若允许按任意顺序显示,可以采用集合等价;如果界面按字段顺序提示,就...
函数式编程09:持久化数据结构与结构共享
系列导读与能力自测 更新一行,不必销毁旧版本 订单编辑器当前保存数量列表二、三。一次操作在头部增加一,另一次操作在同一个旧版本头部增加零。两个新版本分别是一二三、零二三,旧版本仍应是二三。如果每次都复制整个数组,这个行为容易实现,但头插也要搬运所有旧引用。不可变链表可以只新建首节点,让三个版本共享同一段旧尾部。 这里的“持久化”指更新之后旧版本仍可使用,不表示已经写入数据库或磁盘。结构共享是实现这种多版本行为的一种方法,不是定义本身。完整复制也可以保留旧版本,只是成本不同;共享若涉及可变节点,则可能破坏历史。 本章承接 Scala 10“List 的头插为何能够共享旧尾部”。旧段落使用 prepended.tail eq old 观察身份共享,并提醒可变元素仍然泄漏。本章在 Java 21 手写节点构造与路径复制,让共享关系直接对应可阅读的源码,同时与复制 ArrayList 对照。 空节点与非空节点 12345678sealed interface PList<A> permits Nil, Cons {}record Nil<A>(...
函数式编程08:memoize与缓存的可观察行为
系列导读与能力自测 零已经算出来,第二次为何还计算 一个纯计算输入相同就返回相同值,缓存看起来只需把结果放进字典。但如果命中判断写成 cache[key] || compute(),结果恰好是零、false、空字符串或 null 时,第二次调用仍会走 compute。函数值没有算错,调用次数已经与缓存目标不同。 函数缓存 memoize保留了这一技巧入口。它的核心行是 cache[argStr] = cache[argStr] || pureFunc.apply(pureFunc, arguments),键来自 JSON.stringify(arguments)。本章独立复现该代码,分别检查假值、this、键碰撞、可变输入与容量;旧文内容由原入口维护,不在这里覆盖。 本章有 Java 21 与 Node 两个实际入口。Java 展示有界缓存与空值命中,JavaScript 保留动态语言反例。缓存只在明确输入域和值观察方式下讨论优化,不把命中率当作正确性证明。 命中检查必须独立于结果内容 JavaScript 实验逐项让底层函数返回零、false、空字符串、null,连续调用同一键...
函数式编程07:严格求值、惰性与按需共享
系列导读与能力自测 保存了一个 Supplier,还没有保存结果 一段计算每次执行都增加计数并返回新计数。直接调用得到一;把计算保存在 Supplier 中,不会因为变量赋值就再执行,但连续两次 get 得到二和三。再把同一个 Supplier 包进延迟值,第一次取得四,第二次仍得到四。返回类型看起来都是“以后可取得整数”,执行次数却完全不同。 严格求值关注现在就计算,延迟关注何时开始计算,按需共享还规定已经取得的结果会被复用。三者不能仅凭 lambda、Stream 或 Lazy 这些名字判断。本章用 Java 21 显式记录构造、调用、短路、重复消费与关闭后的失败。 已有 Scala 11 的 Iterator、View 与 LazyList分别讨论游标、重复计算和节点记忆化,尤其指出延迟读取可能越过资源关闭位置。本章复用这些问题,改用 Java Supplier、Stream 与一个教学 Lazy,避免把 Scala 集合行为直接套在 Java 对象上。 严格实参和延迟函数体 Java 方法调用前需要先求出实参表达式的值。use(expensive()) 中 expensi...
函数式编程06:递归、fold、map与filter
系列导读与能力自测 三条订单行的不同输出 教学输入有三行:数量二、单价一百分;数量一、单价三百分;数量三、单价一百分。逐行金额是二百、三百、三百,总金额八百分,总件数六。若只显示数量至少为二的行,结果又应保留第一行和第三行。四项需求读取同一个集合,却产生不同形状的输出。 map 为每项输入产生一个结果,filter 保留满足条件的原元素,fold 把逐项贡献归入一个累加状态。递归和循环则是表达遍历控制流的方法。先写输出形状与顺序要求,再选择操作,比把“有循环”当成需要消除的问题更可靠。 Scala 12 的 map 与 foldLeft 段落已经给出相同订单数据的类型推导,并比较左右减法折叠。本章用 Java 21 手写折叠内核,直接比较循环、递归与折叠;旧文是概念承接入口,新的 Java 执行记录独立保存。 从循环提取不变量 123456record Line(int quantity, long cents) {}static long amount(Line line) { return Math.multiplyExact(line.q...
函数式编程05:函数组合与管道
系列导读与能力自测 导入文本怎样变成显示结果 订单文件中的数量是字符串,计算需要整数,页面最终需要带前缀的文本。把三件事塞进一个方法当然可行,但当清理空白、校验范围与展示格式需要独立变化时,逐步连接小函数更容易检查。连接之前先写类型:清理是 String 到 String,解析是 String 到 Integer,展示是 Integer 到 String。 管道能成立,是因为相邻边界的类型对得上。解析后仍然可能得到负数,展示也可能把错误整数格式化得很漂亮,所以类型连接只解决结构兼容性。合法性、失败路径和调用次数仍须验证。本章使用三个整数函数检查组合定律,再以真实字符串解析记录事件次序。 Scala 03 的函数值部分已经解释方法如何进入函数参数与集合;Scala 12 的 map 部分指出融合变换可能改变副作用次序。本章先研究一项输入上的普通函数组合,为之后的集合变换和带失败上下文的组合提供基础。 从展开式读懂方向 12345678static <A,B,C> Function<A,C> andThen(Function<A,B> f, ...
函数式编程04:部分应用、柯里化与偏函数
系列导读与能力自测 九折规则固定之后还剩什么 报价公式接收费率与订单金额,返回按两位小数舍入的应付金额。活动期间费率固定为 0.9,订单金额则逐笔变化。把费率先提供给报价函数,可以得到一个只等待金额的策略。这是部分应用:某次使用已经固定了一部分实参,剩余实参决定之后的调用形状。 另一个接口可以把二元函数改写成“接收费率,返回接收金额的函数”。这个转换是柯里化。再考虑仅允许非负订单金额的报价规则,它没有在所有 BigDecimal 输入上定义正常金额结果,这是偏函数的定义域问题。三个概念会同时出现在一个例子中,但回答的问题不同。 Scala 03 的“多参数列表、柯里化和偏应用”以及“PartialFunction 的部分指输入定义域”已经给出语言侧对照。本章延续其区分,使用 Java 21 写出每一步类型,并检查固定参数之后仍可能遇到非法输入。这里的偏函数是数学建模用语,不声称 Java Function 自动带有 Scala 的 isDefinedAt 方法。 从同一公式推导三个类型 教学金额类型直接采用 BigDecimal。费率在零至一之间,金额非负,最终用 HALF_UP...
函数式编程03:一等函数、高阶函数与闭包
系列导读与能力自测 折扣规则成为参数之后 订单总额为 100.00,普通活动打九折,会员活动打八折。把两个规则分别塞进报价器的条件分支,报价器就要知道所有活动种类。另一种接口只要求传入一个金额变换:报价器负责提供总额,策略负责返回应付金额。增加封顶八十五元的规则时,可以传入另一个函数,不必扩张原来的分支表。 这种拆分并没有把策略变成可信代码。策略可能返回负数、读取数据库或抛出异常。函数作为值只提供一种传递行为的方式,是否纯、是否终止、是否符合金额域,仍由实现和契约决定。本章用相同订单金额检查策略替换,再用闭包读取可变数组反驳“lambda 自然是纯函数”。 已有 Scala 03“闭包保存了什么”对比了捕获 var percent 与冻结整数值的行为。本章把这一区别迁移到 Java:局部变量捕获受 effectively final 约束,但变量指向的数组仍能变化。旧文的 Scala 语法和编译行为保持原归属,不混作 Java 规则。 函数值的输入输出形状 12345678static BigDecimal apply(BigDecimal total, ...
函数式编程02:不可变数据与别名
系列导读与能力自测 保存了报价输入,却没有保存历史 订单导入器交出一份商品行列表,报价器保存它,准备稍后汇总。导入器随后把第一行数量从二改成九,又增加一行。报价器拿到的变量即使声明为 final,仍可能看到九和新增行。引用没有重新赋值,引用指向的对象却变了,先前所谓的“报价快照”只是同一个对象的另一个入口。 本章要建立的契约是:订单值产生之后,调用方继续修改原始列表或原始元素,不会改变已经产生的订单值。这个目标比“调用方不能在返回列表上执行 add”更强,因为修改可以从其他引用进入。实验同时观察列表长度、元素字段以及返回访问器,分别检查每条修改路径。 相关旧文是 Scala 10 的“不可变容器仍然可能共享可变元素”。该段用两个 Vector 共享同一 Item,修改后两者都看到九。这里不重复 Scala 集合选型,改用 Java 21 的只读包装、List.copyOf 和领域快照,解释别名应在何处截断。 三个层次不能互相代替 final 约束变量绑定不能再次赋值。只读视图限制通过这个接口执行的写操作。深层不可变则要求从公开结果能够到达的业务状态不会继续变化。这三个层次涉及的对...


