Scala 类型系统实战:泛型、ADT、协变逆变与类型类

深入 Scala 类型系统:泛型类与方法、代数数据类型(ADT)、Option/Either 的类型结构、协变逆变与不变、类型边界(上下界)、隐式与类型类、路径依赖类型,并通过实战代码展示类型系统如何提升正确性。

引言

Scala 类型系统是 JVM 上最强类型系统之一,也是「为什么写 Scala 不容易出错」的底气。它允许你用类型本身来表达领域约束:Option[Int] 告诉你「可能没有值」、Either[String, Order] 告诉你「失败带原因」、case class 建模出的 ADT 让非法状态根本编译不过。

本文从四个层次系统理解 Scala 类型系统:泛型(抽象出通用的类型结构)、ADT 与 case class(用类型建模领域)、协变/逆变(为什么 List[Int] 是 List[AnyVal] 的子类型)、类型类与隐式(为类型附加行为)。每一层都用「如何帮你写出更安全的代码」来落地。

前置:函数式基础(https://plumephp.com/scala-functional-programming/)。本文的类型类模式与隐式推导是该文的深化。


目录


1. 类型系统的价值:用类型阻止 bug

1.1 类型即文档、类型即契约

// 类型声明就说明了一切
def processOrder(orderId: Long): Either[OrderError, Order]
// 读签名即知:可能失败、失败带原因、成功返回 Order

1.2 编译时 vs 运行时

维度弱类型Scala 强类型
检查时机运行时报错编译期拦截
空指针常见Option 消除
类型混用隐式转换出错编译拒绝

1.3 让非法状态无法表达

// ❌ 用 Boolean 表达状态:可漏
def validate(x: Int): Boolean

// ✅ 用密封类型:状态穷尽
sealed trait ValidationResult
case object Valid extends ValidationResult
case class Invalid(reason: String) extends ValidationResult

2. 泛型:参数化类型与方法

2.1 泛型类

case class Box[A](value: A)

val intBox: Box[Int]    = Box(42)
val strBox: Box[String] = Box("hello")

2.2 泛型方法

def headOr[A](list: List[A], default: A): A =
  list.headOption.getOrElse(default)

headOr(List(1,2), 0)     // 1
headOr(List.empty, 0)    // 0

2.3 泛型约束:让类型更精确

// 约束 A 必须有序,才能 max
def maxOf[A: Ordering](list: List[A]): A =
  list.max

// 约束 A 是 Number 的子类
def sumNumbers[A <: Number](list: List[A]): Double =
  list.map(_.doubleValue).sum

3. ADT:用代数数据类型建模领域

3.1 什么是 ADT

ADT(Algebraic Data Type)由「和类型(sealed trait)」与「积类型(case class)」组合而成:

sealed trait Payment
case class Card(number: String, exp: String) extends Payment
case class BankTransfer(account: String)     extends Payment
case object Cash                              extends Payment
  • 和类型:Payment 是 Card、BankTransfer、Cash 之一。
  • 积类型:Card 同时持有 number 与 exp 两个字段。

3.2 ADT 的价值

  1. 穷尽匹配:match 编译器检查所有 case。
  2. 非法状态不可表达:不能造出「既是 Card 又是 Cash」的值。
  3. 可扩展:新增一种支付方式,编译器强制你处理。

3.3 模式匹配 + sealed 的穷尽检查

def fee(p: Payment): Double = p match {
  case Card(_, _)             => 0.5
  case BankTransfer(_)        => 1.0
  case Cash                    => 0.0
  // sealed + match 穷尽:少一个 case 编译报错
}

4. case class 与模式匹配

4.1 case class 自动提供的能力

case class Order(id: Long, items: List[Item])

// 自动:equals/hashCode/toString/copy/解构/构造器
val o1 = Order(1, Nil)
val o2 = o1.copy(id = 2)       // 复制并改部分字段
val Order(id, items) = o2      // 解构

4.2 值相等 vs 引用相等

case class 默认值相等:Order(1, Nil) == Order(1, Nil) 为 true。普通 class 是引用相等。这让 case class 天然适合值对象建模。

4.3 在集合与匹配中使用

val orders = List(Order(1, List("a")), Order(2, Nil))
orders.collect {
  case Order(id, items) if items.nonEmpty => s"订单$id 有商品"
}

5. 协变、逆变与不变

5.1 三种变化

类型定义直觉
协变 +AList[Int] 是 List[AnyVal] 子类型只读容器适合
逆变 -AFunction1[A,B] 参数逆变输入方向
不变 A严格类型,MutableList[Int] ≠ MutableList[AnyVal]可变容器

5.2 协变的例子

// List 是协变的:List[Int] <: List[AnyVal]
val ints: List[Int] = List(1, 2)
val anys: List[AnyVal] = ints    // ✅ 合法

// Array 是不变的:不能这样赋值
val arrInt: Array[Int] = Array(1)
// val arrAny: Array[AnyVal] = arrInt  // ❌ 编译错误

5.3 为什么可变容器必须不变

// 若 Array 协变:
val arrInt: Array[Int] = Array(1)
val arrAny: Array[AnyVal] = arrInt   // 假想合法
arrAny(0) = "hello"                  // 把字符串放进 Int 数组 → 运行崩溃
// 所以 Scala 让 Array 不变,编译期就拦住

5.4 方差与类型安全

要点:协变安全的前提是「只读不写」。凡可写入的类型参数必须不变。


6. 类型边界:上下界约束

6.1 上界(A <: B)

// A 必须是 Animal 的子类型
def feed[A <: Animal](a: A): Unit = a.eat()

6.2 下界(A >: B)

// 协变容器里写值要用下界避免类型漏洞
class Container[+A](private val value: A) {
  def set[B >: A](newVal: B): Container[B] = new Container[B](newVal)
}

6.3 上下界结合

// 经典案例:排序时允许更宽的类型
def sortPets[A <: Pet](pets: List[A]): List[A] = ...

7. 类型类:为类型附加行为

7.1 传统面向对象的多态 vs 类型类

// OO 方式:类型自身实现方法
trait JsonSerializable { def toJson: String }
case class User(n: String) extends JsonSerializable

// 类型类方式:行为与类型分离,可扩展已有类型
trait JsonWriter[A] { def write(a: A): String }

given userWriter: JsonWriter[User] = u => s"""{"name":"${u.n}"}"""
given intWriter: JsonWriter[Int] = i => i.toString

7.2 类型类的优势

维度继承多态类型类
给已有类型加行为❌ 无法修改✅ given 即可
类型间自由组合受限灵活
对类型约束侵入无侵入

7.3 实战:泛型函数约束类型类

def toJsonString[A](a: A)(using w: JsonWriter[A]): String =
  w.write(a)

toJsonString(User("alice"))   // 自动找 given userWriter
toJsonString(42)              // 自动找 given intWriter

7.4 类型类生态:Cats、Circe

// Circe:JSON 编解码的类型类
import io.circe._, io.circe.syntax._
val json = User("alice").asJson   // 自动编码

8. 路径依赖与内部类型(进阶)

8.1 路径依赖类型

class Database {
  class Connection
  def connect: Connection = new Connection
}

val db1 = new Database
val db2 = new Database
// db1.Connection 与 db2.Connection 是不同类型
// val c: db2.Connection = db1.connect  // ❌ 类型不匹配

8.2 用途

路径依赖类型用于表达「某个实例相关的类型」:如类型安全的资源句柄、依赖注入的精确类型。

8.3 何时值得用

大多数业务代码用不到;但理解它有助于读懂框架源码(如 Akka 内部)。


9. 总结:类型系统实战清单

9.1 类型系统四层武器

层工具解决的 bug
泛型参数化类型重复代码、类型混用
ADTsealed trait + case class非法状态
方差协变/逆变可变容器写穿类型
类型类given/using行为扩展与解耦

9.2 实战建议

建议理由
领域建模用 sealed trait穷尽匹配、非法状态不可表达
DTO 用 case class值相等、copy、解构
可变容器注意方差只读用协变、可变必须不变
行为扩展用类型类无侵入、可组合

9.3 一句话心法

Scala 类型系统的目标是「让编译期替你挡掉大部分 bug」:学会用 ADT 建模、用类型类扩展、理解方差约束,你写出的代码会「不合法就不编译」。


延伸阅读

  • https://plumephp.com/scala-functional-programming/ — Option/Either 的类型结构与模式匹配
  • https://plumephp.com/scala-collections/ — 协变在集合库中的体现
  • Scala 类型系统官方文档 与 Cats 文档

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「scala」更多文章

  1. 纯函数式效果系统实战:Cats Effect IO 与 ZIO
  2. Scala.js 与 Scala Native:跨平台编译、互操作与工程实践
  3. Scala 领域建模实战:ADT、类型驱动设计与模块化架构