Shaplelessで特定の型のフィールドを含むcase classに対する型クラスインスタンスを導出する
はじめに
Shaplessはジェネリックプログラミングを実現するScalaのライブラリです。 今更ですがAdvanced Scala with Shapeless を読んでいて型クラスインスタンスの導出が面白かったので練習してみました。
使ったライブラリのバージョンなど
"org.typelevel" %% "cats-core" % "2.1.1" "com.chuusai" %% "shapeless" % "2.3.3" "org.typelevel" %% "simulacrum" % "1.0.0"
やったこと
まず以下のような型クラスがあります。
@typeclass trait MyValidation[A] { @op("validate") def validate(a: A): Either[ValidationError, A] } object MyValidation { type ValidationError = String def instance[A](f: A => Either[ValidationError, A]): MyValidation[A] = (a: A) => f(a) }
これに対して以下のようなADT ChocolateSnack
型のフィールドを持つcase class OyatuBag
のインスタンスを導出してみます。ChocolateSnack
に対するインスタンスは定義済みです。
(ここではOyatuBagに対するバリデーションの内容はChocolateSnackに関する条件のみとします)
sealed trait ChocolateSnack object ChocolateSnack { final case object Kinoko extends ChocolateSnack final case object Takenoko extends ChocolateSnack implicit val validationInstance: MyValidation[ChocolateSnack] = MyValidation.instance { case Kinoko => "Kinoko is not allowed.".asLeft case t@Takenoko => t.asRight } } //OyatuBagに対するMyValidation[OyatuBag]を導出したい final case class OyatuBag(choco: ChocolateSnack)
こうなる
出落ちですが、型Bを含むcase class A
に対するMyValidation[A]
を導出するコードは以下のようになります。
やっていることは以下の通りです。
- Aに対するGeneric gでHListを生成する
- HListから
#select[B]
でBを取り出す MyValidation[B]#validate
にBを渡してバリデーションする
object MyValidation { type ValidationError = String def instance[A](f: A => Either[ValidationError, A]): MyValidation[A] = (a: A) => f(a) //Bを含むcase class Aのインスタンスを導出する implicit def genericInstanceWithSpecifiedType[A, R <: HList, B]( implicit g: Generic.Aux[A, R], fb: MyValidation[B], select: Selector[R, B] ): MyValidation[A] = instance( a => fb.validate(g.to(a).select[B]).map(_ => a) ) }
使ってみる
実際に導出されるインスタンスを使ってみるのは以下のようになります。コード中にもありますがChocolateSnack
を含まないcase classに対する導出はコンパイルエラーとなります。
object Main extends App { import ChocolateSnack.validationInstance import MyValidation.ops._ println(OyatuBag("ann", ChocolateSnack.Kinoko).validate) //Left(Invalid Oyatu) println(OyatuBag("kazu", ChocolateSnack.Takenoko).validate) //Right(OyatuBag(kazu,Takenoko)) // ChocolateSnackを含まないcase classに対するMyValidationは導出できない final case class OyatuWithoutChoco(owner:String) // Error:(53, 15) Could not find an instance of MyValidation for example.OyatuWithoutChoco // MyValidation[OyatuWithoutChoco] }
まとめ
shaplessを使って任意の型のフィールドを含むcase classの型クラスインスタンスの導出ができました。同一の型のフィールドを持つ複数のcase classに対して挙動を追加したいときにcase classごとに型クラスインスタンスを定義したり、case classにtraitを追加するよりも変更が少なく見通しがいいと思います。