本文由 Jilen 發表在 ScalaCool 團隊部落格。
前面文章中,我們提及了 peano 數型別:Nat
,並且展示了隱式轉換這項 Scala 黑科技的應用。
本文我們通過 HList
的 at
方法來進一步說明 Nat 型別和以及隱式轉換在 shapeless 中的廣泛應用
HList 的 at 操作
前文中提到: HList
可以看成是一個有各種型別連線而成的 List
,如
type Foo = Int :: String :: Boolean :: HNil
val foo: Foo = 1 :: a :: true :: HNil
複製程式碼
HList
有一個 at
函式
scala> foo.at(0)
res4: Int = 1
scala> foo.at(1)
res5: String = a
scala> foo.at(2)
res6: Boolean = true
scala> foo.at(3)
<console>:16: error: Implicit not found: shapeless.Ops.At[shapeless.::[Int,shapeless.::[String,shapeless.::[Boolean,shapeless.HNil]]], shapeless.Succ[shapeless.Succ[shapeless.Succ[shapeless._0]]]]. You requested to access an element at the position shapeless.Succ[shapeless.Succ[shapeless.Succ[shapeless._0]]], but the HList shapeless.::[Int,shapeless.::[String,shapeless.::[Boolean,shapeless.HNil]]] is too short.
foo(3)
複製程式碼
可以看到這個方法,能返回正確的型別而不是 Any
,並且能在編譯時做越界檢查。
該如何實現這樣的at
方法?
def at(n: Int): X
複製程式碼
首先我們想到的是用型別引數實現
def at[A](n: Int): A
複製程式碼
然而呼叫時,仍舊需要手工指定 A
的型別。
同時,不用型別引數的前提下,一個方法又只能返回一種型別
下面我們介紹一種使用帶抽象型別成員 typeclass 來解決返回不同型別的套路
實現基於 Nat
的 at
函式
為了簡化問題,先用 Nat
代替 Int
表示元素所在的位置
def at(n: Nat): X
複製程式碼
為了實現這個函式,我們先介紹一個套路:
如果一個型別
O
由其他幾個型別I1
,I2
,..In
決定
那麼我們可以構造一個
X[I1, I2, .., In] { type Out = O}
這樣的 typeclass 用來計算出 O 對應型別
套用到上面的方法:HList
本身型別和元素所在位置 n,可以決定返回型別,我們可以得到以下定義
trait At[L <: HList, N <: Nat] {
type Out
def apply(l: L): Out
}
implicit class HListSyntax[L <: HList](l: L) {
def at(n: Nat)(implicit at: Nat[L, n.N]): at.Out = at.apply(l)
}
複製程式碼
觀察上圖不難發現 T
的第 n 個元素型別就是 H :: T
的第 n + 1 個元素型別,即
// At[T, N] => At[H :: T, Succ[N]]
implicit def atN[H, T <: HList, N <: Nat](implicit att: At[L, N]): At[H :: L, Succ[N]] = new At[H :: L, Succ[N]]{
type Out = att.Out
def apply(h: H :: T) = att.apply(h.tail)
}
複製程式碼
而第 0 個元素型別則顯而易見的就是 head
的型別 H
implicit def atZero[H, T <: HList] = new At[H :: L, _0] {
type Out = H
def apply(l: H :: T) = l.head
}
複製程式碼
由以上兩條規則,則可以遞迴獲得任意位置 n 上的元素型別
用 Aux 解決編譯期型別丟失問題
然而,當我們嘗試使用上述定義的 at
時會發生編譯錯誤,告訴我們 Out
型別需要 ClassTag
這是因為編譯器沒法在編譯時獲得抽象型別成員 Out
的型別導致的
這裡需要再一次使用 Aux 套路解決問題
最終我們得到如下定義
trait At[L <: HList, N <: Nat] {
type Out
def apply(l: L): Out
}
object At {
type Aux[L <: HList, N <: Nat, O] = At[L, N] {type Out = O}
}
implicit class HListSyntax[L <: HList](l: L) {
def ::[H] (h: H): (H :: L) = new ::(h, l)
def at(n: Nat)(implicit at: At[L, n.N]): at.Out = {
at.apply(l)
}
}
implicit def atZero[H, T <: HList]: At.Aux[H :: T, _0, H] = new At[H :: T, _0] {
type Out = H
def apply(l : H :: T) = l.head
}
implicit def atN[H, T <: HList, N <: Nat](implicit at: At[T, N]): At.Aux[H :: T, Succ[N], at.Out] = new At[H :: T, Succ[N]] {
type Out = at.Out
def apply(l : H :: T) = at.apply(l.tail)
}
複製程式碼
完整可執行程式碼可以參考 scasite 連結
從 Int 到 Nat
Shapeless 除了支援根據 Nat
型別獲得對應元素外,還直接支援根據 Int
作為元素位置獲取元素。
但 Scala 的 Int
目前不支援 literal singleton type,並且不存在可以遞迴推導的後繼關係。
所以 shapeless 實際上是使用 macro 強行構造 Nat
例項來實現 Int -> Nat 的轉換。由於實現較為簡單,不再贅述。
總結
通過本文和前兩篇文章,我們意識到 implicit 和遞迴推理的套路是 shapeless 實現泛型程式設計的基本調性。 後續文章不再重複闡述 shapless 的實現機制,轉而著重介紹一些 shapeless 的實際應用