この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。
はじめに
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を追加するよりも変更が少なく見通しがいいと思います。