circuit-circuit-board-resistor-computer-159201.jpeg

shapelessを使って同じ様なcase classを自動で変換する

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

この記事の例で出した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を使ってFromToをある型に対してインスタンス化すれば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がマクロで生成するので、その型を型パラメータ化しているのだ。
更にPersonPersonRowのHList表現を同じReprというパラメータで指定してあげることで、PersonPersonRowのHListは同じだ、という制約を設けている。

HList表現が同じだからこそPerson -> HList -> PersonRowという変換が可能になっている。

これでできたのかというとそうでもなくて上のままだとPerson -> PersonRowの変換しかできない。
そんなもの使い物にならないので、あらゆる型同士のMapToインスタンスを作成していく。

genericMapTo

じゃあどうやって全ての型同士のMapToインスタンスを作成するかと言うと意外と簡単で、FromToをパラメタ化してあげればいい。

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を使用するときにFromToを明示的に指定してあげれば勝手にそれらのペアのMapToインスタンスが作成されてimplicitで引っ張ってこられる。

じゃああとはPersonmapToメソッドを生やすだけですね。

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

info-outline

お知らせ

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