Shapeless 入門指南(三): Nat 和 implicit 在 shapeless 中的應用

ScalaCool發表於2018-02-06

本文由 Jilen 發表在 ScalaCool 團隊部落格。

前面文章中,我們提及了 peano 數型別:Nat,並且展示了隱式轉換這項 Scala 黑科技的應用。

本文我們通過 HListat 方法來進一步說明 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 來解決返回不同型別的套路

實現基於 Natat 函式

為了簡化問題,先用 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)
}
複製程式碼

at

觀察上圖不難發現 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 的實際應用

Shapeless 入門指南(三): Nat 和 implicit 在 shapeless 中的應用

相關文章