Ior

API Documentation: Ior

Ior represents an inclusive-or relationship between two data types. This makes it very similar to the Either data type, which represents an "exclusive-or" relationship. What this means, is that an Ior[A, B] (also written as A Ior B) can contain either an A, a B, or both an A and B. Another similarity to Either is that Ior is right-biased, which means that the map and flatMap functions will work on the right side of the Ior, in our case the B value. You can see this in the function signature of map:

def map[B, C](fa: A Ior B)(f: B => C): A Ior C

We can create Ior values using Ior.left, Ior.right and Ior.both:

import cats.data._

val right = Ior.right[String, Int](3)
// right: Ior[String, Int] = Right(b = 3)

val left = Ior.left[String, Int]("Error")
// left: Ior[String, Int] = Left(a = "Error")

val both = Ior.both("Warning", 3)
// both: Ior[String, Int] = Both(a = "Warning", b = 3)

Cats also offers syntax enrichment for Ior. The leftIor and rightIor functions can be imported from cats.syntax.ior._:

import cats.syntax.all._

val right = 3.rightIor
// right: Ior[Nothing, Int] = Right(b = 3)

val left = "Error".leftIor
// left: Ior[String, Nothing] = Left(a = "Error")

When we look at the Monad or Applicative instances of Ior, we can see that they actually require a Semigroup instance on the left side. This is because Ior will actually accumulate failures on the left side, very similar to how the Validated data type does. This means we can accumulate data on the left side while also being able to short-circuit upon the first left-side-only value. For example, sometimes, we might want to accumulate warnings together with a valid result and only halt the computation on a "hard error" Here's an example of how we might be able to do that:

import cats.syntax.all._
import cats.data.{ NonEmptyChain => Nec, Ior}


type Failures = Nec[String]

case class Username(value: String) extends AnyVal
case class Password(value: String) extends AnyVal

case class User(name: Username, pw: Password)

def validateUsername(u: String): Failures Ior Username = {
  if (u.isEmpty)
    Nec.one("Can't be empty").leftIor
  else if (u.contains("."))
    Ior.both(Nec.one("Dot in name is deprecated"), Username(u))
  else
    Username(u).rightIor
}

def validatePassword(p: String): Failures Ior Password = {
  if (p.length < 8)
    Nec.one("Password too short").leftIor
  else if (p.length < 10)
    Ior.both(Nec.one("Password should be longer"), Password(p))
  else
    Password(p).rightIor
}

def validateUser(name: String, password: String): Failures Ior User =
  (validateUsername(name), validatePassword(password)).mapN(User)

Now we're able to validate user data and also accumulate non-fatal warnings:

validateUser("John", "password12")
// res1: Ior[Failures, User] = Right(
//   b = User(name = Username(value = "John"), pw = Password(value = "password12"))
// )

validateUser("john.doe", "password")
// res2: Ior[Failures, User] = Both(
//   a = Append(
//     leftNE = Singleton(a = "Dot in name is deprecated"),
//     rightNE = Singleton(a = "Password should be longer")
//   ),
//   b = User(
//     name = Username(value = "john.doe"),
//     pw = Password(value = "password")
//   )
// )

validateUser("jane", "short")
// res3: Ior[Failures, User] = Left(a = Singleton(a = "Password too short"))

To extract the values, we can use the fold method, which expects a function for each case the Ior can represent:

validateUser("john.doe", "password").fold(
  errorNec => s"Error: ${errorNec.head}",
  user => s"Success: $user",
  (warnings, user) => s"Warning: ${user.name.value}; The following warnings occurred: ${warnings.show}"
)
// res4: String = "Warning: john.doe; The following warnings occurred: NonEmptyChain(Dot in name is deprecated, Password should be longer)"

Using with NonEmptyChain

Similar to Validated, there is also a type alias for using a NonEmptyChain on the left side.

import cats.data.NonEmptyChain

type IorNec[B, A] = Ior[NonEmptyChain[B], A]
import cats.syntax.all._, cats.data.NonEmptyChain

val left: IorNec[String, Int] = Ior.fromEither("Error".leftNec[Int])
// left: IorNec[String, Int] = Left(a = Singleton(a = "Error"))

Conversions

We can also convert our Ior to Either, Validated or Option. All of these conversions will discard the left side value if both are available:

Ior.both("Warning", 42).toEither
// res5: Either[String, Int] = Right(value = 42)