IorT

API Documentation: IorT

IorT[F[_], A, B] is a light wrapper on an F[Ior[A, B]]. Similar to OptionT[F[_], A] and EitherT[F[_], A, B], it is a monad transformer for Ior, that can be more convenient to work with than using F[Ior[A, B]] directly.

The boilerplate

Consider the following program that uses Ior to propagate log messages when validating an address:

import cats.data.Ior
import cats.data.{ NonEmptyChain => Nec }
import cats.syntax.all._
import scala.util.{Success, Try}

type Logs = Nec[String]

def parseNumber(input: String): Ior[Logs, Option[Int]] =
  Try(input.trim.toInt) match {
    case Success(number) if number > 0 => Ior.Right(Some(number))
    case Success(_) => Ior.Both(Nec.one(s"'$input' is non-positive number"), None)
    case _ => Ior.Both(Nec.one(s"'$input' is not a number"), None)
  }

def parseStreet(input: String): Ior[Logs, String] = {
  if (input.trim.isEmpty)
    Ior.Left(Nec.one(s"'$input' is not a street"))
  else
    Ior.Right(input)
}

def numberToString(number: Option[Int]): Ior[Logs, String] =
  number match {
    case Some(n) => Ior.Right(n.toString)
    case None => Ior.Both(Nec.one("used default address number"), "n/a")
  }

def addressProgram(numberInput: String, streetInput: String): Ior[Logs, String] =
  for {
    number <- parseNumber(numberInput)
    street <- parseStreet(streetInput)
    sNumber <- numberToString(number)
  } yield s"$sNumber, $street"

Due to the monadic nature of Ior combining the results of parseNumber, parseStreet, and numberToString can be as concise as a for-comprehension. As the following examples demonstrate, log messages of the different processing steps are combined when using flatMap.

addressProgram("7", "Buckingham Palace Rd")
// res0: Ior[Logs, String] = Right(b = "7, Buckingham Palace Rd")
addressProgram("SW1W", "Buckingham Palace Rd")
// res1: Ior[Logs, String] = Both(
//   a = Append(
//     leftNE = Singleton(a = "'SW1W' is not a number"),
//     rightNE = Singleton(a = "used default address number")
//   ),
//   b = "n/a, Buckingham Palace Rd"
// )
addressProgram("SW1W", "")
// res2: Ior[Logs, String] = Left(
//   a = Append(
//     leftNE = Singleton(a = "'SW1W' is not a number"),
//     rightNE = Singleton(a = "'' is not a street")
//   )
// )

Suppose parseNumber, parseStreet, and numberToString are rewritten to be asynchronous and return Future[Ior[Logs, *]] instead. The for-comprehension can no longer be used since addressProgram must now compose Future and Ior together, which means that the error handling must be performed explicitly to ensure that the proper types are returned:

import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits.global

def parseNumberAsync(input: String): Future[Ior[Logs, Option[Int]]] =
  Future.successful(parseNumber(input))

def parseStreetAsync(input: String): Future[Ior[Logs, String]] =
  Future.successful(parseStreet(input))

def numberToStringAsync(number: Option[Int]): Future[Ior[Logs, String]] =
  Future.successful(numberToString(number))

def programHelper(number: Option[Int], streetInput: String): Future[Ior[Logs, String]] =
  parseStreetAsync(streetInput).flatMap { streetIor =>
    numberToStringAsync(number).map { sNumberIor =>
      for {
        street <- streetIor
        sNumber <- sNumberIor
      } yield s"$sNumber, $street"
    }
  }

def addressProgramAsync(numberInput: String, streetInput: String): Future[Ior[Logs, String]] =
  parseNumberAsync(numberInput).flatMap {
    case Ior.Left(logs) => Future.successful(Ior.Left(logs))
    case Ior.Right(number) => programHelper(number, streetInput)
    case b @ Ior.Both(_, number) => programHelper(number, streetInput).map(s => b.flatMap(_ => s))
  }

To keep some readability the program was split in two parts, otherwise code would be repeated. Note that when parseNumberAsync returns an Ior.Both it is necessary to combine it with the result of programHelper, otherwise some log messages would be lost.

import scala.concurrent.Await
import scala.concurrent.duration._

Await.result(addressProgramAsync("7", "Buckingham Palace Rd"), 1.second)
// res3: Ior[Logs, String] = Right(b = "7, Buckingham Palace Rd")
Await.result(addressProgramAsync("SW1W", "Buckingham Palace Rd"), 1.second)
// res4: Ior[Logs, String] = Both(
//   a = Append(
//     leftNE = Singleton(a = "'SW1W' is not a number"),
//     rightNE = Singleton(a = "used default address number")
//   ),
//   b = "n/a, Buckingham Palace Rd"
// )
Await.result(addressProgramAsync("SW1W", ""), 1.second)
// res5: Ior[Logs, String] = Left(
//   a = Append(
//     leftNE = Singleton(a = "'SW1W' is not a number"),
//     rightNE = Singleton(a = "'' is not a street")
//   )
// )

IorT to the rescue

The program of the previous section can be re-written using IorT as follows:

import cats.data.IorT

def addressProgramAsyncIorT(numberInput: String, streetInput: String): IorT[Future, Logs, String] =
  for {
    number <- IorT(parseNumberAsync(numberInput))
    street <- IorT(parseStreetAsync(streetInput))
    sNumber <- IorT(numberToStringAsync(number))
  } yield s"$sNumber, $street"

This version of addressProgramAsync is almost as concise as the non-asynchronous version. Note that when F is a monad, then IorT will also form a monad, allowing monadic combinators such as flatMap to be used in composing IorT values.

Await.result(addressProgramAsyncIorT("7", "Buckingham Palace Rd").value, 1.second)
// res6: Ior[Logs, String] = Right(b = "7, Buckingham Palace Rd")
Await.result(addressProgramAsyncIorT("SW1W", "Buckingham Palace Rd").value, 1.second)
// res7: Ior[Logs, String] = Both(
//   a = Append(
//     leftNE = Singleton(a = "'SW1W' is not a number"),
//     rightNE = Singleton(a = "used default address number")
//   ),
//   b = "n/a, Buckingham Palace Rd"
// )
Await.result(addressProgramAsyncIorT("SW1W", "").value, 1.second)
// res8: Ior[Logs, String] = Left(
//   a = Append(
//     leftNE = Singleton(a = "'SW1W' is not a number"),
//     rightNE = Singleton(a = "'' is not a street")
//   )
// )

Looking back at the implementation of parseStreet the return type could be Either[Logs, String] instead. Thinking of situations like this, where not all the types match, IorT provides factory methods of IorT[F, A, B] from:

From A and/or B to IorT[F, A, B]

To obtain a left version of IorT when given an A use IorT.leftT. When given a B a right version of IorT can be obtained with IorT.rightT (which is an alias for IorT.pure). Given both an A and a B use IorT.bothT.

Note the two styles for providing the IorT missing type parameters. The first three expressions only specify not inferable types, while the last expression specifies all types.

val number = IorT.rightT[Option, String](5)
// number: IorT[Option, String, Int] = IorT(value = Some(value = Right(b = 5)))
val error = IorT.leftT[Option, Int]("Not a number")
// error: IorT[Option, String, Int] = IorT(
//   value = Some(value = Left(a = "Not a number"))
// )
val weirdNumber = IorT.bothT[Option]("Not positive", -1)
// weirdNumber: IorT[Option, String, Int] = IorT(
//   value = Some(value = Both(a = "Not positive", b = -1))
// )

val numberPure: IorT[Option, String, Int] = IorT.pure(5)
// numberPure: IorT[Option, String, Int] = IorT(
//   value = Some(value = Right(b = 5))
// )

From F[A] and/or F[B] to IorT[F, A, B]

Similarly, use IorT.left, IorT.right, IorT.both to convert an F[A] and/or F[B] into an IorT. It is also possible to use IorT.liftF as an alias for IorT.right.

val numberF: Option[Int] = Some(5)
val errorF: Option[String] = Some("Not a number")

val warningF: Option[String] = Some("Not positive")
val weirdNumberF: Option[Int] = Some(-1)

val number: IorT[Option, String, Int] = IorT.right(numberF)
val error: IorT[Option, String, Int] = IorT.left(errorF)
val weirdNumber: IorT[Option, String, Int] = IorT.both(warningF, weirdNumberF)

From Ior[A, B] or F[Ior[A, B]] to IorT[F, A, B]

Use IorT.fromIor to a lift a value of Ior[A, B] into IorT[F, A, B]. An F[Ior[A, B]] can be converted into IorT using the IorT constructor.

val numberIor: Ior[String, Int] = Ior.Right(5)
val errorIor: Ior[String, Int] = Ior.Left("Not a number")
val weirdNumberIor: Ior[String, Int] = Ior.both("Not positive", -1)
val numberFIor: Option[Ior[String, Int]] = Option(Ior.Right(5))

val number: IorT[Option, String, Int] = IorT.fromIor(numberIor)
val error: IorT[Option, String, Int] = IorT.fromIor(errorIor)
val weirdNumber: IorT[Option, String, Int] = IorT.fromIor(weirdNumberIor)
val numberF: IorT[Option, String, Int] = IorT(numberFIor)

From Either[A, B] or F[Either[A, B]] to IorT[F, A, B]

Use IorT.fromEither or IorT.fromEitherF to create a value of IorT[F, A, B] from an Either[A, B] or a F[Either[A, B]], respectively.

val numberEither: Either[String, Int] = Right(5)
val errorEither: Either[String, Int] = Left("Not a number")
val numberFEither: Option[Either[String, Int]] = Option(Right(5))

val number: IorT[Option, String, Int] = IorT.fromEither(numberEither)
val error: IorT[Option, String, Int] = IorT.fromEither(errorEither)
val numberF: IorT[Option, String, Int] = IorT.fromEitherF(numberFEither)

From Option[B] or F[Option[B]] to IorT[F, A, B]

An Option[B] or an F[Option[B]], along with a default value, can be passed to IorT.fromOption and IorT.fromOptionF, respectively, to produce an IorT. For F[Option[B]] and default F[A], there is IorT.fromOptionM.

val numberOption: Option[Int] = None
val numberFOption: List[Option[Int]] = List(None, Some(2), None, Some(5))

val number = IorT.fromOption[List](numberOption, "Not defined")
val numberF = IorT.fromOptionF(numberFOption, "Not defined")
val numberM = IorT.fromOptionM(numberFOption, List("Not defined"))

Creating an IorT[F, A, B] from a Boolean test

IorT.cond allows concise creation of an IorT[F, A, B] based on a Boolean test, an A and a B. Similarly, IorT.condF uses F[A] and F[B].

val number: Int = 10
val informedNumber: IorT[Option, String, Int] = IorT.cond(number % 10 != 0, number, "Number is multiple of 10")
val uninformedNumber: IorT[Option, String, Int] = IorT.condF(number % 10 != 0, Some(number), None)

Extracting an F[Ior[A, B]] from an IorT[F, A, B]

Use the value method defined on IorT to retrieve the underlying F[Ior[A, B]]:

val errorT: IorT[Option, String, Int] = IorT.leftT("Not a number")

val error: Option[Ior[String, Int]] = errorT.value