LYAHFGG:
Either e a
型も失敗の文脈を与えるモナドです。しかも、失敗に値を付加できるので、何が失敗したかを説明したり、そのほか失敗にまつわる有用な情報を提供できます。
標準ライブラリの Either[A, B]
は知ってるし、Cats が Either
の右バイアスのファンクターを実装するという話も何回か出てきた。
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]
}
値はこのように作る:
import cats._, cats.data._, cats.syntax.all._
import Validated.{ valid, invalid }
valid[String, String]("event 1 ok")
// res0: Validated[String, String] = Valid(a = "event 1 ok")
invalid[String, String]("event 1 failed!")
// res1: Validated[String, String] = Invalid(e = "event 1 failed!")
Validated
の違いはこれはモナドではなく、applicative functor を形成することだ。
最初のイベントの結果を次へと連鎖するのでは無く、Validated
は全イベントを検証する:
val result = (valid[String, String]("event 1 ok"),
invalid[String, String]("event 2 failed!"),
invalid[String, String]("event 3 failed!")) mapN {_ + _ + _}
// result: Validated[String, String] = Invalid(
// e = "event 2 failed!event 3 failed!"
// )
最終結果は Invalid(event 2 failed!event 3 failed!)
となった。
計算途中でショートさせた Xor
のモナドと違って、Validated
は計算を続行して全ての失敗を報告する。
これはおそらくオンラインのベーコンショップでユーザのインプットを検証するのに役立つと思う。
だけど、問題はエラーメッセージが 1つの文字列にゴチャっと一塊になってしまっていることだ。リストでも使うべきじゃないか?
ここで使われるのが NonEmptyList
データ型だ。
今のところは、必ず 1つ以上の要素が入っていることを保証するリストだと考えておけばいいと思う。
import cats.data.{ NonEmptyList => NEL }
NEL.of(1)
// res2: NonEmptyList[Int] = NonEmptyList(head = 1, tail = List())
NEL[A]
を invalid 側に使って失敗値の蓄積を行うことができる:
val result2 =
(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!"))) mapN {_ + _ + _}
// result2: Validated[NonEmptyList[String], String] = Invalid(
// e = NonEmptyList(head = "event 2 failed!", tail = List("event 3 failed!"))
// )
Invalid
の中に全ての失敗メッセージが入っている。
fold
を使って値を取り出してみる:
val errs: NEL[String] = result2.fold(
{ l => l },
{ r => sys.error("invalid is expected") }
)
// errs: NonEmptyList[String] = NonEmptyList(
// head = "event 2 failed!",
// tail = List("event 3 failed!")
// )