Ior データ型 

Cats には AB のペアを表すデータ型がもう1つあって、 Ior と呼ばれている。

/** Represents a right-biased disjunction that is either an `A`, or a `B`, or both an `A` and a `B`.
 *
 * An instance of `A [[Ior]] B` is one of:
 *  - `[[Ior.Left Left]][A]`
 *  - `[[Ior.Right Right]][B]`
 *  - `[[Ior.Both Both]][A, B]`
 *
 * `A [[Ior]] B` is similar to `A [[Xor]] B`, except that it can represent the simultaneous presence of
 * an `A` and a `B`. It is right-biased like [[Xor]], so methods such as `map` and `flatMap` operate on the
 * `B` value. Some methods, like `flatMap`, handle the presence of two [[Ior.Both Both]] values using a
 * `[[Semigroup]][A]`, while other methods, like [[toXor]], ignore the `A` value in a [[Ior.Both Both]].
 *
 * `A [[Ior]] B` is isomorphic to `(A [[Xor]] B) [[Xor]] (A, B)`, but provides methods biased toward `B`
 * values, regardless of whether the `B` values appear in a [[Ior.Right Right]] or a [[Ior.Both Both]].
 * The isomorphic [[Xor]] form can be accessed via the [[unwrap]] method.
 */
sealed abstract class Ior[+A, +B] extends Product with Serializable {

  final def fold[C](fa: A => C, fb: B => C, fab: (A, B) => C): C = this match {
    case Ior.Left(a) => fa(a)
    case Ior.Right(b) => fb(b)
    case Ior.Both(a, b) => fab(a, b)
  }

  final def isLeft: Boolean = fold(_ => true, _ => false, (_, _) => false)
  final def isRight: Boolean = fold(_ => false, _ => true, (_, _) => false)
  final def isBoth: Boolean = fold(_ => false, _ => false, (_, _) => true)

  ....
}

object Ior extends IorInstances with IorFunctions {
  final case class Left[+A](a: A) extends (A Ior Nothing)
  final case class Right[+B](b: B) extends (Nothing Ior B)
  final case class Both[+A, +B](a: A, b: B) extends (A Ior B)
}

これらの値は Iorleftrightboth メソッドを使って定義する:

scala> import cats._, cats.data.{ Ior, NonEmptyList => NEL }, cats.instances.all._
import cats._
import cats.data.{Ior, NonEmptyList=>NEL}
import cats.instances.all._

scala> Ior.right[NEL[String], Int](1)
res0: cats.data.Ior[cats.data.NonEmptyList[String],Int] = Right(1)

scala> Ior.left[NEL[String], Int](NEL.of("error"))
res1: cats.data.Ior[cats.data.NonEmptyList[String],Int] = Left(NonEmptyList(error))

scala> Ior.both[NEL[String], Int](NEL.of("warning"), 1)
res2: cats.data.Ior[cats.data.NonEmptyList[String],Int] = Both(NonEmptyList(warning),1)

scaladoc コメントに書いてある通り、IorflatMapIor.both(...) 値をみると Semigroup[A] を用いて失敗値を累積 (accumulate) する。 そのため、これは XorValidated のハイブリッドのような感覚で使えるかもしれない。

flatMap の振る舞いを 9つ全ての組み合わせでみてみよう:

scala> import cats.syntax.flatMap._
import cats.syntax.flatMap._

scala> Ior.right[NEL[String], Int](1) >>=
         { x => Ior.right[NEL[String], Int](x + 1) }
res3: cats.data.Ior[cats.data.NonEmptyList[String],Int] = Right(2)

scala> Ior.left[NEL[String], Int](NEL.of("error 1")) >>=
         { x => Ior.right[NEL[String], Int](x + 1) }
res4: cats.data.Ior[cats.data.NonEmptyList[String],Int] = Left(NonEmptyList(error 1))

scala> Ior.both[NEL[String], Int](NEL.of("warning 1"), 1) >>=
         { x => Ior.right[NEL[String], Int](x + 1) }
res5: cats.data.Ior[cats.data.NonEmptyList[String],Int] = Both(NonEmptyList(warning 1),2)

scala> Ior.right[NEL[String], Int](1) >>=
         { x => Ior.left[NEL[String], Int](NEL.of("error 2")) }
res6: cats.data.Ior[cats.data.NonEmptyList[String],Int] = Left(NonEmptyList(error 2))

scala> Ior.left[NEL[String], Int](NEL.of("error 1")) >>=
         { x => Ior.left[NEL[String], Int](NEL.of("error 2")) }
res7: cats.data.Ior[cats.data.NonEmptyList[String],Int] = Left(NonEmptyList(error 1))

scala> Ior.both[NEL[String], Int](NEL.of("warning 1"), 1) >>=
         { x => Ior.left[NEL[String], Int](NEL.of("error 2")) }
res8: cats.data.Ior[cats.data.NonEmptyList[String],Int] = Left(NonEmptyList(warning 1, error 2))

scala> Ior.right[NEL[String], Int](1) >>=
         { x => Ior.both[NEL[String], Int](NEL.of("warning 2"), x + 1) }
res9: cats.data.Ior[cats.data.NonEmptyList[String],Int] = Both(NonEmptyList(warning 2),2)

scala> Ior.left[NEL[String], Int](NEL.of("error 1")) >>=
         { x => Ior.both[NEL[String], Int](NEL.of("warning 2"), x + 1) }
res10: cats.data.Ior[cats.data.NonEmptyList[String],Int] = Left(NonEmptyList(error 1))

scala> Ior.both[NEL[String], Int](NEL.of("warning 1"), 1) >>=
         { x => Ior.both[NEL[String], Int](NEL.of("warning 2"), x + 1) }
res11: cats.data.Ior[cats.data.NonEmptyList[String],Int] = Both(NonEmptyList(warning 1, warning 2),2)

for 内包表記からも使える:

scala> import cats.syntax.semigroup._
import cats.syntax.semigroup._

scala> for {
         e1 <- Ior.right[NEL[String], Int](1)
         e2 <- Ior.both[NEL[String], Int](NEL.of("event 2 warning"), e1 + 1)
         e3 <- Ior.both[NEL[String], Int](NEL.of("event 3 warning"), e2 + 1)
       } yield (e1 |+| e2 |+| e3)
res12: cats.data.Ior[cats.data.NonEmptyList[String],Int] = Both(NonEmptyList(event 2 warning, event 3 warning),6)

Ior.leftXor[A, B]Either[A, B] の失敗値のようにショート回路になるが、 Ior.bothValidated[A, B] のように失敗値を累積させる。

今日はここまで! 続きはまた今度。