Scala 20:高阶类型与组合定律,F[_] 抽象了什么
同一个金额变换,能否同时处理 Option 和 List 数量可能缺失时使用 Option[Int],需要处理多个候选时使用 List[Int]。两种情况都能把数量乘以单价,但一个结果最多有一项,另一个可以有多项。若通用算法只关心“保持外部计算形状并变换内部值”,就可以把外层类型也作为参数。 普通 A 抽象一个完整类型,F[_] 抽象接受一个类型参数的类型构造器。把 Int 放进去后,F[Int] 才是完整值类型。List 与 Option 都符合这个形状,Int 本身则不符合。这里的下划线是参数位置,不是一个装着任意值的容器对象。 本文固定 Scala 3.3.7、标准库 2.13.16、JDK 21,手写最小 Functor 与 Monad 接口,分别用于 Option 和 List,再用类型 lambda 固定 Either 的错误参数。定律通过有限输入的断言检验,属于回归证据,不是对所有函数和所有输入的形式证明。 从 A 到 F[A] 多抽象了一层 12trait Functor[F[_]]: def map[A, B](fa: F[A])(f: A => B):...
Scala 19:Type class,不修改业务类型也能添加行为
Order 没有 encode 方法,怎样进入通用编码流程 订单类型可能来自另一个模块,不能为了每个输出格式都修改它。若把编码方法直接放进 Order,又会遇到同一种订单需要简洁文本、审计文本和外部协议三种策略的问题。编码行为与订单数据可以分离,由一个独立对象说明某种类型怎样被编码。 这种组织方式通常称为类型类。Scala 中不需要特殊的 typeclass 关键字:定义带类型参数的行为接口,为具体类型提供实现,再让通用算法通过参数接收该实现。given 与 using 可以减少重复传递,但不是类型类成立的唯一条件,显式传普通对象同样能表达核心设计。 本文使用 Scala 3.3.7、标准库 2.13.16 和 JDK 21,定义最小 Encoder[A],组合嵌套列表实例,并显式选择两种 Order 编码。输出只是教学文本,不是完整 JSON 或稳定外部协议。实验不引入第三方库,也不把实例搜索成功当作编码语义正确的证明。 行为接口接收数据,而不是要求数据继承它 12345trait Encoder[A]: def encode(value: A): Stringdef ren...
Scala 18:given 与 using,上下文参数从哪里找到
同一笔金额,省略费率之后用了哪一个 金额一千分,费率百分之十,费用是一百分。显式传入费率时,调用关系很容易看懂;把费率变成上下文参数后,调用只剩 fee(1000),仍然需要解释那份费率从哪里来。若当前范围同时出现百分之十与百分之二十两个候选,编译器不应该替业务随意选择。 given 与 using 的核心仍是参数传递。using 声明一个允许编译器补全的参数列表,given 声明可供搜索的实例。省略实参触发的是编译期搜索与选择,并不等同于运行时到全局注册表里按名字查询对象。 本文冻结 Scala 3.3.7、标准库 2.13.16 和 JDK 21,逐步比较显式传参、伴生范围、专用导入、更深嵌套以及同层歧义。Scala 2 的 implicit 搜索存在不同规则,本章不把旧教程中的优先级直接套入,也不声称已经执行两版本迁移对照。 先写一个完全显式的调用 123456case class Rate(percent: Int)def fee(cents: Int)(using rate: Rate): Int = cents * rate.percent / 100val val...
Scala 17:extension 与 API 设计,增加操作是否改变类型
金额能调用 plus,就成了某个金额接口的子类吗 假设 Money 已经定义在共享模型中,订单模块希望写出 price.plus(fee),但不能修改原始类。扩展方法能提供这样的调用形式。接着另一个接口要求 Priced,开发者给 Money 增加一个同名 cents 扩展,试图把 Money 直接赋给 Priced,却仍然编译失败。 原因在于“能够解析某种方法调用”和“这个类型具有某个子类型关系”不是同一件事。扩展方法提供额外的静态调用规则,不会回到原类中增加继承声明,也不会让 JVM 对象在运行时成为另一个接口的实现。使用点语法不意味着方法就在接收者对象中。 本文固定 Scala 3.3.7、标准库 2.13.16、JDK 21,围绕金额求和验证扩展、成员优先、显式调用与 export。它不借助隐式转换包装对象,也不把扩展语法当作自动提供业务不变量的工具。 先把点语法还原成带接收者的函数 12345case class Money(cents: Long)object Syntax: extension (money: Money) def plus(other: M...
Scala 16:opaque type,让订单标识与商品标识不可互传
两个 Long 写反了,编译器为什么没有意见 查询订单的函数接收 Long,调用方手里同时有订单号与商品号。两个值都能编译通过,只有运行时查不到记录,甚至碰巧查到另一条有效记录,才暴露参数误传。把变量命名得更清楚可以帮助阅读,却不能让类型检查拒绝错误调用。 普通别名也不解决这个问题。type OrderId = Long 主要提供一个名称,仍然与底层 Long 等价;ProductId 若也只是 Long 别名,两者仍能互换。需要的是对外保持区别,对内继续使用同一个表示,而不是只增加注释。 opaque type 提供这种抽象边界。本文固定 Scala 3.3.7、标准库 2.13.16 和 JDK 21,既验证错误标识不能互传,也检查数量受控构造与实际字节码。对于“零成本”的说法,本文只报告所观察到的方法描述符和装箱,不推广成任何使用方式都不分配。 定义区域内透明,区域外保持抽象 12345object Domain: opaque type OrderId = Long opaque type ProductId = Long def orderId(raw: Long...
Scala 15:错误短路与错误累积,校验顺序怎样决定结果
商品为空、数量不是数字,应该返回几条错误 订单表单同时提交空商品标识与数量字符串 “bad”。如果使用 Either.flatMap 串起来,商品检查先失败,数量检查不会执行,结果只有商品错误。若界面希望一次展示所有独立字段问题,就需要另一种组合方式:两个检查都运行,失败信息按规定顺序合并。 但“尽量返回所有错误”不能无限制推广。数量没有解析成整数之前,库存是否足够没有可靠输入。用零代替坏字符串再检查库存,会制造一条并不描述原始问题的错误。独立校验可以累积,依赖校验需要等待前置成功,区分这两种关系比选择哪个容器更重要。 本文使用 Scala 3.3.7、标准库 2.13.16、JDK 21,不引入效果库。最小累积组合器只负责两个已经完成的结果;它不负责并行、取消、超时或网络重试。实验同时检查返回内容、字段顺序和回调次数,避免把“错误列表类型”误解成“自动累积所有错误”。 先让错误保留字段位置与稳定代码 123case class FieldError(field: String, code: String)type Checked[A] = Either[Vector[Field...
Scala 14:纯函数、递归与尾调用,计算怎样保留可替换性
同样两件商品,为什么两次计算价格不同 一个价格函数接收数量,却从外部可变变量读取单价。第一次单价一百分,两件得到二百分;随后外部把单价改成二百分,传入同样数量就得到四百分。函数签名没有显示这个额外输入,所以仅靠参数无法重现结果。 另一种问题发生在递归汇总。对三条订单行,递归相加很直观;对十万条输入,若每层都要等待下一层返回后继续加法,调用栈可能持续增长。给方法加上尾递归注解并不能把任意递归变成循环,必须先改变计算结构。 本章固定 Scala 3.3.7、标准库 2.13.16、JDK 21,分别检验显式状态传递、局部可变实现、尾位置及大输入结果。纯函数与尾递归解决不同问题:前者关注可观察依赖,后者关注自递归控制流。一个尾递归方法仍然可以读取全局变量,一个纯函数也仍然可能使用过深的非尾递归。 把隐藏状态变成显式输入和输出 12345678case class Cart(total: Long)def add(cart: Cart, amount: Long): Cart = Cart(cart.total + amount)val old = Cart(100)val updat...
Scala 13:for 推导式,语法相同为何执行不同
两个生成器,是四个结果还是一个结果 数量列表为一、二、三,单价候选为一百分、二百分。过滤掉数量一之后,每个数量都与两种单价组合,得到二百、四百、三百、六百。如果把数量换成 Option(2),同样的 for 外形却只得到一个可选金额;换成 Left(“quantity”),后面的价格获取甚至不会执行。 for 没有给这些容器强加同一种运行策略。编译器把语法转换成它们支持的方法调用,具体集合或容器决定调用函数几次、怎样组合结果、失败是否短路。因此,理解 for 的入口应该是生成器的类型和所需方法,而不是把所有箭头都解释成普通循环。 本章冻结 Scala 3.3.7、标准库 2.13.16 和 JDK 21。for 的转换规则会随语言版本演进,本文以该版本的 Desugar.makeFor 源码和实际程序为准,不把后续 Better fors 的优化套进旧版本。正文展示的是各例的等价调用,不宣称存在适用于所有模式、绑定与版本的单行模板。 单个生成器把值交给 map 12val amounts = for q <- List(1, 2, 3) yield q * 100val e...
Scala 12:map、flatMap、fold 与 collect,从类型推导变换
三条订单行,需要四种不同结果 输入包含三条订单行:商品甲两件,每件一百分;商品乙一件,三百分;商品甲三件,每件一百分。报表要各行金额,发货任务要逐件商品列表,统计要订单总额,界面只显示数量至少为二的行。它们遍历同一列表,输出形状却不同。 map 保持一项输入对应一项输出;flatMap 允许一项输入对应零项或多项,再把结果接成一层;fold 把输入归入累加状态;collect 只处理部分函数定义的输入。先写输出类型,通常比背方法名更容易找到正确组合。本文以 Scala 3.3.7、标准库 2.13.16、JDK 21 运行断言,不把小整数样例冒充完整货币计算规则。 map 保留每项对应关系 12345678case class Line(product: String, quantity: Int, cents: Long)val lines = List( Line("a", 2, 100), Line("b", 1, 300), Line("a", 3, 100))val amounts = lines.map...
Scala 11:Iterator、View 与 LazyList,延迟之后发生什么
同一份变换读两次,为什么有时没数据,有时又算一遍 把订单行转换成金额,只取前两行,然后再次读取。Iterator 可能从第三行继续,View 可能重新执行前两行的变换,LazyList 则可能复用已经计算的结果。它们都能写成 map 后接 take,但“延迟”没有说明消费位置、缓存和对象保留的差异。 这个问题不能只用返回列表是否相等来回答。两个结果一样,回调可能执行两次;第二次结果为空,可能是游标已经耗尽;回调没有重跑,可能是结果被缓存并继续占用内存。需要同时观察值、执行次数和引用关系,才能判断某个接口是否适合订单预览、重复报价或持续流式读取。 本章固定 Scala 3.3.7、标准库 2.13.16 和 JDK 21。测试用有限消费控制无限来源,用计数器观察计算,用对象身份观察记忆化。本章没有堆转储或 GC 回收实验,因此不报告释放字节数,也不把保留引用的推导冒充实测泄漏规模。 Iterator 保存消费位置 12345678910var calls = 0val it = List(1, 2, 3).iterator.map { n => calls += ...
