Shapeless入門指南(一):自動派生 typeclass 例項

ScalaCool發表於2017-09-15

本文由 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,只是很鬆散的定義 TRepr 之間互相轉換方法。
很多人可能疑惑這個方法為什麼不設計成兩個型別引數 Generic[A, B],這實際上是為了使用 Generic.Aux 繞過編譯器限制。
具體可以檢視此處

case class 和 HList 互相轉換

由於 HList 和 case class 可以一一對應,所以我們很容易想到

Generic.Aux[Foo, Int :: String :: Boolean :: HNil]複製程式碼

這樣的 Generic 物件就可以實現 FooInt :: 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 型別直接顯示為 truefalse
  • 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 classShow 例項


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 派生。

當然,現實應用過程中,我們經常需要屬性名和遞迴以及巢狀定義情況,本文中的實現不支援這些場景,後續文章中,我會介紹這些情況處理。

相關文章