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