Scala 30:Java互操作的类型与运行边界
能读取Java签名还不等于程序能运行 Scala 与 Java 都可以生成 JVM 类文件,但互操作不能只用“能导入一个 Java 类”验收。编译器可能读到源文件中的签名,运行时却找不到对应 class;集合转换可能改变可见接口,却仍共享底层可变对象;Java 返回 null 时,默认 Scala 引用类型也不自动得到非空保证。 本章把这些问题分开实验。独立工程先用 javac 编译 Java 库,再编译调用该库的 Scala 对象,然后用 javac 编译真正的 Java 客户端,最后直接执行 java。普通章节入口另外验证集合视图、快照、单抽象方法接口和空值行为。 冻结环境为 Scala 3.3.7、Scala CLI 1.9.1、JDK 21.0.11。带显式空值检查的样例单独使用 -Yexplicit-nulls,没有把这个选项注入整套默认构建。这样才能比较默认模式与显式空值模式各自接受什么,而不是把两套规则混成一个描述。 双向调用需要真正的两种编译产物 Java 库提供一个静态乘三方法,Scala 对象提供乘二与调用 Java 的方法,Java 客户端再调用这两个 Sc...
Scala 29:编译管线、TASTy与增量编译
同一行修改为什么有时只编译一个模块 一个小库提供 twice,应用调用它得到整数。把实现从乘二改成乘三,应用没有改源码,运行结果却应当变化;把返回类型从 Int 改成 Long,应用中的显式整数变量又必须重新接受类型检查。这两个修改可以用来区分编译器阶段、产物格式与构建工具的增量判断。 本章使用 Scala 3.3.7、sbt 1.11.7、Scala CLI 1.9.1 和 JDK 21.0.11。独立工程包含 core 与 app,运行脚本复制工程到临时目录,按顺序修改副本,保存每次命令、退出码、编译输出及产物摘要。源码模板不会被实验过程反复改写,下一次运行仍从相同起点开始。 1234567object Core: def twice(n: Int): Int = n * 2object Main: def main(args: Array[String]): Unit = val result: Int = Core.twice(21) println(s"result=$result") 正常值从四十二变成六十三、返回类型修改引发拒绝、...
Scala 28:擦除、装箱与字节码
三段源码都返回整数,JVM 接口却不同 泛型恒等函数接收整数并返回整数,普通方法也接收整数并返回整数,闭包还可以捕获一个整数再执行加法。只看 Scala 调用结果,三者都很简单;如果要解释装箱、桥接方法或闭包表示,就必须查看实际 class 文件。 123def identity[A](value: A): A = valuedef increment(value: Int): Int = value + 1def addCaptured(offset: Int): Int => Int = n => n + offset 本章在 Scala 3.3.7、Scala CLI 1.9.1、JDK 21.0.11 上编译这些方法,并保存 javap -c -p -s -v 原始输出。正例断言 identity(7) == 7、increment(7) == 8 和 addCaptured(5)(3) == 8。三个值正确只是起点,字节码才能回答它们用什么描述符、调用了哪些转换方法。 这组实验不直接测量性能。字节码里出现装箱调用,不代表每次热执行都发生新的堆分配;没有显式 ...
Scala 27:Quotes、splices 与宏阶段边界
检查固定数量,同时保留调用者的求值次数 数量配置必须是编译时常量,并处于一到一百之间。上一章用 inline 条件能处理简单约束;当错误需要定位到具体参数,或生成逻辑需要引入局部变量时,Quotes 宏提供了更直接的有类型代码构造方式。 12inline def quantity(inline n: Int): Int = ${ quantityImpl('n) } 调用 quantity(10) 会在编译阶段执行实现中的检查,并把合法结果放进生成程序。调用 quantity(101) 被拒绝。传入一个普通运行时方法参数,也被明确拒绝,而不会自动先运行应用再取回它的值。 第二个需求是生成一个二元组,两项都使用同一次输入计算结果。若简单把输入代码放两次,副作用就可能执行两次。宏必须生成局部绑定,保存一次求值结果,再读取两次。这个需求把类型正确、阶段正确和运行语义正确放在同一个可观察例子里。 实验冻结 Scala 3.3.7、Scala CLI 1.9.1、JDK 21.0.11。宏定义在 snippets/27/Checked.scala,调用在 s...
Scala 26:Mirror、derives 与递归派生
为字段新增编码逻辑,哪些工作可以自动完成 订单行包含商品名称和数量。手写一个文本编码器很直接: 123final case class Item(name: String, quantity: Int)val item = Item("book", 2)val handwritten = s"Item(${item.name},${item.quantity})" 增加一个币种字段后,要同步修改编码逻辑;把订单行放进购物篮,外层又需要组合编码器;再遇到“叶子或分支”的递归树,手写模式匹配还要覆盖每个分支。重复部分确实与类型结构有关,但哪些步骤能够派生,哪些仍然需要业务定义,必须先区分。 本章实现一个教学用 Encoder[A],输出类似 Basket(Item(book,2)) 的文本。它不转义字符串,不生成 JSON,也没有解码器,因此不是可直接用于持久化协议的序列化库。实验目的是解释结构信息怎样组合现有行为,以及递归在哪些阶段可能失败。 环境固定为 Scala 3.3.7、Scala CLI 1....
Scala 25:inline 与 compiletime
同样传入整数,为什么一个调用必须在编译时拒绝 配置代码写下固定批量大小 0,应该尽早报错;用户从命令行输入一个整数,程序则必须在运行时验证。这两种入口都处理 Int,但可获得的信息不同。编译器可以检查源码中的常量,却不能预先知道下一次用户输入。 123456789import scala.compiletime.errorinline def positive(inline n: Int): Int = inline if n > 0 then n else error("quantity must be a positive constant")def positiveRuntime(n: Int): Either[String, Int] = if n > 0 then Right(n) else Left("quantity must be positive") 本章正例中 positive(3) 成功展开,positiveRuntime(-1) 返回 Left。隔离反例 positive(0) 在编译期报出指定错...
Scala 24:Match types 与类型归约
一个返回类型,为什么有时是字符、有时无法确定 集合工具想描述“某种输入的元素类型”:字符串对应字符,数组对应数组元素,迭代容器对应容器元素。普通泛型参数可以在调用者给定类型之后保持一致,却不能单凭一个固定类型别名表达这三个选择。Scala 的 match type 允许按输入类型的结构计算结果类型。 1234type Elem[X] = X match case String => Char case Array[t] => t case Iterable[t] => t 在 Scala 3.3.7 中,Elem[String] 可以归约为 Char,Elem[Array[Int]] 可以归约为 Int,Elem[List[String]] 可以归约为 String。但以下函数不能编译: 1def wrong[X]: Elem[X] = 'x' 诊断没有说 match type 语法非法,而是说 Elem[X] 无法完全归约:编译器不知道任意 X 是否匹配第一条 String,也不能证明它与 String 不相交,因此不能跳过该分支。归...
Scala 23:类型成员、路径依赖与细化
两个仓库的键,为什么不能互换 订单仓库和用户仓库都使用整数编号。若两个接口都接收 Int,orders.fetch(userId) 可以顺利编译,即使编号恰好存在于另一个业务域。第 16 篇的 opaque type 能区分固定业务类型;当隔离依据变成“由哪一个仓库实例创建”时,路径依赖类型提供了另一种表达方法。 1234567final class Repository: final case class Key(value: Int) def fetch(k: Key): Int = k.valueval orders = new Repositoryval users = new Repositoryval wrong = orders.fetch(users.Key(1)) 本章隔离反例的最后一行无法编译。Scala 3.3.7 报告找到的是 users.Key,需要的是 orders.Key。整数相同、内层类名称相同、外层类定义相同,都不能替代所属路径相同这一条件。这一拒绝是本章要解释的结果。 路径依赖类型并不要求编译器读出运行时对象内容,再按内容任意计算新类型。这...
Scala 22:联合、交叉与相等检查
同一个输入接口,三种不同的约束 订单查询接口接收两种标识:新系统使用 OrderId,外部导入文件暂时使用字符串。实现需要保留两种输入的区别,却不想要求两个类型继承同一个业务父类。另一处资源清理函数要求对象既能提供名称,也能关闭。还有一次重构把客户编号传给订单编号比较,代码能够运行,结果却始终为假。这三个问题分别涉及联合类型、交叉类型和相等比较的准入规则。 它们都能缩小错误出现的范围,但表达的约束不同。OrderId | String 表示传入值可以来自其中任何一类;Named & Closeable 要求同一个值满足两份接口;CanEqual[A, B] 表示类型系统允许两类值使用 ==、!= 比较。第三项不说明比较结果,更不替代业务身份定义。把三者统称为“更强的类型安全”,不足以解释编译器为什么接受或拒绝一行程序。 以下实验冻结 Scala 3.3.7、Scala CLI 1.9.1、Amazon Corretto 21.0.11。相等检查开关只用于单独的反例或局部代码,不改变整个实验工程的默认语言模式。完整正例在仓库 examples/scala-lab/snipp...
Scala 21:上下文函数,依赖传递怎样成为函数类型
保存一份报价计算,费率什么时候才需要出现 订单金额已经确定,但费率由稍后的活动选择决定。可以保存 Rate => Int 普通函数,执行时显式传费率;也可以保存 Rate ?=> Int 上下文函数,让执行点像调用 using 方法一样补全参数。两者都表达“还需要一个 Rate 才能得到结果”,差别在参数传递形式。 这个场景里有三个时间点:普通工厂创建计算方案,上下文函数真正计算费用,某个闭包提前捕获了特定费率。如果只看最终数字,可能分不清费率来自创建点还是执行点,也可能把尚未执行的函数误当作已经完成的报价。 本文固定 Scala 3.3.7、标准库 2.13.16 和 JDK 21,用普通 Plan 工厂中的计数观察方案构造,用计算体中的独立计数观察执行,再与提前捕获费率的普通闭包对照。计数放在真正边界内,不由调用者手工增加一个数字后拿它自证发生了构造。 问号箭头描述需要上下文参数的函数 12case class Rate(percent: Int)type Pricing = Rate ?=> Int Pricing 是一种函数类型。它需要一个 Rate 上...
