shapelessを使って同じ様なcase classを自動で変換する

この記事の例で出したcase classを自動で変換するmapToメソッドを実装してみる。

実装コード

import shapeless.Generic

case class Person(name: String, age: Long)
case class PersonRow(name: String, age: Long)

trait MapTo[From, To] {
  def apply(a: From): To
}

implicit def genericMapTo[From, To, Repr](
      implicit
      fromGen: Generic.Aux[From, Repr],
      toGen: Generic.Aux[To, Repr]  
  ): MapTo[From, To] = new MapTo[From, To] {
    override def apply(a: From) = toGen.from(fromGen.to(a))
  }

implicit class MapToOps[From](a: From) {
  def mapTo[To](implicit pm: MapTo[From, To]) = pm(a)
}

Person("Alice", 20).mapTo[PersonRow]
// => res0: PersonRow = PersonRow(Alice,20)

以下解説。

対象となるcase class

mapToメソッドを使用する条件はcase classのHList表現が同じであることである。
つまりそういうこと。

// どちらもHList表現は String :: Long :: HNil
case class Person(name: String, age: Long)
case class PersonRow(name: String, age: Long)

MapTo trait

mapTo処理を実行できる型を表す型クラスをMapTo traitで表現する。

// From型からTo型に変換できることを表す
trait MapTo[From, To] {
  def apply(a: From): To
}

このtraitを使ってFromとToをある型に対してインスタンス化すればFromからToに変換する処理をその型クラスインスタンスが保証してくれる。
例えば以下のインスタンスはPersonからPersonRowへの変換を担保する。

implicit def personToPersonRow[Repr](
      implicit personGen: Generic.Aux[Person, Repr],
      personRowGen: Generic.Aux[PersonRow, Repr]     // PersonとPersonRowは同じHList表現=Repr
  ): MapTo[Person, PersonRow] =
    new MapTo[Person, PersonRow] {
      override def apply(a: Person) = personRowGen.from(personGen.to(a))
    }

def mapTo(a: Person)(implicit pm: MapTo[Person, PersonRow]) = pm(a)

mapTo(Person("Alice", 20))
// => res0: PersonRow = PersonRow(Alice,20)

Generic.Aux[Person, Repr]とかそもそもReprは何やねんという感じだが、これはcase classとそのHList表現を表す。
つまりGeneric.Aux[Person, Repr]とはPersonのHList表現はReprですよということを言いたがっている。
実際のPersonのHList表現はshapelessがマクロで生成するので、その型を型パラメタ化しているのだ。
更にPersonとPersonRowのHList表現を同じReprというパラメタで指定してあげることで、PersonとPersonRowのHListは同じだ、という制約を設けている。

HList表現が同じだからこそPerson -> HList -> PersonRowとう変換が可能になっている。

これでできたのかというとそうでもなくて上のままだとPerson -> PersonRowの変換しかできない。
そんなもの使い物にならないので、あらゆる型同士のMapToインスタンスを作成していく。

genericMapTo

じゃあどうやって全ての型同士のMapToインスタンスを作成するかと言うと意外と簡単で、FromToをパラメタ化してあげればいい。

implicit def genericMapTo[From, To, Repr](
      implicit
      fromGen: Generic.Aux[From, Repr],
      toGen: Generic.Aux[To, Repr]
  ): MapTo[From, To] = new MapTo[From, To] {
    override def apply(a: From) = toGen.from(fromGen.to(a))
  }

def mapTo[From, To](a: From)(implicit pm: MapTo[From, To]) = pm(a)

mapTo[Person, PersonRow](Person("Alice", 20))
// => res0: PersonRow = PersonRow(Alice,20)

これでmapToを使用するときにFromToを明示的に指定してあげれば勝手にそれらのペアのMapToインスタンスが作成されてimplicitで引っ張ってこられる。

じゃああとはPersonにmapToメソッドを生やすだけですね。

MapToOps

もちろんimplicitクラスを用いて拡張メソッドとしてmapToを生やすのだが、Personだけに生やしても使い物にならないので全ての型に対してmapToを生やしていく。

implicit class MapToOps[From](a: From) {
  def mapTo[To](implicit pm: MapTo[From, To]) = pm(a)
}

Person("Alice", 20).mapTo[PersonRow]
// => res0: PersonRow = PersonRow(Alice,20)

これでFromでパラメタライズされた全ての型、すなわちあらゆる型にmapToメソッドが生えた。
あとはMapTo[From, To]インスタンスは前述の通り勝手に引っ張ってこられる。

もちろん逆の変換やいろんなcase class同士の変換が上の実装で可能になっている。

PersonRow("Alice", 20).mapTo[Person]
// => res0: Person = Person(Alice,20)

case class Dog(name: String, age: Long, color: String)
case class Cat(name: String, age: Long, color: String)

Dog("john", 1, "black").mapTo[Cat]
// => res1: Cat = Cat(john,1,black)

もちろん変換不可能なものに変換しようとするとコンパイルエラー。

Dog("john", 1, "black").mapTo[Person]
// => Error:(46, 30) could not find implicit value for parameter pm: MapTo[Dog,Person]
// Dog("john", 1, "black").mapTo[Person]

優秀。

おわり

今回はmapToを実装したが、これにもいろんなバリエーションがある。
例えばcase classのattributeの定義順が違う場合も対応できたり、片方に必要なattributeなどを変換したりもできる。

参考

https://books.underscore.io/shapeless-guide/shapeless-guide.pdf

Ordersky先生が説明してくれたdottyに入るかもしれないchecked exceptionのモチベ ZIOへの環境Rのprovide方法各種 ScalaのAuxパターン
View Comments
There are currently no comments.