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:
A,Bor bothF[A],F[B]or bothIor[A, B]orF[Ior[A, B]]Either[A, B]orF[Either[A, B]]Option[B]orF[Option[B]]- A
Booleantest
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