Scala E01:Cats 与 IO 的执行、取消和资源释放
同一笔订单被计算两次,是否会扣款两次,取决于“计算”究竟是什么。保存一个已经提交的 Future,再等待它两次,通常是在观察同一项任务;保存 IO,再组合执行两次,通常是在执行两次描述。这个区别会影响重试、计费、日志、资源关闭,不能只用“它们都是异步容器”概括。 Cats 提供类型类、数据类型和组合规则;Cats Effect 提供效果执行所需的抽象与运行时。Validated 的错误累积不需要启动 fiber,IO 的取消也不能从 Applicative 定律直接推导。把这两组问题分开,才能判断一次实验究竟证明了什么。 本篇冻结 Scala 3.3.7、Cats Effect 3.5.7、JDK 21,使用仓库 examples/scala-lab/electives/E01 中的独立工程。实际执行记录为 evidence/20261002-b/lifecycle.log,运行命令和退出码存放在同名 JSON。这个组合是可复现基线,不代表当前最新发行版。 错误累积先于效果执行 假设订单有姓名和数量两个字段。姓名为空与数量小于一是独立输入错误,用户通常希望一次看到两项。若用 Ei...
Scala 39:订单批处理与故障诊断
一份订单文件怎样得到可信的结果 订单文件中的一行 one,2,12.00,CNY 表示编号、数量、单价和币种。程序按 5% 费率得到 25.20,再输出带行号的回执。算式很短,但外部输入可能缺列、数量为负、价格不是数字,报价可能拒绝或迟迟不返回,文件也可能在读取和关闭时分别失败。批处理需要把这些结果保留下来,调用者才能决定哪些命令可以重试。 结课工程将前面的数据类型、错误组合、上下文参数、Java 互操作、Future 和资源作用域放进同一条执行路径。模型只接受人民币订单;输入最多一万行;重复编号仅在当前批次拒绝;报价用本地实现替代外部服务。这些限制让每一种故障都有可重复的触发条件,也限定了“成功”究竟证明了什么。 冻结组合为 Scala 3.3.7、Scala CLI 1.9.1、Amazon Corretto JDK 21.0.11。状态为 LAB_VERIFIED。实验已经实际编译并运行 CLI,核验正常输入、非法字段、重复命令、报价拒绝、同步与异步报价失败、超时后任务继续、空批次、行数超限、读失败、关闭失败,以及两种失败同时出现时的异常保留。金额核心另在 Scala 2....
Scala 38:集合与抽象的性能证据
四种写法先算出同一个金额 订单批次的金额可以用数组上的 while 循环计算,也可以把 List、Vector 的每个订单行映射成小计,再折叠求和。Vector 的 view 则把映射推迟到消费阶段。源代码的长度不同,执行过程还受集合结构、闭包、十进制计算、JIT 与垃圾回收共同影响。要比较成本,首先要保证每一种实现完成同样的工作。 本实验把数量和单价存入 OrderLine,四种路径都使用 BigDecimal 乘法和加法,并返回整批小计。输入在基准启动前构造,测量期间复用;循环中没有文件读取、随机生成或控制台输出。正确性程序先在 1、7、512、4096 行下对照结果,同时拒绝零行负载。这些断言通过之后才运行 JMH。 状态为 LAB_VERIFIED。冻结组合是 Scala 3.3.7、Scala CLI 1.9.1、JMH 1.37、Amazon Corretto JDK 21.0.11。已经运行两 fork、每 fork 两轮预热及三轮正式测量,得到八组带原始样本的结果;GC profiler 另行运行四组诊断。代码在 examples/scala-lab/snippe...
Scala 37:Scala 2 到 Scala 3 迁移
同一个金额库,为什么有两种迁移结果 订单库定义了 def amount(): BigDecimal,旧客户端使用 Pricing.amount 取得金额。Scala 2.13.16 可以编译这份代码。把库和客户端一起交给 Scala 3.3.7,编译失败;保留 Scala 2 编译的库,只迁移客户端到 Scala 3,旧调用却仍能通过。 区别来自方法的定义来源。Scala 3 默认要求调用 Scala 3 定义的空参数列表方法时写出括号,同时保留 Java、Scala 2 方法及相应覆盖方法的互操作豁免。只写“Scala 3 不再允许省略括号”,会漏掉迁移中很常见的分模块状态。Scala 3.3.7 的 auto-application 规则 迁移验收因而需要记录三件事:源文件交给哪个编译器、依赖来自哪份产物、最后执行了哪个客户端。只改构建文件里的版本号,无法说明一个调用为什么成功,更不能证明订单规则没有变化。 本实验固定 Scala 2.13.16、Scala 3.3.7、Scala CLI 1.9.1 与 Corretto 21.0.11。正常编译、预期拒绝、自动改写、双向二...
Scala 36:纯业务规则接入文件与异步服务
把一个订单文件变成可判定的进程结果 本章完成一个小型批量订单校验 CLI。输入文件每行包含订单标识和数量,例如 A,2。数量必须是一到一百;所有行合法后,程序通过显式报价接口取得金额并汇总。正常输入输出已接受条数和总额,非法输入、报价失败、文件错误使用不同退出码。 这组需求会同时触及纯函数、错误模型、文件生命周期、Future 与进程边界。把所有逻辑写在 main 里虽然能运行,但很难分别验证“规则正确”“外部依赖失败被翻译”“资源关闭”“退出码正确”。本章保留少量明确函数,不预先引入通用效果框架。 实验使用 Scala 3.3.7、Scala CLI 1.9.1、JDK 21.0.11。报价端是可注入故障的假实现,不访问真实网络。文件则真实创建、读取和删除,CLI以独立子进程运行,因此退出码和标准输出不是通过直接调用函数推测出来的。 最终四种场景分别得到零、二、三、四。每个场景都保存原始命令、stdout、stderr和退出码,便于检查它们是否互相一致。正常路径的 accepted=2 total=500 对应两条数量分别为二和三的订单,单价固定为一百个最小货币单位。 纯核心只...
Scala 35:sbt、模块与依赖
订单程序可以运行以后,构建还需要证明什么 订单总额计算只有几行代码,把它和命令行入口放在一起很方便。但当另一个程序也要复用计算规则时,两个问题随之出现:核心规则能否独立编译,使用者拿到发布的 jar 后能否得到相同行为?源码放进同一个仓库,只能说明文件接近,不能回答这两个问题。 本章把纯计算放在 core,把输出入口放在 cli,再增加一个只读取发布坐标的 consumer。模块边界由实际编译成功和失败来观察;发布边界由另一条依赖路径来验证。测试采用简单断言,业务规则仍是两项订单合计五百分,避免构建问题被复杂业务掩盖。 实验固定 sbt 1.11.7、JDK 21.0.11-amzn,主版本 Scala 3.3.7,并以 Scala 2.13.16 做交叉构建。实验状态为 LAB_VERIFIED,最终运行编号为 20261002-ch35-r3。完整源码位于 examples/scala-lab/modules/35,命令、退出码和原始日志见配套 RUN.md。 构建定义同时描述配置值与任务依赖 下面是工程的版本配置: 1234ThisBuild / organization ...
Scala 34:MUnit、性质测试与失败重放
一个往返性质怎样发现最小反例 整数编码器把整数变成十进制字符串,解码器把合法字符串还原成整数。它们的关系可以写成 decode(encode(n)) == Some(n)。普通例子可以检查零、负一和上下界;性质测试则针对生成的多个输入重复检查这条关系。 本章使用 MUnit 1.0.4 与 ScalaCheck 1.18.1,运行在 Scala 3.3.7、Scala CLI 1.9.1、JDK 21.0.11 上。依赖仅放入独立模块,整套章节的默认标准库构建不需要引入这些测试库。 正常实现通过两个 MUnit 用例与三百个固定 seed 的性质检查。故障实现故意拒绝所有非负数,性质测试先得到输入十二,随后缩减四次得到零。用相同配置重放,反例结构一致;把零写成独立 MUnit 用例,测试进程实际非零退出。 这条证据链包含正常结果、发现错误、缩小错误、重现错误和固定回归。测试全绿只是其中一种观察;预期失败如果没有真正发生,也不能算完成了验证。 先把业务关系写清楚 本章正常代码非常小: 12def encode(n: Int): String = n.toStringdef deco...
Scala 33:同步与异步资源的生命周期
返回Iterator或Future时,工作可能还没完成 一个读取文件的方法把 Source 放进 Using.resource,返回 getLines() 得到的迭代器。方法正常返回,资源也被关闭,但调用者随后才取第一行,实际读到的是已经关闭的输入。另一个方法在同样作用域里创建 Future,Future 尚未使用资源,作用域就已经结束。 这两类错误有相同结构:词法作用域结束得比实际消费早。自动关闭机制按函数体的返回时刻工作,并不会检查返回值是否还间接依赖资源。Iterator[String] 和 Future[Int] 的类型本身没有证明它们与文件或连接的生命周期脱离。 本章同时使用真实临时文件和可记录事件的资源。正常路径、业务异常、关闭异常、双重异常、嵌套资源、惰性逃逸和异步逃逸都有断言。异步反例用门闩固定顺序,不靠线程调度偶然出现故障。 冻结环境为 Scala 3.3.7、Scala CLI 1.9.1、JDK 21.0.11;Using 实现核验到标准库 2.13.16 固定提交。具体运行证据位于 examples/scala-lab/evidence/20261002-...
Scala 32:异步失败、超时与共享状态
收到超时之后,副作用还可能发生 调用方收到超时,只能说明某个等待或组合结果已经失败;它不自动证明原工作停止。本章让一个慢任务先进入执行,再被门闩阻塞。定时器使另一个 Promise 失败,组合结果观察到超时后,慢任务仍未完成。随后释放门闩,副作用计数从零变成一。 这个实验把“调用方不再等待”和“任务已取消”分成两个可观察事实。若慢任务代表外部写入,超时之后直接重新提交可能产生重复操作。是否需要重试、如何避免重复、取消请求是否被下游接受,都需要额外协议。 同一章还重现一个共享计数错误:两个任务都读到零,再各自写回一,最终结果是一。换成原子递增后结果是二。这说明异步结果组合不会替共享可变字段提供原子性,Future 类型也不是线程安全容器。 工具冻结为 Scala 3.3.7、Scala CLI 1.9.1、JDK 21.0.11。所有情形都在 snippets/32/Chapter32.scala 中运行,证据包含命令、退出码和有限输出。超时和丢失更新都是预期的错误场景,测试通过表示这些错误被准确重现并由断言确认。 Promise控制完成,Future用于观察 Promise[A]...
Scala 31:Future与ExecutionContext的执行模型
两个Future写进for之后是否并行 下面两种写法都能合并两个结果,却不一定具有相同的任务创建时机: 123456val first = fetchFirst()val second = fetchSecond()for a <- first b <- secondyield combine(a, b) 1234for a <- fetchFirst() b <- fetchSecond(a)yield combine(a, b) 第一种在组合之前已调用两个创建函数。第二种需要第一个结果,才进入创建第二个任务的函数。判断并发不能只看是否使用 for,而要把表达式求值、Future 创建、任务开始和结果完成分开。 本章使用命名的双线程池与门闩实际验证这些时刻。独立任务都先报告开始,再等待释放;顺序任务则记录第二段函数何时执行。实验不依赖“睡一小会儿之后应该已经开始”的推测,而是用同步信号建立可以检查的先后关系。 冻结工具仍为 Scala 3.3.7、Scala CLI 1.9.1、JDK 21.0.11。标准库实现核对到 2.13.16 的固定提交...
