SuperAPI AI 工具集

loom-scala

编程与开发

查看项目

更新于 10-19 00:15

技能介绍

概述

loom-scala 提供面向 JVM 的惯用法、生产级 Scala 3 知识,假定读者已经会写 Scala。内容聚焦实际开发中容易出问题的地方:import 静默漏掉的 given、IO.pure 提前且只执行一次的副作用、在计算池上的阻塞调用、只给出警告的非穷尽匹配,以及什么都没运行却以 0 退出的 sbt testOnly。

工具链与构建

默认构建工具为 sbt。建议保持一个 sbt shell(或 sbt --client)常开以支持迭代,因为每次批量调用都会先启动 JVM 并加载构建。

常用命令:

  • 完整检查:sbt scalafmtCheckAll scalafmtSbtCheck "scalafixAll --check" test
  • 编译主代码与测试:sbt Test/compile
  • 全部测试:sbt test
  • 单个套件:sbt 'testOnly com.acme.FooSuite'
  • 按子串选单个 ScalaTest 测试:sbt 'testOnly com.acme.FooSpec -- -z "rejects symlinks"'
  • 仅失败与变更测试:sbt testQuick(~testQuick 保存即重跑)
  • 格式化:sbt scalafmtAll scalafmtSbt
  • 依赖树:sbt dependencyTree(需先在 project/plugins.sbt 中 addDependencyTreePlugin)

示例 build.sbt 使用 Scala 3.3.4(3.3.x 为 LTS),启用 semanticdbEnabled(scalafix 语义规则需要),依赖 cats-effect、munit、munit-cats-effect,并设置 Test / fork := true 让测试在独立 JVM 中运行。根项目通过 .aggregate(core) 聚合子项目。

%% 会追加 Scala 二进制版本,% 用于 Java 库。Scala 3 可通过 .cross(CrossVersion.for3Use2_13) 使用 Scala 2.13 构件,但带 Scala 2 宏的除外。sbt-tpolecat 会按版本设置严格编译标志(含 -Werror);scalafmt 读取 .scalafmt.conf,需设置 version 与 runner.dialect = scala3。Scala CLI(3.5 起的 scala 命令)和 Mill 是另外的构建工具,均不使用 build.sbt。

Scala 3 语法

  • 用 enum 与 sealed trait 定义代数数据类型,配合模式匹配;意义缩进取代花括号,then/do 跟随条件,end 标记结束长块。
  • 顶层定义取代 package object;export 转发成员以组合代替手写委托。
  • 支持联合类型(String | Int)与交集类型(A & B);derives 请求类型类派生。
  • opaque type 提供无运行时开销的独立类型;AnyVal 值类在集合与泛型中会装箱,opaque 类型不会超出其底层类型装箱。
  • 从 2.13 迁移可用 -source:3.0-migration -rewrite 修正语法。

ADT 与模式匹配

sealed trait 和 enum 让编译器检查穷尽性,但检查只警告,因此要开启 -Werror。编译器看不穿守卫条件,需为每个变体保留无守卫分支。类型模式会被擦除(case xs: List[String] 只给 unchecked 警告)。无匹配分支会在运行时抛 MatchError;collect 显式表达偏函数。自定义提取器可让 case Email(user, domain) 作用于普通字符串。Scala 3 弃用 lambda 内的非局部返回,改用 scala.util.boundary 的 boundary/break。

Given、Using 与扩展方法

类型类实例应放在类型类或数据类型的伴生对象中(隐式作用域,无需导入);无关对象中的孤儿实例需显式导入且易引发歧义。import a.* 不会导入 given,需用 import a.given 或 import a.{*, given}。同一类型的两个可用 given 会触发歧义错误,可通过更具体或把后备实例移入低优先级父 trait 解决。using 参数承载上下文(ExecutionContext、事务、配置),普通依赖应改用构造参数。隐式转换需要语言导入,扩展方法可覆盖其合理场景。深类型类树的自动派生编译慢,逐类型半自动 derives 可保持编译时间平稳。

错误处理

用 Either[E, A] 加错误 ADT 表达预期失败,Option 表示缺失,Try 仅用于捕获 Java 或遗留 API 的异常并在边界转为 Either。缺陷(bug)应抛出;在效应类型内部,失败通过效应传递(IO.raiseError、ZIO 的错误通道)。捕获时用 case NonFatal(e) =>。scala.util.Using.resource 和 Using.Manager 在所有路径上关闭 AutoCloseable 资源。

效应系统:Cats Effect 与 ZIO

每个代码库只选一种效应系统。Future 是急切、记忆化且不可取消的,适合简单异步粘合,其中阻塞代码应放入 blocking {}。

Cats Effect 要点:IO 值描述计算,运行时执行前什么都不发生;IO.pure(expr) 在构造时求值一次;副作用放入 IO(...)/IO.delay;阻塞调用用 IO.blocking 或 IO.interruptible;有释放步骤的资源用 Resource;共享状态用 Ref、一次性信号用 Deferred、纤程间交接用 Queue;unsafeRunSync() 只应在程序边界使用。

ZIO 要点:ZIO[R, E, A] 携带环境、类型化错误与结果;预期失败放入 E,缺陷(die)用于 bug;ZIO.succeed 按名传参故延迟执行,可能抛异常的代码用 ZIO.attempt,阻塞代码用 ZIO.attemptBlocking;ZLayer 装配依赖,ZIO.acquireRelease 配合 ZIO.scoped 管理资源,withParallelism(n) 限制并发扇出。

集合

默认不可变:List 头部插入 O(1),索引与 :+ 为 O(n);Vector 索引与追加接近常数。局部构建用 ListBuffer/ArrayBuffer,返回不可变结果。.view 使链式惰性,Iterator 单次遍历,LazyList 会记忆化。Map.mapValues 与 filterKeys 在 2.13/3 返回每次访问重算的惰性视图(已弃用),应写 .view.mapValues(f).toMap。toMap 遇重复键静默保留最后值;Array 相等是引用相等,应使用 sameElements 或 ArraySeq。深 foldRight 可能导致栈溢出。

测试

MUnit(munit.FunSuite)提供 test、带差异输出的 assertEquals、intercept[E]、FunFixture 每次测试的初始化与清理;munit-cats-effect 的 CatsEffectSuite 可运行 IO 测试。ScalaTest 提供多种风格与 Matchers、AsyncFunSuite。ZIO Test 用 ZIOSpecDefault 与 assertTrue,TestClock 控制时间。属性测试可用 ScalaCheck 的 forAll。不 fork 时 sbt 在自己的 JVM 内并行运行套件;Test / fork := true 时在单个独立 JVM 中依次运行(除非 testGrouping 拆分)。共享文件、端口或全局状态的套件需 Test / parallelExecution := false 或隔离。

Loom 测试运行适配器

Scala 使用 sbt:含 build.sbt 的目录类型为 scala,其运行器始终是 sbt;loom project detect 会逐包打印。Mill 与 Scala CLI 项目没有 build.sbt,因此检测不到。单测试命令为 sbt 'testOnly {test}',以存放 build.sbt 的包目录为工作目录;test 字段为完全限定套件名(ZIO Test 为源码中的对象名),该命令选择整个套件,不传单测过滤。名称无匹配时 sbt 打印 “No tests to run for Test / testOnly” 并退出 0,loom 读取运行器摘要而非仅看退出码,未选中任何测试会被归类为 NotSelected 并判定失败。

编写契约测试:测试源码放在拥有该代码的子项目的 src/test/scala/ 下;每个契约一个只含该契约测试的套件并以契约命名;包声明与目录路径保持一致;根项目需 .aggregate(子项目),否则根上的 testOnly 到不了未聚合的子项目;无显式根项目时会得到聚合全部子项目的默认根。契约套件在冻结时若尚不能编译,会被归类为 BuildFailed,因为 sbt 会整体编译子项目的 Test 配置。