函数式编程18:错误累积与依赖校验的边界
订单表单同时缺少商品编号、又把数量写成 bad 时,可以一次返回两个字段问题。数量尚未解析为整数时,却不能继续声称它“不大于零”,更不能拿一个默认数量检查库存。错误越多不代表校验越完整,只有具备前置数据的检查才有资格产生对应的错误。 本章使用 Scala 3.3.7 与 Cats 2.12.0,将字段内部的依赖检查和字段之间的独立检查分开。Either 表达先取得值再继续的过程,ValidatedNel 累积相互独立的错误,andThen 在完整订单形成后接上库存规则。实验不访问数据库,库存固定为三件,用它验证组合语义,不模拟真实库存交易。 系列导读与能力自测 先确定错误要表达什么 123case class FieldError(field: String, code: String)case class Order(sku: String, quantity: Int)type Errors = NonEmptyList[FieldError] FieldError 的 field 指向输入字段,code 表示稳定的机器原因。实验使用 sku、quantity 两个字段,以及...
函数式编程17:Applicative与独立输入的组合
数量与单价分别解析完成后,可以用乘法构造金额。若两者都是普通整数,函数只需要两个参数;若两者各自放在 Option 中,单次 map 只能取得其中一个值。组合规则还需要回答:任一输入缺席时怎么办,两个输入都有多个候选时又怎么办。 Applicative 描述这种不依赖前一步结果来决定下一步计算结构的组合。本章使用 Scala 3.3.7 手写 pure、ap 与 map2,用 Option 表达必须齐备的输入,用 List 表达候选组合,并检查四组常见定律。独立只表示数据依赖关系;它既不自动创建线程,也不保证错误累积。 系列导读与能力自测 从二元函数缺少一个参数说起 设数量为 Some(2),单价为 Some(125)。对数量 map,可以把二元乘法固定第一个参数,得到一个等待单价的函数。但这个函数也放在 Option 内,而单价仍是 Option[Int]。普通 map 接收裸函数,此时还需要一种操作,将上下文中的函数应用到上下文中的值。 123456trait Applicative[F[_]]: def pure[A](a: A): F[A] def ap[A,B](f...
函数式编程30:效果流、背压与资源作用域
逐行读取文件,看起来只需要一个惰性迭代器。可是调用方读取五行就返回时,谁关闭文件?第三行转换失败时,是否还会读第四行?消费者正在等待一个外部请求时,上游已经读了多少数据?这些不是“按需求值”四个字能够完整回答的问题。它们同时涉及需求传播、效果失败、任务取消与资源范围。 本文用 FS2 把这些问题放进一条可执行管道。依赖固定为 Scala 3.3.7、Cats Effect 3.5.7、Cats 2.12.0、FS2 3.11.0。完整 Main.scala 在临时目录创建真实文件,不依赖仓库 fixture;result.json 保存本次十四条断言、运行命令、环境与源码摘要。初次依赖获取曾遇到 DNS 失败,联网重试后才完成当前验证,没有将下载失败记作测试通过。 集合的惰性不等于效果的生命周期 一个 Iterator[Int] 可以逐个产生整数,但这个类型没有表达文件如何获取、谁拥有句柄,以及遍历中断时如何归还。把迭代器从同步资源范围里返回,甚至可能让第一次 next() 就面对已经关闭的文件。给集合增加惰性,并不会自动把资源寿命延长到最后一个消费者。 Stream[IO, ...
函数式编程29:取消、超时与任务归属
请求超时以后,后台任务是否还在运行?这个问题不能靠接口返回了一个超时错误来回答。调用方停止等待、运行时收到取消请求、计算抵达可取消位置、资源清理完成,是几个不同的事件。业务代码若只观察第一个事件,就可能在连接仍被占用时开始重试,或者在旧任务仍可能提交写入时宣布操作已经失败。 Cats Effect 的 fiber 提供了描述这些事件的工具,但并不会替应用决定所有任务的归属。本文固定 Scala 3.3.7、Cats Effect 3.5.7、Cats 2.12.0;超时测试使用同版本的 cats-effect-testkit。完整代码在 Main.scala,七项实际断言及命令、环境、摘要在 result.json。测试使用 Deferred 建立顺序,超时使用虚拟时钟,没有用随机 sleep 推测调度结果。 start 给出句柄,不自动给出任务所有权 io.start 的结果是一个描述“启动 io 并取得 fiber 句柄”的效果。执行这个效果后,调用方可以通过句柄等待或取消任务。但变量名叫 parent、代码写在一个方法内,都不自动建立“方法返回时取消其创建的所有 fibe...
函数式编程28:异步与有界并行组合
两个查询都返回 IO[Int],并不意味着它们已经启动,也不意味着把它们放进同一个 for 就会并行。异步程序至少有三个不同的问题:描述何时构造,任务何时开始,以及结果按什么顺序交给调用者。再加上资源限制,还要问同一时刻允许多少任务占用连接。混淆这些问题,常见后果不是类型错误,而是一个看似并行的服务实际串行,或者一个看似有界的批量操作提前启动了全部外部请求。 本文用可控信号隔离这些差异。示例版本固定为 Scala 3.3.7、Cats Effect 3.5.7、Cats 2.12.0。完整程序在 Main.scala,本次实际运行的命令、环境、源码摘要及九条断言输出在 result.json。实验不用随机等待猜测任务是否启动,也不测吞吐量。 类型中的依赖和执行中的重叠 设 loadUser: IO[User],loadOrders: User => IO[List[Order]]。第二步需要第一步产生的用户,因而组合必须等到这个值存在。loadUser.flatMap(loadOrders) 表达的是数据依赖,不是“把两个线程接起来”。第一步可以异步等待网络,也可以立即完...
函数式编程27:bracket 与 Resource 的生命周期
一个导入函数打开文件,读完后关闭,看起来只需要三行。实际接口还必须回答:打开失败后是否调用关闭,解析中途抛错是否关闭,请求取消时谁等待关闭,以及关闭本身失败后能否返回成功。只在正常路径末尾添加 close(),无法覆盖这些出口。 本章把文件抽象成带名字的逻辑资源,以受控错误和取消验证生命周期。资源名字及事件都是教学数据;真实文件句柄的关闭验证放在效果流章节。前置知识是 IO 描述与执行边界,目标是能写出明确的 acquire、use、release 归属,并用失败断言检查它们,而不是把所有清理交给一个没有观察点的 finally。 实验固定 Scala 3.3.7、Cats 2.12.0、Cats Effect 3.5.7 和 JDK 21。Main.scala包含九个场景,result.json记录本次实际执行。所有事件通过 Ref 保存,取消时序通过 Deferred 建立,测试不使用随机等待。 清理必须覆盖使用区的所有出口 用普通 flatMap 串联三个动作,可以表达正常路径,却还不能表达释放保障。acquire.flatMap(r => use(r).flatMa...
函数式编程26:IO 描述与执行边界
把扣款函数的返回值从 Int 改成 IO[Int],不能自动消除重复扣款。需要先确定三个时刻:调用函数时是否已经扣款,得到 IO 之后是否还没有扣款,以及同一个 IO 被运行两次时究竟发生几次请求。返回类型相同的两个实现,在这些问题上可以完全不同。 本章以事件记录代替真实扣款。输入固定为整数二十,计算加一,事件包括构造、运行和映射。这样既能观察最终结果,也能观察相同结果背后的执行次数。关键前置是纯函数、惰性求值、flatMap 和效果嵌套;教学目标是能够判断副作用是否真正进入延迟边界,而不只是识别 IO 这个名称。 实验固定 Scala 3.3.7、Cats 2.12.0、Cats Effect 3.5.7、JDK 21。完整的实验源码 Main.scala与本次执行证据 result.json对应。证据保存环境、源码摘要、命令、退出码与断言输出。它验证的是本章的有限场景,不是 IO 的全部定律或运行时实现。 从返回结果改为返回计算描述 普通函数 readPrice(): Int 调用后给出一个整数。如果实现内部读取文件,文件操作已经发生;调用者拿到整数后无法再决定这次读取应当延...
函数式编程16:Functor与映射定律的适用边界
订单数量可能缺席,也可能来自一组候选订单。对一个确定的整数加一很容易;对 Option[Int] 或 List[Int] 做同样变换,还要决定缺席是否保留、元素是否丢失、顺序是否改变。Functor 把这部分上下文规则从具体计算中分离出来:业务函数处理一个值,实例负责把函数用于上下文中的值。 具有 map 方法的类不一定满足 Functor 定律。map 如果额外反转列表,虽然能编译,却改变了结构。Java Optional.map 会把回调返回的 null 转为空,影响复合映射;回调中的日志也可能改变两种写法的可观察行为。本章用 Scala 3.3.7 手写 Option、List 映射,通过具体反例说明哪些改写需要额外前提。 系列导读与能力自测 先把函数与上下文分开 12trait Functor[F[_]]: def map[A,B](fa: F[A])(f: A => B): F[B] F 不是某个具体的元素类型,而是等待元素类型的类型构造器。把 F 选成 Option,签名变成 Option[A] 与 A => B 得到 Option[B];选成 List...
函数式编程15:Semigroup与Monoid的合并规则
订单金额可以求和,字段错误可以连接,处理日志也可以连接。三种操作都有“两份结果合成一份”的外观,但能否先分组计算再合并,要看合并是否满足结合律;能否处理空输入,还需要一个合适的单位元。仅仅提供名为 combine 的方法不够。 本章使用 Scala 3.3.7 手写最小 Semigroup 与 Monoid。金额用单币种、整数分的 BigInt 表示,错误与日志用有序 Vector。实验枚举有限样本检查结合与单位元,再用减法和 Double 加法构造反例。有限测试提供回归证据,不代替对全部输入的形式证明。 系列导读与能力自测 合并操作必须留在同一个类型中 12trait Semigroup[A]: def combine(a: A, b: A): A 两个 A 合成一个 A,称为对这个类型封闭的二元操作。例如两份错误向量合并后仍是错误向量;两个金额相加仍是金额。如果操作返回另一个无法继续参与相同合并的类型,就不能直接用这个接口反复归约。 Semigroup 还要求结合律:combine(combine(a,b),c) 与 combine(a,combine(b,c)) 相等。它...
函数式编程14:最小Scala桥接与类型类
Optional 与 List 都能 map,但 Java 方法通常分别写成 Optional<A> 和 List<A>。若希望同一个算法只要求“能映射”,就需要把外层结构也当成参数。Scala 的 F[_] 用于表达这一层抽象;它与接受一个函数作为参数的高阶函数不是同一个概念。 本章只引入后续组合章节需要的 Scala 3 语法:函数类型、enum、模式匹配、泛型、given/using、类型构造器和最小 for 表达式。实验固定 Scala 3.3.7、Scala CLI 1.9.1 与 JDK 21,不依赖 Cats。目标是能读懂并实现一个小型能力接口,不要求先掌握宏、复杂型变或整个 Scala 生态。 系列导读与能力自测 函数值仍然是值 Int => Int 表示接收整数、返回整数的函数类型。n => n * 125 是一个函数值,val 给它绑定名字。函数体在被调用时执行,写出 lambda 并不会自动执行其中的乘法。 12val cents: Int => Int = n => n * 125assert(cents(2...

