函数式编程01:纯函数、引用透明性与总函数
系列导读与能力自测 同一订单为何需要保存日期和抽签结果 教学订单包含两件单价 12.50 元的商品。活动规则是每月第三日且抽签数小于十时打九折,其他情况原价。若报价方法只接收单价和数量,内部读取系统日期并生成随机数,保存这两个参数仍不足以重放报价。第二天重算、换一次随机结果,都可能得到不同金额。把方法改成静态方法,或者把实现缩成一行 lambda,都没有补齐缺失的信息。 报价核心可以接收已经取得的日期与抽签数:quote(unit, quantity, day, draw)。外层从时钟和随机源读取输入,核心按照固定规则返回金额。这一分工使 12.50、2、2026-10-03、5 对应 22.50,而同一天把抽签数改为 50 得到 25.00。记录一次计算所需的信息,现在可以直接从参数表得到。 本章的前置是 Java 方法、异常和基本测试。目标是区分纯度、可替换性和总性,不要求先理解任何组合抽象。Scala 14 的“把隐藏状态变成显式输入和输出”已经给出购物车状态的例子;这里保留其值语义观察范围,增加时钟、抽签、异常和不终止路径的 Java 对照。旧章的 Scala 3.3.7...
函数式编程10:和类型与积类型,约束订单的合法形状
三个布尔字段 paid、shipped、cancelled 可以组成八种值。若订单只能在未付款时取消,发货又必须以付款为前提,其中只有四种形状有业务含义。paid=false, shipped=true 仍然能通过 Java 编译;每个字段各自合法,组合却不合法。 本章用 Java 21 的 record 与 sealed 建立一个教学订单模型,把“属于哪个阶段”和“该阶段需要什么数据”放进类型结构,再在外部输入进入领域模型的位置检查内容。实验只处理草稿、付款、发货和付款前取消,不包含退款、部分发货或支付网关事实核验。有限模型的用途是把保证范围写清,而不是假装几种类型已经覆盖完整电商系统。 系列导读与能力自测 先数状态,再决定字段 可以从四个有效布尔组合推导阶段:三个字段均为 false 是草稿;只有 paid 为 true 是已付款;paid 与 shipped 为 true 是已发货;只有 cancelled 为 true 是已取消。其余四种组合都需要排除。若继续保留三个独立字段,每个构造入口、数据迁移和更新路径都要维护这组关系。 约束也能写进普通类的构造器,并不要求使用函数...
函数式编程19:map、join 与 flatMap,嵌套从哪里产生
查到订单以后,为什么还不能直接查配送区 订单查询返回 Maybe<Order>,地址查询接收 Order,返回 Maybe<Address>,配送区查询又接收 Address,返回 Maybe<String>。每一步都可能缺席。订单不存在时,没有订单可以交给地址查询;地址不存在时,也没有地址可以交给配送区查询。这条依赖关系决定后续函数能否调用,不能靠给空值补一个默认订单来消除。 本章采用内存中的合成查询,正常路径得到配送区 Z1。实验通过参数分别让第一、第二、第三步缺席,观察最终结果和三项调用次数。它不访问数据库,也不模拟异步请求。这样可以把关注点限定在组合规则上:同一业务链条分别写成嵌套分支、嵌套 map、map 后 join,以及 flatMap,比较改变写法以后究竟保留了什么。 前置是能够读懂函数参数与返回值、理解 Maybe 的有值和缺席两个分支,以及 map 对普通结果的变换。Scala map、flatMap 与 fold中“flatMap 连接每项贡献的结果段”已经解释集合展开。本章承接其类型分析方法,但改为逐步查询;多结果列表展...
函数式编程20:Monad 定律与 Kleisli 组合,接续计算需要什么保证
能编译的 flatMap 也可能无法可靠组合 一个列表组合器先运行普通 flatMap,再把整个结果反转。它的输入输出类型完全正确:接收 List[A] 和 A => List[B],返回 List[B]。有些单元素测试也能通过。但把一串组合提取成辅助函数以后,结果顺序可能变化。类型正确还不足以支持这种重构,组合还需要满足规律。 Monad 描述一族类型 M 的接续能力及其规则。pure 把已有值表示为 M 中的计算,flatMap 允许后一步根据前一步得到的值选择另一个 M 计算。这个定义既适用于缺席传播,也适用于多结果、环境读取和状态传递。将 Monad 解释成“一个装东西的盒子”,无法说明环境函数为什么也能满足同一规则。 前置是 map、join 与 flatMap的四种查询形式。已有 Scala 高阶类型与组合定律中“Monad 加入 pure 与依赖组合”展示了最小接口与 Option/List 实例,本章承接这个背景,重点增加可观察等价、坏实现反例、Kleisli 组合及 for 的局部展开。 独立实验源码使用 Scala 3.3.7、Cats Core 2.1...
函数式编程21:List 与 Reader,多结果和共享环境怎样接续
一个报价程序可能有多个答案,也可能等待配置 订单数量有多个候选,每个数量又对应若干价格方案,程序需要保留所有组合及其顺序。这可以用 List 表达。另一种报价程序已经确定数量,只差单价和附加费配置,此时计算可以表示为 Env => Amount。两者都能提供 pure 与 flatMap,但接续规则完全不同。 List 的 flatMap 为每个已有结果产生一段后续结果,再依次连接。Reader 的 flatMap 构造一个函数,等环境到来后计算前一步,用其结果选择后一步,并将同一个环境交给后一步。Reader 无需内部存放一个现成的金额;这正是“Monad 必须是物理容器”解释不充分的地方。 前置为 Monad 定律与 Kleisli 组合。Scala for 推导式的双生成器示例已经介绍两层有序组合,本章保留其顺序语义,再加入依赖候选、重复项和函数环境。已有 Scala Typeclass 与组合实例说明能力对象与数据可分离,Reader 的核心则是运行时函数组合,不要把它与 given 搜索混为一谈。 实验源码固定 Scala 3.3.7、Cats Core 2.12...
函数式编程22:State,把状态变化放进函数结果
连续导入两条订单,第二条必须使用新的序号 导入程序保存下一个可用编号与已接受编号列表。初态是编号十、列表为空,接受两条订单后,应返回十与十一,终态的下一个编号变为十二,列表为十、十一。若第二次分配又拿初态来执行,就会重复使用十,即使函数签名和代码缩进都没有异常。 State 将这种“计算结果加新状态”表达成函数。它不要求通过共享变量修改当前状态,而是接收一个状态值,返回结果和新的状态值。组合器负责把前一步返回的新状态传给后一步,业务函数负责每一步的具体转换。 前置是 List 与 Reader 的组合。Reader 在同一次运行中共享原环境;State 在连续步骤之间传递更新后的状态。这一处差别直接体现在 flatMap 的函数展开中,不能仅凭两者都叫 run 就认为行为相同。 已有 Scala ADT 与 enum中“命令是请求,状态是已经成立的事实”说明了领域状态与事件建模。本章的 State 是一个组合抽象,类型参数 S 可以保存领域状态,但名为 State 的抽象本身不会验证哪些订单迁移合法。旧文的付款、发货模型仍归原系列,这里只做序号与接受列表实验。 完整源码使用 Sc...
函数式编程23:Writer,返回计算结果与可组合记录
价格相同,计算记录也必须相同吗 一个报价计算先取得单价一百,再乘数量二,最后加附加费五。结果是二百零五。如果测试只断言这个数字,就无法区分“按数量计价后增加附加费”和某段直接返回二百零五的代码。某些场景还需要一份计算说明:经过哪些规则、命中了哪些折扣、为什么没有使用另一个价格。这份说明可以成为返回值的一部分,不必在计算过程中直接写到控制台。 Writer 表达的就是结果与附加记录的组合。它的重点不在记录使用字符串还是结构化对象,而在连续计算如何合并各自生成的记录。前一步给出结果,后一步根据这个结果选择计算;两步记录按约定的运算合并。业务步骤不需要接收并手动传递整个历史记录。 前置是 State 与显式状态传递。State 将新状态交给后一步;本章普通 Writer 回调只得到业务结果,不会自动读取此前的日志。把两者都理解为“顺便带上一些数据”,会漏掉这个输入方向上的区别。 已有 Scala map、flatMap 与 fold介绍了集合转换与归约。本文借用归约时的结合运算来解释日志合并,但不把 Writer 当作另一种集合遍历工具。报价步骤之间仍然有数值依赖,记录只是与结果一起返...
函数式编程24:Traverse,从逐项结果到整体结果
一批输入产生了多个 Option,调用者要哪一种结果 逐项解析一批输入时,map 很容易得到 List[Option[Int]]。它保留每个位置的成功或缺失,但没有回答整批输入的成功条件。调用者可能希望任意一项失败就拒绝整批,也可能希望保留所有错误,还可能只需要筛出成功项。这些需求不能用同一个“展平”含糊处理。 本章先选择第一种契约:全部成功时返回原顺序的结果列表,出现缺失时返回 None。类型从 List[Option[A]] 变为 Option[List[A]]。这叫 sequence;如果输入还是 List[A],需要先逐项应用 A => Option[B],再组织整体结果,就对应 traverse。 前置是 Writer 与可组合日志,也需要理解 Applicative 的独立组合。已有 Scala 错误短路与错误累积区分了依赖检查与独立检查。本章保留那个边界,增加集合层面的类型转换和真实调用轨迹,不把旧文的领域校验模型重新写成另一个版本。 三个相似表达式有三个不同契约 对 List(Some(1), None, Some(2)) 使用集合 flatten,结果是 ...
函数式编程25:EitherT 与 OptionT,逐层解释嵌套计算
返回类型多了一层,原来的 flatMap 接不上了 查找数量的函数原来返回 Either[String, Int],报价函数接收数量,返回同样的 Either。直接 flatMap 就能在成功时继续、失败时保留错误。现在要求查找过程先保存为一段待执行计算,函数返回 F[Either[String, Int]]。业务错误仍然需要表达,外层 F 又有自己的求值规则,两个问题同时存在。 此时调用外层 flatMap,回调拿到的是 Either[String, Int],不是 Int。调用内层 flatMap,回调又需要返回 Either,不能直接返回报价函数产生的 F[Either[String, Int]]。这不是编译器推断能力不足,而是两个 flatMap 分别组合不同种类的计算。需要写出一种操作,把外层的顺序规则与内层的失败规则合起来。 EitherT 与 OptionT 将这种重复组合封装成类型。它们没有让两种效果凭空消失,也不是把任意两个 Monad 放在一起就自动得到第三个。本章从实际嵌套类型和分支代码推导操作,再与 Cats 对照;不会从一个盒子比喻跳到库用法。 前置是 ...
服务网格 E01:Linkerd 的代理、身份与策略
库存服务凭什么相信 orders 累计工程里 loadgen → frontend → orders → inventory 是只读调用;orders 在库存成功后才调用 external-stub。如果另一个服务也能直接请求库存,单看 inventory-v1 返回的报价,分辨不出调用来源。Linkerd 能在两端工作负载都正确入网、策略覆盖入站连接时,把“谁连进来”变成由身份凭据验证的条件;它不能凭 orders 这个 HTTP 字符串证明对端身份。 图的左边是出站代理,右边的入站代理验证来源并执行策略。Kubernetes API 和 Linkerd 控制面负责提供身份/目的地/策略信息,不是每个报价请求必经的同步授权网关。以 Linkerd 2.18 文档快照为基线(2026-10-03 核对);这里没有安装该版本,不能把文档中的默认值当作当前环境的配置。 两个代理,不是两个应用里的 TLS 库 Linkerd 数据面使用 Rust 编写的轻量 linkerd2-proxy,并非 Envoy 换一个名称。控制面的 destination 提供目的地及端点变化信息,ide...

