shapelessでフィールドの順番の違うcase classを自動で変換する
以前の記事でいろんなcase classを下のような感じで自動変換する型クラスを作成した。
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のフィールドの順番が同じじゃないと変換できないという制約があった。
case class Person(name: String, age: Long)
case class PersonRow(age: Long, name: String)
Person("Alice", 20).mapTo[PersonRow]
// => コンパイルエラー
今回はこれをもうちょっと曖昧にしてフィールドの名前さえ揃っていれば変換できるようにしてみる。
上のやつが正しく動くようにするのが目標。
実装コード
早速だが実装結果。
import shapeless._
import shapeless.ops.hlist.Align
case class Person(name: String, age: Long)
case class PersonRow(age: Long, name: String)
trait MapTo[From, To] {
def apply(a: From): To
}
implicit def genericMapTo[From, To, FromRepr <: HList, ToRepr <: HList](
implicit
fromGen: LabelledGeneric.Aux[From, FromRepr],
toGen: LabelledGeneric.Aux[To, ToRepr],
align: Align[FromRepr, ToRepr]
): MapTo[From, To] = new MapTo[From, To] {
override def apply(a: From): To = toGen.from(align(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(20,Alice)
以下説明。
基本的な説明は以前の記事で説明したので参照されたし。
LabelledGeneric
Genericでcase classを表現するとただの順番付きの型として表現される。
つまりTuple+Listのような表現になる。
しかし今回はフィールド名での一致を見たいのでこれだけでは不十分である。
フィールド名付きでジェネリックな表現に変換する型クラスとしてLabelledGenericというものがある。
LabelledGeneric[Person].to(Person("Alice", 20))
// => res1: String with shapeless.labelled.KeyTag[Symbol with shapeless.tag.Tagged[String("name")],String] :: Long with shapeless.labelled.KeyTag[Symbol with shapeless.tag.Tagged[String("age")],Long] :: shapeless.HNil = Alice :: 20 :: HNil
HListの型を見ればわかるがフィールドの型とその名前をタグ付けされた様な表現になっているのがわかる。
実態としてはただのHListなのでほとんど同じ様に扱える。
Align
Allign型クラスはHList同士の順番を合わせるのに使用できる。
HListがGenericから生み出されたただのHListだと型の順番しか見てくれないので同じ型があるとコンパイルエラーが起きる。
しかしLabelledGenericから生み出されたHListだとフィールドまで見て並び替えてくれるので安心である。
val pgen = Generic[Person]
val prgen = Generic[PersonRow]
val phlist = pgen.to(Person("Alice", 20))
// phlist: pgen.Repr = Alice :: 20 :: HNil
val prhlist = prgen.to(PersonRow(20, "Alice"))
// prhlist: prgen.Repr = 20 :: Alice :: HNil
Align[pgen.Repr, prgen.Repr].apply(phlist)
// res2: prgen.Repr = 20 :: Alice :: HNil
// phlistがprlistの型に並べ替えられている
// ただのGenericでもできるけど以下の様な場合だとコンパイルエラー
case class Dog(name: String, color: String)
case class Cat(color: String, name: String)
val doggen = Generic[Dog]
val catgen = Generic[Cat]
val dhlist = doggen.to(Dog("john", "black"))
val chlist = catgen.to(Cat("black", "john"))
Align[doggen.Repr, catgen.Repr].apply(dhlist)
// Stringが複数
あり並べ替え方がわからないのでコンパイルエラー
// LabellledGenericを使えばフィールドを見れる
val doggen = LabelledGeneric[Dog]
val catgen = LabelledGeneric[Cat]
val dhlist = doggen.to(Dog("john", "black"))
// dhlist: doggen.Repr = john :: black :: HNil
val chlist = catgen.to(Cat("black", "john"))
// chlist: catgen.Repr = black :: john :: HNil
Align[doggen.Repr, catgen.Repr].apply(dhlist)
// res3: catgen.Repr = black :: john :: HNil
実装
あとは以前やったように任意の型に対するMapToインスタンス、任意の型に拡張メソッドを生やせば完成である。
前回との違いは
- FromをHListに変換
- 変換されたHListをToのHList型に並べ替える
- 並べ替えたものをTo型に戻す
と三段階になっているところ。
細かい実装の詳細は前回の記事を参照。
trait MapTo[From, To] {
def apply(a: From): To
}
implicit def genericMapTo[From, To, FromRepr <: HList, ToRepr <: HList](
implicit
fromGen: LabelledGeneric.Aux[From, FromRepr],
toGen: LabelledGeneric.Aux[To, ToRepr],
align: Align[FromRepr, ToRepr]
): MapTo[From, To] = new MapTo[From, To] {
override def apply(a: From): To = toGen.from(align(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(20,Alice)
おわり
今回は前回よりちょっと融通が効くようにしたが、まだまだ制約が多い。
例えば同じフィールド名を含んでいないといけなかったり、対応するフィールドの型が同じじゃないといけなかったり。
コイツラも次回以降次第に剥がしていく。