
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インスタンスを作成するかと言うと意外と簡単で、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))
}
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を使用するときにFromとToを明示的に指定してあげれば勝手にそれらのペアの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











