よくあるケースとして、入力値に対してバリデーションを行いすべて評価してから結果を返したい場合がある。
たとえばこのようなPerson型があったときに、
class Person private constructor(val name: Name, val age: Age)
- ①
name
は空文字を許さない - ②
age
は0以上
の要件があったうえで nameが空文字でageが0
のような入力があったら、 1,2それぞれのエラーを返却したい。
こういうときにはArrow.ktのEither型を利用してバリデーションを実装した上で、zipOrAccumulateで蓄積していくのが良い。
class ValidationError(val field: String, val value: String, reason: String) // コンパニオンオブジェクト以外からnewすることを防ぐ data class Name private constructor(val value: String) { companion object { // ファクトリでバリデーションしOKならRight, ダメならLeft fun of(value: String): Either<ValidationError, Name> = if (value.isNotEmpty()) Name(value).right() else ValidationError("Name", value, "input is empty").left() } } data class Age private constructor(val value: Int) { companion object { fun of(value: Int): Either<ValidationError, Age> = if (value > 0) Age(value).right() else ValidationError("Age", value.toString(), "input is less than or equal 0").left() } } class Person private constructor(val name: Name, val age: Age) { companion object { fun of(name: String, age: Int): Either<Nel<ValidationError>, Person> = Either.zipOrAccumulate( Name.of(name), Age.of(age) ) { validatedName, validatedAge -> Person(validatedName, validatedAge) } } }
この例の場合だと結果は以下のようになる。
- nameとage両方Right → Personのインスタンスが生成できる
- nameはRight, ageはLeft → ageのバリデーションエラーとなる
- nameはLeft, ageはRIght → nameのバリデーションエラーとなる
- nameとage両方Left → age, name両方のバリデーションエラーとなる
欠点として、 zipOrAccumulateでシュッと積み上げられるのが、 EIther<E, A>
もしくは Either<NonEmptyList<E>, A>
だけなので、例えば要件として
- ③
name
に禁則文字を含まない
のような要件が追加され、 name
に対する要件①と区別したい場合には、Nameのファクトリメソッド戻りの型がこのようになるはずである。
data class Name private constructor(val value: String) { companion object { fun of(value: String): Either<NonEmptyList<ValidationError>, Name> = TODO() } }
こうするとNameとAgeでファクトリメソッドの型がそれぞれ Either<NonEmptyList<E>, A>
と Either<E, A>
になり zipOrAccumurate
で積み上げていくことができなくなるので、Ageのファクトリの型も同じように Either<NonEmptyList<E>, A>
としなければならないのが難点である。
とはいえ、任意の値をNonEmptyListに含めるには .nel()
すればよいだけなのでそこまで苦労しないはずである。
data class Age private constructor(val value: Int) { companion object { fun of(value: Int): Either<NonEmptyList<ValidationError>, Age> = if (value > 0) Age(value).right() else ValidationError("Age", value.toString(), "input is less than or equal 0").nel().left() }