動機
ある対象target: T
に対してバリデーションを行い、
- バリデーションをパスしなかった場合
- バリデーションをパスしなかった理由でハンドリングしたい
- バリデーションをパスした場合
- バリデーションをパスした場合の
target: T
の状態でハンドリングしたい
- バリデーションをパスした場合の
みたいな動機があって、
def validate(target: T): Either[UnPassed, Passed]
のようなバリデーションの為の関数を実装した際に、返り値全体を同じグループとして表現したかった。
実例
以下の様にsealed traitを継承し、階層化します。階層化はあまり珍しくないかと思います。
sealed trait Result
object Result {
sealed trait Passed extends Result
object Passed {
case object Status1 extends Passed
case object Status2 extends Passed
...
case object StatusN extends Passed
}
sealed trait UnPassed extends Result
object UnPassed {
case object Reason1 extends UnPassed
case object Reason2 extends UnPassed
...
case object ReasonN extends UnPassed
}
}
sealed trait
以外にも、Scalaの列挙型なら継承によって階層化することができます。
例えば、以下の様にfinal case class
を用いると、特定の理由の時には特定の値を返す様なことができます。
final case class Reason(value: V) extends UnPassed
意味のまとまりが生まれるのが階層化を行う一番のメリットだと思います。
パターンマッチを行う
階層化した列挙型は、階層的なsealed
の恩恵を得ることができます。
マッチ対象の型を一番上のReason
とすれば、match
式のパターンマッチではReason
を継承する全ての要素をマッチしなければなりません。その下のPassed
またはUnPassed
をマッチ対象にすると、それぞれを継承する全ての要素をマッチする必要があります。
例えば、マッチ対象をResult.Passed
にすれば、バリデーションを通過した全ての状態をハンドリングすることができます。
val handler: Result.Passed => V = (status: Result.Passed) =>
status match {
case Result.Passed.Status1 => ???
case Result.Passed.Status2 => ???
...
case Result.Passed.StatusN => ???
}
仮に、Result.Passed.Status1
のケースを登録しない様なPartialFunction
を作成しようとすると、
warning: match may not be exhaustive.
It would fail on the following input: Status1
の様に警告を出してくれます。
Eitherとの組み合わせ
やり方は色々あると思いますが、実際にEitherと組み合わせて使う場合には以下の様になります。
def validate(target: T): Either[UnPassed, Passed]
def statusHandler(status: Passed): V
def reasonHandler(reason: UnPassed): V
validate(target) match {
case Right(status) => statusHandler(status)
case Left(reason) => reasonHandler(reason)
}
もっと良い方法もあるかと思いますので、何かあればおしらせください。