pexels-photo-226611.jpeg

shapelessでフィールドの順番の違うcase classを自動で変換する

 
0
このエントリーをはてなブックマークに追加
Kazuki Moriyama
Kazuki Moriyama (森山 和樹)

以前の記事でいろんな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インスタンス、任意の型に拡張メソッドを生やせば完成である。
前回との違いは

  1. FromをHListに変換
  2. 変換されたHListをToのHList型に並べ替える
  3. 並べ替えたものを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)

おわり

今回は前回よりちょっと融通が効くようにしたが、まだまだ制約が多い。
例えば同じフィールド名を含んでいないといけなかったり、対応するフィールドの型が同じじゃないといけなかったり。
コイツラも次回以降次第に剥がしていく。


info-outline

お知らせ

K.DEVは株式会社KDOTにより運営されています。記事の内容や会社でのITに関わる一般的なご相談に専門の社員がお答えしております。ぜひお気軽にご連絡ください。