本文由 Jilen 發表在 ScalaCool 團隊部落格。
shapeless 是一個型別相關的庫,提供了很多有趣的功能。
本文介紹其中一個重要功能:自動派生 typeclass 例項。
Hlist
Shapeless 實現了 HList
,不同於 Scala 標準庫的 Tuple 的扁平結構,HList 是遞迴定義的,和標準庫 List 類似。
HList 可以簡單理解成每個元素型別可以不同 List
簡化後的 HList
sealed trait HList
case object HNil extends HNil
case class ::[+H, +T <: HList](head : H, tail : T) extends HList複製程式碼
很容易看出 HList 可以對應到任意 case class,例如
case class Foo(a: Int, b: String, c: Boolean)
Int :: String :: Boolean :: HNil複製程式碼
而 shapeless 也提供 Generic 物件實現任意 case class 例項和對應的 HList 之間的轉換。
Generic 物件
trait Generic[T] extends Serializable {
def to(t : T) : Repr
def from(r : Repr) : T
}
object Generic {
type Aux[T, Repr0] = Generic[T] { type Repr = Repr0 }
...
}複製程式碼
Miles 設計這個物件不侷限於 case class
,只是很鬆散的定義 T
和 Repr
之間互相轉換方法。
很多人可能疑惑這個方法為什麼不設計成兩個型別引數 Generic[A, B]
,這實際上是為了使用 Generic.Aux
繞過編譯器限制。
具體可以檢視此處
case class 和 HList 互相轉換
由於 HList 和 case class 可以一一對應,所以我們很容易想到
Generic.Aux[Foo, Int :: String :: Boolean :: HNil]複製程式碼
這樣的 Generic
物件就可以實現 Foo
和 Int :: String :: Boolean :: HNil
之間的相互轉換。
而且 shapeless 會自動使用 macro 生成這樣的 Generic
物件
scala> case class Foo(a: Int, b: String, c: Boolean)
defined class Foo
scala> Generic[Foo]
res0: shapeless.Generic[Foo]{type Repr = shapeless.::[Int,shapeless.::[String,shapeless.::[Boolean,shapeless.HNil]]]} = anon$macro$4$1@42db6e8e複製程式碼
自動派生 typeclass 例項
現在假設我們要設計一個 typeclass
trait Show[A] {
def show(a: A): String
}複製程式碼
其功能是可以將任意 case class 例項顯示成字串。為了簡化問題,我們定義以下顯示規則。
Int
型別直接顯示為數值Boolean
型別直接顯示為true
或false
String
型別用引號包圍,例如"str"
case class
顯示為[]
包圍的屬性列表,屬性之間逗號隔開[field1, field2, field3...]
我們很容易實現基本型別的 Show
例項
基本型別 Show 例項
implicit val intShow: Show[Int] = new Show[Int] {
def show(a: Int) = a.toString
}
implicit val stringShow: Show[String] = new Show[String] {
def show(a: String) = "\"" + a + "\""
}
implicit val booleanShow: Show[Boolean] = new Show[Boolean] {
def show(a: Boolean) = if(a) "true" else "false"
}複製程式碼
現在來看看如何派生任意 case class 的 Show 例項。當然我們可以通過反射或者 macro 實現,這裡我們展示 shapeless 如何利用 scala 編譯器自動推匯出需要例項
任意
case class
的Show
例項
implicit val hnilShow: Show[HNil] = new Show[HNil] {
def show(a: HNil) = ""
}
implicit def hlistShow[H, T <: HList](
implicit hs: Show[H],
ts: Show[T]
): Show[H :: T] = new Show[H :: T]{
def show(a: H :: T) = hs.show(a.head) + "," + ts.show(a.tail)
}
implicit def caseClassShow[A, R <: HList](
implicit val gen: Generic.Aux[A, R],
hlistShow: Show[R]
): Show[A] = {
def show(a: A) = hlistShow(gen.to(a))
}複製程式碼
我們視覺化以下編譯器自動推匯出 Show[Foo]
的過程

Shapeless 巧妙的利用編譯器自動推導功能,推匯出了任意 case class 物件的
Show
例項。整個過程雖然理解起來很複雜,但規則卻意外的簡單:編譯器自動推導。
這樣例項派生過程就轉化成了
Generic
物件和對應 HList
的 typeclass 派生。
當然,現實應用過程中,我們經常需要屬性名和遞迴以及巢狀定義情況,本文中的實現不支援這些場景,後續文章中,我會介紹這些情況處理。