1. 概述
泛型编程(Generic Programming)是一种避免重复代码、提升代码复用性的编程方式。在 Scala 中,shapeless 库通过引入泛型数据类型、类型类(type-class)以及值级到类型级的计算能力,极大简化了泛型编程的实现。
本文将介绍 shapeless 的一些典型使用场景。由于泛型编程本身是一个非常宽泛的话题,我们不会面面俱到,而是聚焦于几个核心特性。
2. 泛型与类型级编程
泛型编程是一种将具体类型延迟指定、以通用方式编写程序的技术。
举个例子,与其为 Int
和 String
分别定义 IntList
和 StringList
,不如直接使用泛型 List[T]
,并为其定义通用操作如 head
、tail
、map
、size
等。这样可以一次编写,多处复用:
val intList: List[Int] = List(1, 2, 3)
val stringList: List[String] = List("foo", "bar", "baz")
assert(intList.head == 1)
assert(stringList.head == "foo")
shapeless 大量使用 类型级编程(Type-Level Programming) 来实现泛型编程。那什么是类型级编程?
✅ 类型级编程 是一种将计算逻辑编码到类型系统中,并在编译期进行求值的技术。
当我们把值级别的信息提升到类型级别,这些逻辑会在编译期被检查,从而帮助我们编写更安全、更可靠的代码。如果代码能编译通过,那它在运行时基本就是正确的。
来看一个实际问题:假设我们有一个包含不同类型元素的列表:
val list: List[Any] = List(1, 1.0, "One", false)
当我们调用 head
时,得到的是 Any
类型,而不是我们期望的 Int
。这显然不够精确。
那么在 Scala 中,我们如何解决这个问题?✅ 借助类型级编程,我们可以使用异构列表(HList)来保留每个元素的类型信息。
虽然异构列表本身是一个复杂话题,但我们可以直接使用 shapeless 提供的 HList
。它能够在运行时保留每个元素的类型,避免类型擦除的问题。
shapeless 提供了许多数据类型和类型类。本文将重点介绍以下几个:
HList
Coproduct
Generic
LabelledGeneric
- 多态函数(Polymorphic Function)
3. 泛型数据结构
在范畴论中,每个构造都有其对偶(dual),比如乘积类型(product type)的对偶是余积类型(coproduct,也称和类型)。在 shapeless 中:
- 乘积类型对应
HList
- 余积类型对应
Coproduct
3.1. 异构列表(HList)
HList
结合了 列表(List) 和 元组(Tuple) 的特性:
- 元组:固定长度,元素类型不同,但定义后不能扩展
- 列表:长度可变,但所有元素类型相同
✅ HList
的优势在于:长度可变 + 元素类型不同,且类型信息在编译时保留。
import shapeless._
import HList._
val hlist = 1 :: 1.0 :: "One" :: false :: HNil
hlist
的类型是:
Int :: Double :: String :: Boolean :: HNil
这是 shapeless 中的递归数据结构定义:
sealed trait HList extends Product with Serializable
final case class ::[+H, +T <: HList](head : H, tail : T) extends HList
sealed trait HNil extends HList {
def ::[H](h : H) = shapeless.::(h, this)
}
case object HNil extends HNil
HList
是递归结构,每个节点要么是 HNil
(空),要么是 ::[H, T]
,其中 H
是头部元素类型,T
是尾部的 HList
。
我们可以像操作普通集合一样操作 HList
:
assert(hlist.head == 1)
assert(hlist.take(2) == 1 :: 1.0 :: HNil)
assert(hlist.tail == 1.0 :: "One" :: false :: HNil)
✅ 使用场景举例:结合 Generic
,可以将 case class 转为 HList
,进而实现 CSV 编码器等。
3.2. Coproduct
在标准库中,我们通常使用 sealed trait
来表示和类型(sum type):
sealed trait TrafficLight
case class Green() extends TrafficLight
case class Red() extends TrafficLight
case class Yellow() extends TrafficLight
shapeless 提供了 Coproduct
数据类型,它同样是递归结构,功能更强大:
import shapeless._
object Green
object Red
object Yellow
type Light = Green.type :+: Red.type :+: Yellow.type :+: CNil
创建一个 Red
类型的实例:
val light: Light = Coproduct[Light](Red)
我们可以检查其类型:
assert(light.select[Red.type] == Some(Red))
assert(light.select[Green.type] == None)
Coproduct
支持 head
、tail
、drop
、map
等操作。
4. Generic 类型类
Generic
类型类可以将常见的乘积类型(如 case class、tuple)或余积类型(sealed trait 的子类)转换为对应的泛型表示。
trait Generic[T] extends Serializable {
type Repr
def to(t : T) : Repr
def from(r : Repr) : T
}
其中 Repr
是路径依赖类型,表示类型 T
的泛型表示形式:
- case class →
HList
- sealed trait →
Coproduct
4.1. 乘积类型转换(HList)
import shapeless._
case class User(name: String, age: Int)
val user = User("John", 25)
val userHList = Generic[User].to(user)
assert(userHList == "John" :: 25 :: HNil)
转换回来:
val userRecord: User = Generic[User].from(userHList)
assert(user == userRecord)
✅ 可用于实现通用的 CSV 序列化/反序列化工具。
4.2. Coproduct 转换
val gen = Generic[TrafficLight]
val green = gen.to(Green())
val red = gen.to(Red())
val yellow = gen.to(Yellow())
结果为嵌套的 Inl
和 Inr
:
assert(green == Inl(Green()))
assert(red == Inr(Inl(Red())))
assert(yellow == Inr(Inr(Inl(Yellow()))))
5. LabelledGeneric 类型类
Generic
不保留字段名,而 LabelledGeneric
会保留字段名称信息:
import shapeless._
import record._
val user = User("John", 25)
val userGen = LabelledGeneric[User]
val userLabelledRecord = userGen.to(user)
字段名被编码为类型级别的标签:
assert(userLabelledRecord('name) == "John")
assert(userLabelledRecord.keys == 'name :: 'age :: HNil)
✅ 这是 Circe、Argonaut、Play JSON 等库实现自动派生的基础。
6. 多态函数(Polymorphic Functions)
假设我们有一个普通列表:
val list = List("foo", "bar")
我们想获取每个元素的长度:
def length: String => Int = _.length
val lengthList = list.map(length)
这个函数是单态的(monomorphic),只能处理 String
。
但如果是一个异构列表呢?
val list = List(1, 2) :: "123" :: Array(1, 2, 3, 4) :: HNil
每个元素类型不同,怎么办?✅ shapeless 提供了 Poly
类型来定义多态函数:
import shapeless._
object polyLength extends Poly1 {
implicit val listCase = at[List[Int]](_.length)
implicit val stringCase = at[String](_.length)
implicit val arrayCase = at[Array[Int]](_.length)
}
使用方式:
assert(polyLength(List(1, 2)) == 2)
assert(polyLength("123") == 3)
assert(polyLength(Array(1, 2, 3, 4)) == 4)
assert(list.map(polyLength) == 2 :: 3 :: 4 :: HNil)
7. 总结
本文我们学习了:
✅ 泛型编程与类型级编程的基本概念
✅ 如何使用 HList
和 Coproduct
构建异构数据结构
✅ Generic
和 LabelledGeneric
的使用方法
✅ 多态函数的实现方式
这些工具让 Scala 的泛型编程能力大幅提升,也为 JSON 序列化、CSV 编码等通用场景提供了强大支持。
本文代码示例可在 GitHub 获取。