Validated データ型 

Validated という、Either の代わりに使えるデータ型がもう1つ Cats に定義されている:

sealed abstract class Validated[+E, +A] extends Product with Serializable {

  def fold[B](fe: E => B, fa: A => B): B =
    this match {
      case Invalid(e) => fe(e)
      case Valid(a) => fa(a)
    }

  def isValid: Boolean = fold(_ => false, _ => true)
  def isInvalid: Boolean = fold(_ => true, _ => false)

  ....
}

object Validated extends ValidatedInstances with ValidatedFunctions{
  final case class Valid[+A](a: A) extends Validated[Nothing, A]
  final case class Invalid[+E](e: E) extends Validated[E, Nothing]
}

値はこのように作る:

scala> import cats._, cats.data.Validated, cats.instances.all._
import cats._
import cats.data.Validated
import cats.instances.all._

scala> import Validated.{ valid, invalid }
import Validated.{valid, invalid}

scala> valid[String, String]("event 1 ok")
res0: cats.data.Validated[String,String] = Valid(event 1 ok)

scala> invalid[String, String]("event 1 failed!")
res1: cats.data.Validated[String,String] = Invalid(event 1 failed!)

Validated の違いはこれはモナドではなく、applicative functor を形成することだ。 最初のイベントの結果を次へと連鎖するのでは無く、Validated は全イベントを検証する:

scala> import cats.syntax.cartesian._
import cats.syntax.cartesian._

scala> val result = (valid[String, String]("event 1 ok") |@|
        invalid[String, String]("event 2 failed!") |@|
        invalid[String, String]("event 3 failed!")) map {_ + _ + _}
result: cats.data.Validated[String,String] = Invalid(event 2 failed!event 3 failed!)

最終結果は Invalid(event 3 failed!event 2 failed!) となった。 計算途中でショートさせた Xor のモナドと違って、Validated は計算を続行して全ての失敗を報告する。 これはおそらくオンラインのベーコンショップでユーザのインプットを検証するのに役立つと思う。

だけど、問題はエラーメッセージが 1つの文字列にゴチャっと一塊になってしまっていることだ。リストでも使うべきじゃないか?

NonEmptyList を用いた失敗値の蓄積 

ここで使われるのが NonEmptyList データ型だ。 今のところは、必ず 1つ以上の要素が入っていることを保証するリストだと考えておけばいいと思う。

scala> import cats.data.{ NonEmptyList => NEL }
import cats.data.{NonEmptyList=>NEL}

scala> NEL.of(1)
res2: cats.data.NonEmptyList[Int] = NonEmptyList(1)

NEL[A] を invalid 側に使って失敗値の蓄積を行うことができる:

scala> val result =
         (valid[NEL[String], String]("event 1 ok") |@|
           invalid[NEL[String], String](NEL.of("event 2 failed!")) |@|
           invalid[NEL[String], String](NEL.of("event 3 failed!"))) map {_ + _ + _}
result: cats.data.Validated[cats.data.NonEmptyList[String],String] = Invalid(NonEmptyList(event 2 failed!, event 3 failed!))

Invalid の中に全ての失敗メッセージが入っている。

fold を使って値を取り出してみる:

scala> val errs: NEL[String] = result.fold(
         { l => l },
         { r => sys.error("invalid is expected") }
       )
errs: cats.data.NonEmptyList[String] = NonEmptyList(event 2 failed!, event 3 failed!)