[cats] ValidatedでList[A]のバリデーションをする

cats.data.Validatedによってリストをバリデーションする場合は`Semigroup#combineAllOption`および`Monoid#combineAll`によってバリデーション結果を結合できます。
2021.05.27

はじめに

cats.data.Validatedを使ってList[A]の要素をバリデーションし結果をバリデーション済みのリストValidatedNec[E, List[A]] として得る方法を試してみました。

作戦

ValidatedNec[+E, +A]に対してAがMonoidまたはSemigroupであれば、ValidatedNecにもSemigroupおよびMonoidが得られます。そしてListはMonoidなのでMonoid#combineAllによって結合できます。

コード例

コードは以下のようになります。この例ではList[String]を各文字列の長さを基準にバリデーションしています。

package example

import cats.data._
import cats.implicits._
import cats.kernel.{Monoid, Semigroup}

package object validation {

  type ValidationError = String
  type ValidatedResult[A] = ValidatedNec[ValidationError, A]
  type ValidatedStrings = ValidatedResult[List[String]]

  def validateStrings(ss: String*): ValidatedResult[List[String]] =
    Monoid[ValidatedResult[List[String]]].combineAll(ss.map(validateLength))

  def validateLength(s: String): ValidatedResult[List[String]] =
    if (s.length >= 5) List(s).validNec else s"too short: $s".invalidNec

  //println(validateStrings(Range(1, 10).map("a".repeat): _*))
  //Invalid(Chain(too short: a, too short: aa, too short: aaa, too short: aaaa))
}

Semigroupの場合

ListはMonoidなのでcombineAllによって常に結果のリストが得られますが、Semigroupの場合はそうとは限りません。実際NonEmptyListで確かめてみます。

package example

import cats.data._
import cats.implicits._
import cats.kernel.{Monoid, Semigroup}

package object validation {

  type ValidationError = String
  type ValidatedResult[A] = ValidatedNec[ValidationError, A]
  type ValidatedStrings = ValidatedResult[List[String]]
  type ValidatedNonEmptyList = ValidatedResult[NonEmptyList[String]]

  implicitly[Semigroup[ValidatedStrings]]
  implicitly[Monoid[ValidatedStrings]]

  implicitly[Semigroup[ValidatedNonEmptyList]]
  // 以下はコンパイルエラー
  // implicitly[Monoid[ValidatedNonEmptyList]]
  //could not find implicit value for parameter e: cats.kernel.Monoid[example.validation.package.ValidatedNonEmptyList]
  //implicitly[Monoid[ValidatedNonEmptyList]]

}

まとめ

cats.data.Validatedによってリストをバリデーションする場合はSemigroup#combineAllOptionおよびMonoid#combineAllによってバリデーション結果を結合できます。