From ab5433e757031eb9472822b5420cb358cc11077b Mon Sep 17 00:00:00 2001 From: Filipe Oliveira <1899304+frroliveira@users.noreply.github.com> Date: Wed, 22 Nov 2017 02:28:23 +0000 Subject: [PATCH] Add Ior Monad Transformer (#1977) * Create IorT class * Create IorT companion object * Create IorT instances * Use test naming standard for IorT * Add more functions to IorT * Add mapK to IorT * Add .condF to IorT * First attempt at IorT docs * Update CONTRIBUTING to install latest of jekyll with apt * Switch priority of MonadError instances for IorT * Add liftK to IorT --- CONTRIBUTING.md | 2 +- core/src/main/scala/cats/data/IorT.scala | 524 ++++++++++++++++++ .../main/resources/microsite/data/menu.yml | 4 + docs/src/main/tut/datatypes/iort.md | 256 +++++++++ .../cats/laws/discipline/Arbitrary.scala | 5 + .../src/test/scala/cats/tests/IorTSuite.scala | 355 ++++++++++++ 6 files changed, 1145 insertions(+), 1 deletion(-) create mode 100644 core/src/main/scala/cats/data/IorT.scala create mode 100644 docs/src/main/tut/datatypes/iort.md create mode 100644 tests/src/test/scala/cats/tests/IorTSuite.scala diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index cd1b2d7e21..fbe49ecfcd 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -177,7 +177,7 @@ run `sbt docs/makeMicrosite` yum install jekyll - apt-get install jekyll + apt-get install ruby-full; gem install jekyll gem install jekyll diff --git a/core/src/main/scala/cats/data/IorT.scala b/core/src/main/scala/cats/data/IorT.scala new file mode 100644 index 0000000000..01285647ba --- /dev/null +++ b/core/src/main/scala/cats/data/IorT.scala @@ -0,0 +1,524 @@ +package cats +package data + +import cats.syntax.either._ +import cats.syntax.option._ + +final case class IorT[F[_], A, B](value: F[Ior[A, B]]) { + + def fold[C](fa: A => C, fb: B => C, fab: (A, B) => C)(implicit F: Functor[F]): F[C] = F.map(value)(_.fold(fa, fb, fab)) + + def isLeft(implicit F: Functor[F]): F[Boolean] = F.map(value)(_.isLeft) + + def isRight(implicit F: Functor[F]): F[Boolean] = F.map(value)(_.isRight) + + def isBoth(implicit F: Functor[F]): F[Boolean] = F.map(value)(_.isBoth) + + def swap(implicit F: Functor[F]): IorT[F, B, A] = IorT(F.map(value)(_.swap)) + + def getOrElse[BB >: B](default: => BB)(implicit F: Functor[F]): F[BB] = F.map(value)(_.getOrElse(default)) + + def getOrElseF[BB >: B](default: => F[BB])(implicit F: Monad[F]): F[BB] = + F.flatMap(value) { + case Ior.Left(_) => default + case Ior.Right(b) => F.pure(b) + case Ior.Both(_, b) => F.pure(b) + } + + def valueOr[BB >: B](f: A => BB)(implicit F: Functor[F], BB: Semigroup[BB]): F[BB] = F.map(value)(_.valueOr(f)) + + def forall(f: B => Boolean)(implicit F: Functor[F]): F[Boolean] = F.map(value)(_.forall(f)) + + def exists(f: B => Boolean)(implicit F: Functor[F]): F[Boolean] = F.map(value)(_.exists(f)) + + def toOption(implicit F: Functor[F]): OptionT[F, B] = OptionT(F.map(value)(_.toOption)) + + def toEither(implicit F: Functor[F]): EitherT[F, A, B] = EitherT(F.map(value)(_.toEither)) + + def toNested: Nested[F, Ior[A, ?], B] = Nested[F, Ior[A, ?], B](value) + + def toNestedValidated(implicit F: Functor[F]): Nested[F, Validated[A, ?], B] = + Nested[F, Validated[A, ?], B](F.map(value)(_.toValidated)) + + def toValidated(implicit F: Functor[F]): F[Validated[A, B]] = F.map(value)(_.toValidated) + + def to[G[_]](implicit F: Functor[F], G: Alternative[G]): F[G[B]] = F.map(value)(_.to[G, B]) + + def collectRight(implicit FA: Alternative[F], FM: FlatMap[F]): F[B] = FM.flatMap(value)(_.to[F, B]) + + def merge[AA >: A](implicit ev: B <:< AA, F: Functor[F], AA: Semigroup[AA]): F[AA] = F.map(value)(_.merge(ev, AA)) + + def show(implicit show: Show[F[Ior[A, B]]]): String = show.show(value) + + def map[D](f: B => D)(implicit F: Functor[F]): IorT[F, A, D] = IorT(F.map(value)(_.map(f))) + + def mapK[G[_]](f: F ~> G): IorT[G, A, B] = IorT[G, A, B](f(value)) + + def bimap[C, D](fa: A => C, fb: B => D)(implicit F: Functor[F]): IorT[F, C, D] = IorT(F.map(value)(_.bimap(fa, fb))) + + def leftMap[C](f: A => C)(implicit F: Functor[F]): IorT[F, C, B] = IorT(F.map(value)(_.leftMap(f))) + + def leftFlatMap[BB >: B, C](f: A => IorT[F, C, BB])(implicit F: Monad[F], BB: Semigroup[BB]): IorT[F, C, BB] = + IorT(F.flatMap(value) { + case Ior.Left(a) => f(a).value + case r @ Ior.Right(_) => F.pure(r.asInstanceOf[Ior[C, BB]]) + case Ior.Both(a, b) => F.map(f(a).value) { + case Ior.Left(c) => Ior.Both(c, b) + case Ior.Right(b1) => Ior.Right(BB.combine(b, b1)) + case Ior.Both(c, b1) => Ior.Both(c, BB.combine(b, b1)) + } + }) + + def leftSemiflatMap[C](f: A => F[C])(implicit F: Monad[F]): IorT[F, C, B] = + IorT(F.flatMap(value) { + case Ior.Left(a) => F.map(f(a))(Ior.Left(_)) + case r @ Ior.Right(_) => F.pure(r.asInstanceOf[Ior[C, B]]) + case Ior.Both(a, b) => F.map(f(a))(Ior.Both(_, b)) + }) + + def transform[C, D](f: Ior[A, B] => Ior[C, D])(implicit F: Functor[F]): IorT[F, C, D] = IorT(F.map(value)(f)) + + def applyAlt[D](ff: IorT[F, A, B => D])(implicit F: Apply[F], A: Semigroup[A]): IorT[F, A, D] = + IorT[F, A, D](F.map2(value, ff.value)((iorb, iorbd) => Apply[Ior[A, ?]].ap(iorbd)(iorb))) + + def flatMap[AA >: A, D](f: B => IorT[F, AA, D])(implicit F: Monad[F], AA: Semigroup[AA]): IorT[F, AA, D] = + IorT(F.flatMap(value) { + case l @ Ior.Left(_) => F.pure(l.asInstanceOf[Ior[AA, D]]) + case Ior.Right(b) => f(b).value + case Ior.Both(a, b) => F.map(f(b).value) { + case Ior.Left(a1) => Ior.Left(AA.combine(a, a1)) + case Ior.Right(d) => Ior.Both(a, d) + case Ior.Both(a1, d) => Ior.Both(AA.combine(a, a1), d) + } + }) + + def flatMapF[AA >: A, D](f: B => F[Ior[AA, D]])(implicit F: Monad[F], AA: Semigroup[AA]): IorT[F, AA, D] = + flatMap(f andThen IorT.apply) + + def subflatMap[AA >: A, D](f: B => Ior[AA, D])(implicit F: Functor[F], AA: Semigroup[AA]): IorT[F, AA, D] = + IorT(F.map(value)(_.flatMap(f))) + + def semiflatMap[D](f: B => F[D])(implicit F: Monad[F]): IorT[F, A, D] = + IorT(F.flatMap(value) { + case l @ Ior.Left(_) => F.pure(l.asInstanceOf[Ior[A, D]]) + case Ior.Right(b) => F.map(f(b))(Ior.right) + case Ior.Both(a, b) => F.map(f(b))(Ior.both(a, _)) + }) + + def traverse[G[_], D](f: B => G[D])(implicit traverseF: Traverse[F], applicativeG: Applicative[G]): G[IorT[F, A, D]] = + applicativeG.map(traverseF.traverse(value)(ior => Traverse[Ior[A, ?]].traverse(ior)(f)))(IorT.apply) + + def foldLeft[C](c: C)(f: (C, B) => C)(implicit F: Foldable[F]): C = + F.foldLeft(value, c)((c, ior) => ior.foldLeft(c)(f)) + + def foldRight[C](lc: Eval[C])(f: (B, Eval[C]) => Eval[C])(implicit F: Foldable[F]): Eval[C] = + F.foldRight(value, lc)((ior, lc) => ior.foldRight(lc)(f)) + + def ===(that: IorT[F, A, B])(implicit eq: Eq[F[Ior[A, B]]]): Boolean = + eq.eqv(value, that.value) + + def combine(that: IorT[F, A, B])(implicit F: Apply[F], A: Semigroup[A], B: Semigroup[B]): IorT[F, A, B] = + IorT(F.map2(this.value, that.value)(_ combine _)) +} + +object IorT extends IorTInstances { + + /** + * Uses the [[http://typelevel.org/cats/guidelines.html#partially-applied-type-params Partially Applied Type Params technique]] for ergonomics. + */ + private[data] final class LeftPartiallyApplied[B](val dummy: Boolean = true) extends AnyVal { + def apply[F[_], A](fa: F[A])(implicit F: Functor[F]): IorT[F, A, B] = IorT(F.map(fa)(Ior.left)) + } + + /** + * Creates a left version of `IorT[F, A, B]` from a `F[A]` + * {{{ + * scala> import cats.data.IorT + * scala> import cats.implicits._ + * scala> IorT.left[Int](Option("err")) + * res0: cats.data.IorT[Option,String,Int] = IorT(Some(Left(err))) + * }}} + */ + final def left[B]: LeftPartiallyApplied[B] = new LeftPartiallyApplied[B] + + /** + * Uses the [[http://typelevel.org/cats/guidelines.html#partially-applied-type-params Partially Applied Type Params technique]] for ergonomics. + */ + private[data] final class LeftTPartiallyApplied[F[_], B](val dummy: Boolean = true) extends AnyVal { + def apply[A](a: A)(implicit F: Applicative[F]): IorT[F, A, B] = IorT(F.pure(Ior.left(a))) + } + + /** + * Creates a left version of `IorT[F, A, B]` from a `A` + * {{{ + * scala> import cats.data.IorT + * scala> import cats.implicits._ + * scala> IorT.leftT[Option, Int]("err") + * res0: cats.data.IorT[Option,String,Int] = IorT(Some(Left(err))) + * }}} + */ + final def leftT[F[_], B]: LeftTPartiallyApplied[F, B] = new LeftTPartiallyApplied[F, B] + + /** + * Uses the [[http://typelevel.org/cats/guidelines.html#partially-applied-type-params Partially Applied Type Params technique]] for ergonomics. + */ + private[data] final class RightPartiallyApplied[A](val dummy: Boolean = true) extends AnyVal { + def apply[F[_], B](fb: F[B])(implicit F: Functor[F]): IorT[F, A, B] = IorT(F.map(fb)(Ior.right)) + } + + /** + * Creates a right version of `IorT[F, A, B]` from a `F[B]` + * {{{ + * scala> import cats.data.IorT + * scala> import cats.implicits._ + * scala> IorT.right[String](Option(3)) + * res0: cats.data.IorT[Option,String,Int] = IorT(Some(Right(3))) + * }}} + */ + final def right[A]: RightPartiallyApplied[A] = new RightPartiallyApplied[A] + + /** + * Alias for [[pure]] + * {{{ + * scala> import cats.data.IorT + * scala> import cats.implicits._ + * scala> IorT.rightT[Option, String](3) + * res0: cats.data.IorT[Option,String,Int] = IorT(Some(Right(3))) + * }}} + */ + final def rightT[F[_], A]: PurePartiallyApplied[F, A] = pure + + /** + * Creates a both version of `IorT[F, A, B]` from a `F[A]` and a `F[B]` + * {{{ + * scala> import cats.data.IorT + * scala> import cats.implicits._ + * scala> IorT.both(Option("err"), Option(3)) + * res0: cats.data.IorT[Option,String,Int] = IorT(Some(Both(err,3))) + * }}} + */ + final def both[F[_], A, B](fa: F[A], fb: F[B])(implicit F: Apply[F]): IorT[F, A, B] = + IorT(F.map2(fa, fb)((a, b) => Ior.Both(a, b))) + + /** + * Uses the [[http://typelevel.org/cats/guidelines.html#partially-applied-type-params Partially Applied Type Params technique]] for ergonomics. + */ + private[data] final class BothTPartiallyApplied[F[_]](val dummy: Boolean = true) extends AnyVal { + def apply[A, B](a: A, b: B)(implicit F: Applicative[F]): IorT[F, A, B] = IorT(F.pure(Ior.Both(a, b))) + } + + /** + * Creates a both version of `IorT[F, A, B]` from a `A` and a `B` + * {{{ + * scala> import cats.data.IorT + * scala> import cats.implicits._ + * scala> IorT.bothT[Option]("err", 3) + * res0: cats.data.IorT[Option,String,Int] = IorT(Some(Both(err,3))) + * }}} + */ + final def bothT[F[_]]: BothTPartiallyApplied[F] = new BothTPartiallyApplied[F] + + /** + * Uses the [[http://typelevel.org/cats/guidelines.html#partially-applied-type-params Partially Applied Type Params technique]] for ergonomics. + */ + private[data] final class PurePartiallyApplied[F[_], A](val dummy: Boolean = true) extends AnyVal { + def apply[B](b: B)(implicit F: Applicative[F]): IorT[F, A, B] = IorT(F.pure(Ior.right(b))) + } + + /** + * Creates a right version of `IorT[F, A, B]` from a `B` + * {{{ + * scala> import cats.data.IorT + * scala> import cats.implicits._ + * scala> IorT.pure[Option, String](3) + * res0: cats.data.IorT[Option,String,Int] = IorT(Some(Right(3))) + * }}} + */ + final def pure[F[_], A]: PurePartiallyApplied[F, A] = new PurePartiallyApplied[F, A] + + /** + * Alias for [[right]] + * {{{ + * scala> import cats.data.IorT + * scala> import cats.implicits._ + * scala> val o: Option[Int] = Some(3) + * scala> val n: Option[Int] = None + * scala> IorT.liftF(o) + * res0: cats.data.IorT[Option,Nothing,Int] = IorT(Some(Right(3))) + * scala> IorT.liftF(n) + * res1: cats.data.IorT[Option,Nothing,Int] = IorT(None) + * }}} + */ + final def liftF[F[_], A, B](fb: F[B])(implicit F: Applicative[F]): IorT[F, A, B] = right(fb) + + /** + * Same as [[liftF]], but expressed as a FunctionK for use with [[IorT.mapK]] + * {{{ + * scala> import cats._, data._, implicits._ + * scala> val a: OptionT[Eval, Int] = 1.pure[OptionT[Eval, ?]] + * scala> val b: OptionT[IorT[Eval, String, ?], Int] = a.mapK(IorT.liftK) + * scala> b.value.value.value + * res0: cats.data.Ior[String,Option[Int]] = Right(Some(1)) + * }}} + */ + final def liftK[F[_], A](implicit F: Functor[F]): F ~> IorT[F, A, ?] = + new (F ~> IorT[F, A, ?]) { + def apply[B](fb: F[B]): IorT[F, A, B] = right(fb) + } + + /** + * Uses the [[http://typelevel.org/cats/guidelines.html#partially-applied-type-params Partially Applied Type Params technique]] for ergonomics. + */ + private[data] final class FromIorPartiallyApplied[F[_]](val dummy: Boolean = true) extends AnyVal { + def apply[A, B](ior: Ior[A, B])(implicit F: Applicative[F]): IorT[F, A, B] = IorT(F.pure(ior)) + } + + /** + * Transforms an `Ior` into an `IorT`, lifted into the specified `Applicative`. + * {{{ + * scala> import cats.data.{IorT, Ior} + * scala> import cats.implicits._ + * scala> val i: Ior[String, Int] = Ior.both("warning", 3) + * scala> IorT.fromIor[Option](i) + * res0: cats.data.IorT[Option,String,Int] = IorT(Some(Both(warning,3))) + * }}} + */ + final def fromIor[F[_]]: FromIorPartiallyApplied[F] = new FromIorPartiallyApplied[F] + + /** + * Uses the [[http://typelevel.org/cats/guidelines.html#partially-applied-type-params Partially Applied Type Params technique]] for ergonomics. + */ + private[data] final class FromEitherPartiallyApplied[F[_]](val dummy: Boolean = true) extends AnyVal { + def apply[E, A](either: Either[E, A])(implicit F: Applicative[F]): IorT[F, E, A] = IorT(F.pure(either.toIor)) + } + + /** + * Transforms an `Either` into an `IorT`, lifted into the specified `Applicative`. + * {{{ + * scala> import cats.data.IorT + * scala> import cats.implicits._ + * scala> val e: Either[String, Int] = Either.right(3) + * scala> IorT.fromEither[Option](e) + * res0: cats.data.IorT[Option,String,Int] = IorT(Some(Right(3))) + * }}} + */ + final def fromEither[F[_]]: FromEitherPartiallyApplied[F] = new FromEitherPartiallyApplied[F] + + /** + * Transforms an `F[Either]` into an `IorT`. + * {{{ + * scala> import cats.data.IorT + * scala> import cats.implicits._ + * scala> val e: Either[String, Int] = Either.right(3) + * scala> IorT.fromEitherF(Option(e)) + * res0: cats.data.IorT[Option,String,Int] = IorT(Some(Right(3))) + * }}} + */ + final def fromEitherF[F[_], E, A](feither: F[Either[E, A]])(implicit F: Functor[F]): IorT[F, E, A] = + IorT(F.map(feither)(_.toIor)) + + /** + * Uses the [[http://typelevel.org/cats/guidelines.html#partially-applied-type-params Partially Applied Type Params technique]] for ergonomics. + */ + private[data] final class FromOptionPartiallyApplied[F[_]](val dummy: Boolean = true) extends AnyVal { + def apply[E, A](option: Option[A], ifNone: => E)(implicit F: Applicative[F]): IorT[F, E, A] = + IorT(F.pure(option.toRightIor(ifNone))) + } + + /** + * Transforms an `Option` into an `IorT`, lifted into the specified `Applicative` and using + * the second argument if the `Option` is a `None`. + * {{{ + * scala> import cats.data.IorT + * scala> import cats.implicits._ + * scala> val o: Option[Int] = None + * scala> IorT.fromOption[List](o, "Answer not known.") + * res0: cats.data.IorT[List,String,Int] = IorT(List(Left(Answer not known.))) + * scala> IorT.fromOption[List](Some(42), "Answer not known.") + * res1: cats.data.IorT[List,String,Int] = IorT(List(Right(42))) + * }}} + */ + final def fromOption[F[_]]: FromOptionPartiallyApplied[F] = new FromOptionPartiallyApplied[F] + + /** + * Transforms an `F[Option]` into an `IorT`, using the second argument if the `Option` is a `None`. + * {{{ + * scala> import cats.data.IorT + * scala> import cats.implicits._ + * scala> val o: Option[Int] = None + * scala> IorT.fromOptionF(List(o), "Answer not known.") + * res0: cats.data.IorT[List,String,Int] = IorT(List(Left(Answer not known.))) + * scala> IorT.fromOptionF(List(Option(42)), "Answer not known.") + * res1: cats.data.IorT[List,String,Int] = IorT(List(Right(42))) + * }}} + */ + final def fromOptionF[F[_], E, A](foption: F[Option[A]], ifNone: => E)(implicit F: Functor[F]): IorT[F, E, A] = + IorT(F.map(foption)(_.toRightIor(ifNone))) + + /** + * Uses the [[http://typelevel.org/cats/guidelines.html#partially-applied-type-params Partially Applied Type Params technique]] for ergonomics. + */ + private[data] final class CondPartiallyApplied[F[_]](val dummy: Boolean = true) extends AnyVal { + def apply[A, B](test: Boolean, right: => B, left: => A)(implicit F: Applicative[F]): IorT[F, A, B] = + IorT(F.pure(if (test) Ior.right(right) else Ior.left(left))) + } + + /** + * If the condition is satisfied, return the given `B` in `Ior.Right`, otherwise, return the given + * `A` in `Ior.Left`, lifted into the specified `Applicative`. + * {{{ + * scala> import cats.data.IorT + * scala> import cats.implicits._ + * scala> val userInput = "hello world" + * scala> IorT.cond[Option]( + * | userInput.forall(_.isDigit) && userInput.size == 10, + * | userInput, + * | "The input does not look like a phone number") + * res0: cats.data.IorT[Option,String,String] = IorT(Some(Left(The input does not look like a phone number))) + * }}} + */ + final def cond[F[_]]: CondPartiallyApplied[F] = new CondPartiallyApplied[F] + + /** + * If the condition is satisfied, return the value of `IorT.right` on `F[B]`, otherwise, return the + * value of `IorT.left` on `F[A]`. + * {{{ + * scala> import cats.data.IorT + * scala> import cats.implicits._ + * scala> val userInput = "hello world" + * scala> IorT.condF[Option, String, String]( + * | userInput.forall(_.isDigit) && userInput.size == 10, + * | Some(userInput), + * | None) + * res0: cats.data.IorT[Option,String,String] = IorT(None) + * }}} + */ + final def condF[F[_], A, B](test: Boolean, right: => F[B], left: => F[A])(implicit F: Functor[F]): IorT[F, A, B] = + IorT(if (test) F.map(right)(Ior.right) else F.map(left)(Ior.left)) +} + +private[data] abstract class IorTInstances extends IorTInstances1 { + + implicit def catsDataShowForIorT[F[_], A, B](implicit sh: Show[F[Ior[A, B]]]): Show[IorT[F, A, B]] = + Contravariant[Show].contramap(sh)(_.value) + + implicit def catsDataBifunctorForIorT[F[_]](implicit F: Functor[F]): Bifunctor[IorT[F, ?, ?]] = + new Bifunctor[IorT[F, ?, ?]] { + override def bimap[A, B, C, D](iort: IorT[F, A, B])(fa: A => C, fb: B => D): IorT[F, C, D] = iort.bimap(fa, fb) + } + + implicit def catsDataTraverseForIorT[F[_], A](implicit F: Traverse[F]): Traverse[IorT[F, A, ?]] = + new IorTTraverse[F, A] { val F0: Traverse[F] = F } + + implicit def catsDataMonoidForIorT[F[_], A, B](implicit F: Monoid[F[Ior[A, B]]]): Monoid[IorT[F, A, B]] = + new IorTMonoid[F, A, B] { val F0: Monoid[F[Ior[A, B]]] = F } +} + +private[data] abstract class IorTInstances1 extends IorTInstances2 { + implicit def catsDataSemigroupForIorT[F[_], A, B](implicit F: Semigroup[F[Ior[A, B]]]): Semigroup[IorT[F, A, B]] = + new IorTSemigroup[F, A, B] { val F0: Semigroup[F[Ior[A, B]]] = F } + + implicit def catsDataFoldableForIorT[F[_], A](implicit F: Foldable[F]): Foldable[IorT[F, A, ?]] = + new IorTFoldable[F, A] { val F0: Foldable[F] = F } + + implicit def catsDataMonadErrorForIorT[F[_], A](implicit F: Monad[F], A: Semigroup[A]): MonadError[IorT[F, A, ?], A] = + new IorTMonadError[F, A] { + val A0: Semigroup[A] = A + val F0: Monad[F] = F + } +} + +private[data] abstract class IorTInstances2 extends IorTInstances3 { + implicit def catsDataMonadErrorFForIorT[F[_], A, E](implicit FE: MonadError[F, E], A: Semigroup[A]): MonadError[IorT[F, A, ?], E] = + new IorTMonadErrorF[F, A, E] { + val A0: Semigroup[A] = A + val F0: MonadError[F, E] = FE + } + + implicit def catsDataEqForIorT[F[_], A, B](implicit F: Eq[F[Ior[A, B]]]): Eq[IorT[F, A, B]] = + new IorTEq[F, A, B] { val F0: Eq[F[Ior[A, B]]] = F } +} + +private[data] abstract class IorTInstances3 { + implicit def catsDataFunctorForIorT[F[_], A](implicit F: Functor[F]): Functor[IorT[F, A, ?]] = + new IorTFunctor[F, A] { val F0: Functor[F] = F } +} + +private[data] sealed trait IorTFunctor[F[_], A] extends Functor[IorT[F, A, ?]] { + implicit def F0: Functor[F] + + override def map[B, D](iort: IorT[F, A, B])(f: B => D): IorT[F, A, D] = iort.map(f) +} + +private[data] sealed trait IorTEq[F[_], A, B] extends Eq[IorT[F, A, B]] { + implicit def F0: Eq[F[Ior[A, B]]] + + override def eqv(x: IorT[F, A, B], y: IorT[F, A, B]): Boolean = x === y +} + +private[data] sealed trait IorTMonad[F[_], A] extends Monad[IorT[F, A, ?]] with IorTFunctor[F, A] { + implicit def A0: Semigroup[A] + override implicit def F0: Monad[F] + + override def pure[B](b: B): IorT[F, A, B] = IorT.pure(b) + + override def flatMap[B, D](iort: IorT[F, A, B])(f: B => IorT[F, A, D]): IorT[F, A, D] = iort.flatMap(f) + + override def tailRecM[B, D](b: B)(f: B => IorT[F, A, Either[B, D]]): IorT[F, A, D] = + IorT(F0.tailRecM(Tuple2[B, Option[A]](b, None)) { case (b0, optionA) => + F0.map(f(b0).value) { + case Ior.Left(aa) => Right(Ior.Left(Semigroup.maybeCombine(optionA, aa))) + case Ior.Right(Left(b1)) => Left(b1 -> optionA) + case Ior.Right(Right(d)) => Right(optionA.fold(Ior.right[A, D](d))(Ior.both(_, d))) + case Ior.Both(aa, Right(d)) => Right(Ior.both(Semigroup.maybeCombine(optionA, aa), d)) + case Ior.Both(aa, Left(b1)) => Left(b1 -> Some(Semigroup.maybeCombine(optionA, aa))) + } + }) +} + +private[data] sealed trait IorTMonadError[F[_], A] extends MonadError[IorT[F, A, ?], A] with IorTMonad[F, A] { + override def raiseError[B](a: A): IorT[F, A, B] = IorT(F0.pure(Ior.left(a))) + + override def handleErrorWith[B](iort: IorT[F, A, B])(f: A => IorT[F, A, B]): IorT[F, A, B] = + IorT(F0.flatMap(iort.value) { + case Ior.Left(a) => f(a).value + case r @ Ior.Right(_) => F0.pure(r) + case Ior.Both(a, _) => f(a).value // should a be combined with result ?? + }) +} + +private[data] sealed trait IorTMonadErrorF[F[_], A, E] extends MonadError[IorT[F, A, ?], E] with IorTMonad[F, A] { + override implicit def F0: MonadError[F, E] + + override def raiseError[B](e: E): IorT[F, A, B] = IorT(F0.raiseError(e)) + + override def handleErrorWith[B](iort: IorT[F, A, B])(f: E => IorT[F, A, B]): IorT[F, A, B] = + IorT(F0.handleErrorWith(iort.value)(f(_).value)) +} + +private[data] sealed trait IorTSemigroup[F[_], A, B] extends Semigroup[IorT[F, A, B]] { + implicit def F0: Semigroup[F[Ior[A, B]]] + + override def combine(x: IorT[F, A, B], y: IorT[F, A, B]): IorT[F, A, B] = + IorT(F0.combine(x.value, y.value)) +} + +private[data] sealed trait IorTMonoid[F[_], A, B] extends Monoid[IorT[F, A, B]] with IorTSemigroup[F, A, B] { + override implicit def F0: Monoid[F[Ior[A, B]]] + + override def empty: IorT[F, A, B] = IorT(F0.empty) +} + +private[data] sealed trait IorTFoldable[F[_], A] extends Foldable[IorT[F, A, ?]] { + implicit def F0: Foldable[F] + + override def foldLeft[B, C](iort: IorT[F, A, B], c: C)(f: (C, B) => C): C = iort.foldLeft(c)(f) + + override def foldRight[B, C](iort: IorT[F, A, B], lc: Eval[C])(f: (B, Eval[C]) => Eval[C]): Eval[C] = iort.foldRight(lc)(f) +} + +private[data] sealed trait IorTTraverse[F[_], A] extends Traverse[IorT[F, A, ?]] with IorTFoldable[F, A] { + override implicit def F0: Traverse[F] + + override def traverse[G[_] : Applicative, B, D](iort: IorT[F, A, B])(f: B => G[D]): G[IorT[F, A, D]] = iort.traverse(f) +} diff --git a/docs/src/main/resources/microsite/data/menu.yml b/docs/src/main/resources/microsite/data/menu.yml index a8ad48aba7..2ef6321489 100644 --- a/docs/src/main/resources/microsite/data/menu.yml +++ b/docs/src/main/resources/microsite/data/menu.yml @@ -164,6 +164,10 @@ options: url: datatypes/eithert.html menu_type: data + - title: IorT + url: datatypes/iort.html + menu_type: data + - title: State url: datatypes/state.html menu_type: data diff --git a/docs/src/main/tut/datatypes/iort.md b/docs/src/main/tut/datatypes/iort.md new file mode 100644 index 0000000000..e617f6283d --- /dev/null +++ b/docs/src/main/tut/datatypes/iort.md @@ -0,0 +1,256 @@ +--- +layout: docs +title: "IorT" +section: "data" +source: "core/src/main/scala/cats/data/IorT.scala" +scaladoc: "#cats.data.IorT" +--- +# 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: + +```tut:silent +import cats.data.Ior +import cats.data.{ NonEmptyList => Nel } +import cats.implicits._ +import scala.util.{Success, Try} + +type Logs = Nel[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(Nel.one(s"'$input' is non-positive number"), None) + case _ => Ior.Both(Nel.one(s"'$input' is not a number"), None) + } + +def parseStreet(input: String): Ior[Logs, String] = { + if (input.trim.isEmpty) + Ior.Left(Nel.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(Nel.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`. + +```tut:book +addressProgram("7", "Buckingham Palace Rd") +addressProgram("SW1W", "Buckingham Palace Rd") +addressProgram("SW1W", "") +``` + +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: + +```tut:silent +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. + +```tut:book +import scala.concurrent.Await +import scala.concurrent.duration._ + +Await.result(addressProgramAsync("7", "Buckingham Palace Rd"), 1.second) +Await.result(addressProgramAsync("SW1W", "Buckingham Palace Rd"), 1.second) +Await.result(addressProgramAsync("SW1W", ""), 1.second) +``` + +## IorT to the rescue + +The program of the previous section can be re-written using `IorT` as follows: + +```tut:silent +import cats.data.IorT + +def addressProgramAsync(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. + +```tut:book +Await.result(addressProgramAsync("7", "Buckingham Palace Rd").value, 1.second) +Await.result(addressProgramAsync("SW1W", "Buckingham Palace Rd").value, 1.second) +Await.result(addressProgramAsync("SW1W", "").value, 1.second) +``` + +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`, `B` or both +* `F[A]`, `F[B]` or both +* `Ior[A, B]` or `F[Ior[A, B]]` +* `Either[A, B]` or `F[Either[A, B]]` +* `Option[B]` or `F[Option[B]]` +* A `Boolean` test + +## 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. + +```tut:book +val number = IorT.rightT[Option, String](5) +val error = IorT.leftT[Option, Int]("Not a number") +val weirdNumber = IorT.bothT[Option]("Not positive", -1) + +val number: IorT[Option, String, Int] = IorT.pure(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`. + +```tut:silent +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. + +```tut:silent +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. + +```tut:silent +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`. + +```tut:silent +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") +``` + +## 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]`. + +```tut:silent +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]]`: + +```tut:silent +val errorT: IorT[Option, String, Int] = IorT.leftT("Not a number") + +val error: Option[Ior[String, Int]] = errorT.value +``` \ No newline at end of file diff --git a/laws/src/main/scala/cats/laws/discipline/Arbitrary.scala b/laws/src/main/scala/cats/laws/discipline/Arbitrary.scala index eadc26eb9d..54f2238353 100644 --- a/laws/src/main/scala/cats/laws/discipline/Arbitrary.scala +++ b/laws/src/main/scala/cats/laws/discipline/Arbitrary.scala @@ -94,6 +94,11 @@ object arbitrary extends ArbitraryInstances0 { B.perturb(seed, _), (a, b) => A.perturb(B.perturb(seed, b), a))) + implicit def catsLawsArbitraryForIorT[F[_], A, B](implicit F: Arbitrary[F[Ior[A, B]]]): Arbitrary[IorT[F, A, B]] = + Arbitrary(F.arbitrary.map(IorT(_))) + + implicit def catsLawsCogenForIorT[F[_], A, B](implicit F: Cogen[F[Ior[A, B]]]): Cogen[IorT[F, A, B]] = + F.contramap(_.value) implicit def catsLawsArbitraryForOptionT[F[_], A](implicit F: Arbitrary[F[Option[A]]]): Arbitrary[OptionT[F, A]] = Arbitrary(F.arbitrary.map(OptionT.apply)) diff --git a/tests/src/test/scala/cats/tests/IorTSuite.scala b/tests/src/test/scala/cats/tests/IorTSuite.scala new file mode 100644 index 0000000000..b2f713291a --- /dev/null +++ b/tests/src/test/scala/cats/tests/IorTSuite.scala @@ -0,0 +1,355 @@ +package cats +package tests + +import cats.data.{Ior, IorT} +import cats.kernel.laws.discipline.{ + EqTests, + MonoidTests, + SemigroupTests +} +import cats.laws.discipline._ +import cats.laws.discipline.arbitrary._ + +class IorTSuite extends CatsSuite { + + { + implicit val F = ListWrapper.functor + + checkAll("IorT[ListWrapper, ?, ?]", BifunctorTests[IorT[ListWrapper, ?, ?]].bifunctor[Int, Int, Int, String, String, String]) + checkAll("Bifunctor[IorT[ListWrapper, ?, ?]]", SerializableTests.serializable(Bifunctor[IorT[ListWrapper, ?, ?]])) + + checkAll("IorT[ListWrapper, Int, ?]", FunctorTests[IorT[ListWrapper, Int, ?]].functor[Int, Int, Int]) + checkAll("Functor[IorT[ListWrapper, Int, ?]]", SerializableTests.serializable(Functor[IorT[ListWrapper, Int, ?]])) + } + + { + implicit val F = ListWrapper.traverse + + checkAll("IorT[ListWrapper, Int, ?]", TraverseTests[IorT[ListWrapper, Int, ?]].traverse[Int, Int, Int, Int, Option, Option]) + checkAll("Traverse[IorT[ListWrapper, Int, ?]]", SerializableTests.serializable(Traverse[IorT[ListWrapper, Int, ?]])) + } + + { + implicit val F = ListWrapper.monad + + checkAll("IorT[ListWrapper, String, Int]", MonadErrorTests[IorT[ListWrapper, String, ?], String].monadError[Int, Int, Int]) + checkAll("MonadError[IorT[List, ?, ?]]", SerializableTests.serializable(MonadError[IorT[ListWrapper, String, ?], String])) + } + + { + implicit val F: MonadError[Option, Unit] = catsStdInstancesForOption + + checkAll("IorT[Option, String, String]", MonadErrorTests[IorT[Option, String, ?], Unit].monadError[String, String, String]) + checkAll("MonadError[IorT[Option, ?, ?]]", SerializableTests.serializable(MonadError[IorT[Option, String, ?], Unit])) + } + + { + implicit val F = ListWrapper.foldable + + checkAll("IorT[ListWrapper, Int, ?]", FoldableTests[IorT[ListWrapper, Int, ?]].foldable[Int, Int]) + checkAll("Foldable[IorT[ListWrapper, Int, ?]]", SerializableTests.serializable(Foldable[IorT[ListWrapper, Int, ?]])) + } + + { + implicit val F = ListWrapper.semigroup[Ior[String, Int]] + + checkAll("IorT[ListWrapper, String, Int]", SemigroupTests[IorT[ListWrapper, String, Int]].semigroup) + checkAll("Semigroup[IorT[ListWrapper, String, Int]]", SerializableTests.serializable(Semigroup[IorT[ListWrapper, String, Int]])) + } + + { + implicit val F = ListWrapper.monoid[Ior[String, Int]] + + checkAll("IorT[ListWrapper, String, Int]", MonoidTests[IorT[ListWrapper, String, Int]].monoid) + checkAll("Monoid[IorT[ListWrapper, String, Int]]", SerializableTests.serializable(Monoid[IorT[ListWrapper, String, Int]])) + } + + { + implicit val F = ListWrapper.eqv[Ior[String, Int]] + + checkAll("IorT[ListWrapper, String, Int]", EqTests[IorT[ListWrapper, String, Int]].eqv) + checkAll("Eq[IorT[ListWrapper, String, Int]]", SerializableTests.serializable(Eq[IorT[ListWrapper, String, Int]])) + } + + test("fold with Id consistent with Ior fold") { + forAll { (iort: IorT[Id, String, Int], fa: String => Long, fb: Int => Long, fab: (String, Int) => Long) => + iort.fold(fa, fb, fab) should === (iort.value.fold(fa, fb, fab)) + } + } + + test("isLeft with Id consistent with Ior isLeft") { + forAll { (iort: IorT[Id, String, Int]) => + iort.isLeft should === (iort.value.isLeft) + } + } + + test("isRight with Id consistent with Ior isRight") { + forAll { (iort: IorT[Id, String, Int]) => + iort.isRight should === (iort.value.isRight) + } + } + + test("isBoth with Id consistent with Ior isBoth") { + forAll { (iort: IorT[Id, String, Int]) => + iort.isBoth should === (iort.value.isBoth) + } + } + + test("isBoth consistent with swap") { + forAll { (iort: IorT[List, String, Int]) => + iort.isBoth should === (iort.swap.isBoth) + } + } + + test("double swap is noop") { + forAll { (iort: IorT[List, String, Int]) => + iort.swap.swap.value should === (iort.value) + } + } + + test("getOrElse with Id consistent with Ior getOrElse") { + forAll { (iort: IorT[Id, String, Int], i: Int) => + iort.getOrElse(i) should === (iort.value.getOrElse(i)) + } + } + + test("getOrElseF with Id consistent with Ior getOrElse") { + forAll { (iort: IorT[Id, String, Int], i: Int) => + iort.getOrElseF(i) should === (iort.value.getOrElse(i)) + } + } + + test("valueOr with Id consistent with Ior valueOr") { + forAll { (iort: IorT[Id, String, Int], f: String => Int) => + iort.valueOr(f) should === (iort.value.valueOr(f)) + } + } + + test("forall with Id consistent with Ior forall") { + forAll { (iort: IorT[Id, String, Int], f: Int => Boolean) => + iort.forall(f) should === (iort.value.forall(f)) + } + } + + test("exists with Id consistent with Ior exists") { + forAll { (iort: IorT[Id, String, Int], f: Int => Boolean) => + iort.exists(f) should === (iort.value.exists(f)) + } + } + + test("toOption consistent with isLeft") { + forAll { (iort: IorT[List, String, Int]) => + iort.toOption.isDefined.map(! _) should === (iort.isLeft) + } + } + + test("toEither consistent with toOption") { + forAll { (iort: IorT[List, String, Int]) => + iort.toEither.toOption should === (iort.toOption) + } + } + + test("toEither consistent with isLeft") { + forAll { (iort: IorT[List, String, Int]) => + iort.toEither.isLeft should === (iort.isLeft) + } + } + + test("toNested has no loss") { + forAll { (iort: IorT[List, String, Int]) => + iort.toNested.value should === (iort.value) + } + } + + test("toNestedValidated consistent with Ior toValidated") { + forAll { (iort: IorT[List, String, Int]) => + iort.toNestedValidated.value should === (iort.value.map(_.toValidated)) + } + } + + test("toValidated consistent with Ior toValidated") { + forAll { (iort: IorT[List, String, Int]) => + iort.toValidated should === (iort.value.map(_.toValidated)) + } + } + + test("to consistent with toOption") { + forAll { (iort: IorT[List, String, Int]) => + iort.to[Option] should === (iort.toOption.value) + } + } + + test("collectRight with List consistent with flattening a to[List]") { + forAll { (iort: IorT[List, String, Int]) => + iort.collectRight should === (iort.to[List].flatten) + } + } + + test("merge with Id consistent with Ior merge") { + forAll { (iort: IorT[Id, Int, Int]) => + iort.merge should === (iort.value.merge) + } + } + + test("mapK consistent with f(value)+pure") { + val f: List ~> Option = λ[List ~> Option](_.headOption) + forAll { (iort: IorT[List, String, Int]) => + iort.mapK(f) should === (IorT(f(iort.value))) + } + } + + test("leftMap with Id consistent with Ior leftMap") { + forAll { (iort: IorT[Id, String, Int], f: String => Long) => + iort.leftMap(f).value should === (iort.value.leftMap(f)) + } + } + + test("leftFlatMap consistent with leftMap") { + forAll { (iort: IorT[List, String, Int], f: String => String) => + iort.leftFlatMap(v => IorT.left[Int](List(f(v)))) should ===(iort.leftMap(f)) + } + } + + test("leftFlatMap consistent with swap and then flatMap") { + forAll { (iort: IorT[List, String, Int], f: String => IorT[List, String, Int]) => + iort.leftFlatMap(f) should ===(iort.swap.flatMap(a => f(a).swap).swap) + } + } + + test("leftSemiflatMap consistent with leftMap") { + forAll { (iort: IorT[List, String, Int], f: String => String) => + iort.leftSemiflatMap(v => List(f(v))) should ===(iort.leftMap(f)) + } + } + + test("leftSemiflatmap consistent with swap and the semiflatMap") { + forAll { (iort: IorT[List, String, Int], f: String => List[String]) => + iort.leftSemiflatMap(f) should ===(iort.swap.semiflatMap(a => f(a)).swap) + } + } + + test("transform consistent with value.map") { + forAll { (iort: IorT[List, String, Int], f: Ior[String, Int] => Ior[Long, Double]) => + iort.transform(f) should === (IorT(iort.value.map(f))) + } + } + + test("applyAlt with Id consistent with map") { + forAll { (iort: IorT[Id, String, Int], f: Int => String) => + iort.applyAlt(IorT.pure(f)) should === (iort.map(f)) + } + } + + test("flatMapF consistent with flatMap") { + forAll { (iort: IorT[List, String, Int], f: Int => IorT[List, String, Int]) => + iort.flatMapF(f(_).value) should === (iort.flatMap(f)) + } + } + + test("subflatMap consistent with value.map+flatMap") { + forAll { (iort: IorT[List, String, Int], f: Int => Ior[String, Double]) => + iort.subflatMap(f) should === (IorT(iort.value.map(_.flatMap(f)))) + } + } + + test("semiflatMap consistent with value.flatMap+f+right/both") { + forAll { (iort: IorT[List, String, Int], f: Int => List[Long]) => + iort.semiflatMap(f) should === (IorT(iort.value.flatMap { + case l @ Ior.Left(_) => List(l.asInstanceOf[Ior[String, Long]]) + case Ior.Right(b) => f(b).map(Ior.right) + case Ior.Both(a, b) => f(b).map(Ior.both(a, _)) + })) + } + } + + test("IorT.left with Option isLeft") { + forAll { (option: Option[String]) => + IorT.left[Int](option).isLeft should === (option.map(_ => true)) + } + } + + test("IorT.leftT isLeft") { + forAll { (s: String) => + IorT.leftT[Option, Int](s).isLeft should === (Some(true)) + } + } + + test("IorT.right with Option isRight") { + forAll { (option: Option[Int]) => + IorT.right[String](option).isRight should === (option.map(_ => true)) + } + } + + test("IorT.rightT consistent with IorT.pure") { + forAll { (i: Int) => + IorT.rightT[Option, String](i).value should === (IorT.pure[Option, String](i).value) + } + } + + test("IorT.both isBoth with Option consistent with Option zip") { + forAll { (optionS: Option[String], optionI: Option[Int]) => + IorT.both(optionS, optionI).isBoth should === (optionS.zip(optionI).headOption.map(_ => true)) + } + } + + test("IorT.bothT isBoth") { + forAll { (s: String, i: Int) => + IorT.bothT[Option](s, i).isBoth should === (Some(true)) + } + } + + test("IorT.pure isRight") { + forAll { (i: Int) => + IorT.rightT[Option, String](i).isRight should === (Some(true)) + } + } + + test("IorT.liftF consistent with IorT.right") { + forAll { (option: Option[Int]) => + IorT.liftF[Option, String, Int](option).value should === (IorT.right[String](option).value) + } + } + + test("IorT.fromIor with Id is noop") { + forAll { (ior: Ior[String, Int]) => + IorT.fromIor[Id](ior).value should === (ior) + } + } + + test("IorT.fromEither toEither is noop") { + forAll { (either: Either[String, Int]) => + IorT.fromEither[Id](either).value.toEither should === (either) + } + } + + test("IorT.fromEitherF toEither is noop") { + forAll { (either: Either[String, Int]) => + IorT.fromEitherF[Id, String, Int](either).value.toEither should === (either) + } + } + + test("IorT.fromOption isLeft consistent with Option isEmpty") { + forAll { (option: Option[Int], s: String) => + IorT.fromOption[Id](option, s).isLeft should === (option.isEmpty) + } + } + + test("IorT.fromOptionF isLeft consistent with Option isEmpty") { + forAll { (option: Option[Int], s: String) => + IorT.fromOptionF[Id, String, Int](option, s).isLeft should === (option.isEmpty) + } + } + + test("IorT.cond isRight equals test") { + forAll { (test: Boolean, s: String, i: Int) => + val iort = IorT.cond[Id](test, s, i) + iort.isRight && !iort.isLeft && !iort.isBoth should === (test) + } + } + + test("IorT.condF consistent with IorT.right and IorT.left") { + forAll { (test: Boolean, optionS: Option[String], optionI: Option[Int]) => + IorT.condF(test, optionS, optionI) === (if (test) IorT.right(optionS) else IorT.left(optionI)) + } + } +} \ No newline at end of file