この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。
はじめに
scalaでコードを書いていると似ているけど微妙に違いcase classの間でデータの詰め替えを行うケースがよくあります(e.g. インフラレイヤーのDBレコードからドメインオブジェクトへ)。shapeless を使うとそのようなデータの詰め替えを簡単に実装できます。
build.sbt
今回のbuild.sbtは以下の通りです。
scalaVersion := "2.13.2"
libraryDependencies ++= Seq(
"org.typelevel" %% "cats-core" % "2.1.1",
"com.chuusai" %% "shapeless" % "2.3.3"
)
概要
ざっくりと書き出すと以下のような方法でcaseクラス間の変換をします。
- 変換元クラスのLabelledGenericによって元のインスタンスからフィールド名ラベル付きのHListを生成する
- Align, Intersectionによってフィールドの並び替えと共通フィールドの絞り込みを行う
- 変換後クラスのLabelledGenericによってHListからインスタンスを生成する
コード
以上を下記のように変換前後の型を表すFrom、Toを型パラメータとしてとるtrait MapToとして定義しコンパニオンオブジェクトでその実装を参照できるようにします。apply
とinstance
は冗長に見えますがinstanceのimplicit パラメータのマクロによる解決のためにこのような実装が必要になります。
package example
import cats.Eq
import cats.implicits._
import shapeless._
import shapeless.ops.hlist
/**
* Fromのインスタンスから各フィールド値をコピーしてToのインスタンスを生成する。From、Toは以下の条件をみたす必要がある
* - Toのフィールドがすべて、同じ型、同じ名前でFromにフィールドとして存在する
* @tparam From コピー元の型
* @tparam To コピー先の型
*/
sealed trait MapTo[From, To] {
def apply(a: From): To
}
object MapTo {
def apply[From, To](implicit mapTo: MapTo[From, To]): MapTo[From, To] = mapTo
implicit def instance[From, To, FromRepr <: HList, ToRepr <: HList, Unaligned <: HList](
implicit
from: LabelledGeneric.Aux[From, FromRepr],
to: LabelledGeneric.Aux[To, ToRepr],
inter: hlist.Intersection.Aux[FromRepr, ToRepr, Unaligned],
align: hlist.Align[Unaligned, ToRepr]
): MapTo[From, To] = new MapTo[From, To] {
override def apply(a: From): To = to.from(inter(from.to(a)))
}
}
object MapExample extends App {
final case class PersonWithPhoneNumber(name: String, age: Int, address: Address, phoneNumber: String)
final case class Address(pref: String, city: String, street: String)
final case class Person(name: String, age: Int, address: Address)
// 順序が違う
final case class Person2(age: Int, address: Address, name:String)
// PersonWithPhoneNumberにないフィールド
final case class Person3(name:String, age: Int, address: Address, color:String)
implicit val personWithPhoneNumberEq: Eq[PersonWithPhoneNumber] = Eq.fromUniversalEquals
implicit val personEq: Eq[Person] = Eq.fromUniversalEquals
implicit val person2Eq: Eq[Person2] = Eq.fromUniversalEquals
implicit val addressEq: Eq[Address] = Eq.fromUniversalEquals
//生成元
val from = PersonWithPhoneNumber("ann", 2, Address("Hokkaido", "Sapporo", "Bird Street"), "090-1234-5678")
assert(
MapTo[PersonWithPhoneNumber, Person].apply(from)
===
Person("ann", 2, Address("Hokkaido", "Sapporo", "Bird Street"))
)
assert(
MapTo[PersonWithPhoneNumber, Person2].apply(from)
===
Person2(2, Address("Hokkaido", "Sapporo", "Bird Street"), "ann")
)
// コンパイルエラー
// Error:(66, 8) could not find implicit value for parameter mapTo: example.MapTo[example.MapExample.PersonWithPhoneNumber,example.MapExample.Person3]
// MapTo[PersonWithPhoneNumber, Person3]
}
HListに対する他の操作
今回紹介したcase class間の変換を含むHListの操作の例がAdvanced Scala with Shapless Courseで紹介されていますのでそちらも合わせてどうぞ。