この記事の例で出した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