1. 概述

泛型编程(Generic Programming)是一种避免重复代码、提升代码复用性的编程方式。在 Scala 中,shapeless 库通过引入泛型数据类型、类型类(type-class)以及值级到类型级的计算能力,极大简化了泛型编程的实现。

本文将介绍 shapeless 的一些典型使用场景。由于泛型编程本身是一个非常宽泛的话题,我们不会面面俱到,而是聚焦于几个核心特性。

2. 泛型与类型级编程

泛型编程是一种将具体类型延迟指定、以通用方式编写程序的技术。

举个例子,与其为 IntString 分别定义 IntListStringList,不如直接使用泛型 List[T],并为其定义通用操作如 headtailmapsize 等。这样可以一次编写,多处复用:

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 支持 headtaildropmap 等操作。

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())

结果为嵌套的 InlInr

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. 总结

本文我们学习了:

✅ 泛型编程与类型级编程的基本概念
✅ 如何使用 HListCoproduct 构建异构数据结构
GenericLabelledGeneric 的使用方法
✅ 多态函数的实现方式

这些工具让 Scala 的泛型编程能力大幅提升,也为 JSON 序列化、CSV 编码等通用场景提供了强大支持。

本文代码示例可在 GitHub 获取。


原始标题:Introduction to Generic Programming in Scala with shapeless