diff --git a/core/src/main/scala/cats/arrow/NaturalTransformation.scala b/core/src/main/scala/cats/arrow/NaturalTransformation.scala index 3dc50bfd36..336fc92a10 100644 --- a/core/src/main/scala/cats/arrow/NaturalTransformation.scala +++ b/core/src/main/scala/cats/arrow/NaturalTransformation.scala @@ -1,6 +1,8 @@ package cats package arrow +import cats.data.{Xor, Coproduct} + trait NaturalTransformation[F[_], G[_]] extends Serializable { self => def apply[A](fa: F[A]): G[A] @@ -18,4 +20,12 @@ object NaturalTransformation { new NaturalTransformation[F, F] { def apply[A](fa: F[A]): F[A] = fa } + + def or[F[_], G[_], H[_]](f: F ~> H, g: G ~> H): Coproduct[F, G, ?] ~> H = + new (Coproduct[F, G, ?] ~> H) { + def apply[A](fa: Coproduct[F, G, A]): H[A] = fa.run match { + case Xor.Left(ff) => f(ff) + case Xor.Right(gg) => g(gg) + } + } } diff --git a/core/src/main/scala/cats/data/Coproduct.scala b/core/src/main/scala/cats/data/Coproduct.scala new file mode 100644 index 0000000000..1c4b8c64a0 --- /dev/null +++ b/core/src/main/scala/cats/data/Coproduct.scala @@ -0,0 +1,217 @@ +package cats +package data + +import cats.functor.Contravariant + +/** `F` on the left and `G` on the right of [[Xor]]. + * + * @param run The underlying [[Xor]]. */ +final case class Coproduct[F[_], G[_], A](run: F[A] Xor G[A]) { + + import Coproduct._ + + def map[B](f: A => B)(implicit F: Functor[F], G: Functor[G]): Coproduct[F, G, B] = + Coproduct(run.bimap(F.lift(f), G.lift(f))) + + def coflatMap[B](f: Coproduct[F, G, A] => B)(implicit F: CoflatMap[F], G: CoflatMap[G]): Coproduct[F, G, B] = + Coproduct( + run.bimap(a => F.coflatMap(a)(x => f(leftc(x))), a => G.coflatMap(a)(x => f(rightc(x)))) + ) + + def duplicate(implicit F: CoflatMap[F], G: CoflatMap[G]): Coproduct[F, G, Coproduct[F, G, A]] = + Coproduct(run.bimap( + x => F.coflatMap(x)(a => leftc(a)) + , x => G.coflatMap(x)(a => rightc(a))) + ) + + def extract(implicit F: Comonad[F], G: Comonad[G]): A = + run.fold(F.extract, G.extract) + + def contramap[B](f: B => A)(implicit F: Contravariant[F], G: Contravariant[G]): Coproduct[F, G, B] = + Coproduct(run.bimap(F.contramap(_)(f), G.contramap(_)(f))) + + def foldRight[B](z: Eval[B])(f: (A, Eval[B]) => Eval[B])(implicit F: Foldable[F], G: Foldable[G]): Eval[B] = + run.fold(a => F.foldRight(a, z)(f), a => G.foldRight(a, z)(f)) + + def foldLeft[B](z: B)(f: (B, A) => B)(implicit F: Foldable[F], G: Foldable[G]): B = + run.fold(a => F.foldLeft(a, z)(f), a => G.foldLeft(a, z)(f)) + + def foldMap[B](f: A => B)(implicit F: Foldable[F], G: Foldable[G], M: Monoid[B]): B = + run.fold(F.foldMap(_)(f), G.foldMap(_)(f)) + + def traverse[X[_], B](g: A => X[B])(implicit F: Traverse[F], G: Traverse[G], A: Applicative[X]): X[Coproduct[F, G, B]] = + run.fold( + x => A.map(F.traverse(x)(g))(leftc(_)) + , x => A.map(G.traverse(x)(g))(rightc(_)) + ) + + def isLeft: Boolean = + run.isLeft + + def isRight: Boolean = + run.isRight + + def swap: Coproduct[G, F, A] = + Coproduct(run.swap) + + def toValidated: Validated[F[A], G[A]] = + run.toValidated + +} + +object Coproduct extends CoproductInstances { + + def leftc[F[_], G[_], A](x: F[A]): Coproduct[F, G, A] = + Coproduct(Xor.left(x)) + + def rightc[F[_], G[_], A](x: G[A]): Coproduct[F, G, A] = + Coproduct(Xor.right(x)) + + final class CoproductLeft[G[_]] private[Coproduct] { + def apply[F[_], A](fa: F[A]): Coproduct[F, G, A] = Coproduct(Xor.left(fa)) + } + + final class CoproductRight[F[_]] private[Coproduct] { + def apply[G[_], A](ga: G[A]): Coproduct[F, G, A] = Coproduct(Xor.right(ga)) + } + + def left[G[_]]: CoproductLeft[G] = new CoproductLeft[G] + + def right[F[_]]: CoproductRight[F] = new CoproductRight[F] + +} + +private[data] sealed abstract class CoproductInstances3 { + + implicit def coproductEq[F[_], G[_], A](implicit E: Eq[F[A] Xor G[A]]): Eq[Coproduct[F, G, A]] = + Eq.by(_.run) + + implicit def coproductFunctor[F[_], G[_]](implicit F0: Functor[F], G0: Functor[G]): Functor[Coproduct[F, G, ?]] = + new CoproductFunctor[F, G] { + implicit def F: Functor[F] = F0 + + implicit def G: Functor[G] = G0 + } + + implicit def coproductFoldable[F[_], G[_]](implicit F0: Foldable[F], G0: Foldable[G]): Foldable[Coproduct[F, G, ?]] = + new CoproductFoldable[F, G] { + implicit def F: Foldable[F] = F0 + + implicit def G: Foldable[G] = G0 + } +} + +private[data] sealed abstract class CoproductInstances2 extends CoproductInstances3 { + + implicit def coproductContravariant[F[_], G[_]](implicit F0: Contravariant[F], G0: Contravariant[G]): Contravariant[Coproduct[F, G, ?]] = + new CoproductContravariant[F, G] { + implicit def F: Contravariant[F] = F0 + + implicit def G: Contravariant[G] = G0 + } +} + +private[data] sealed abstract class CoproductInstances1 extends CoproductInstances2 { + implicit def coproductCoflatMap[F[_], G[_]](implicit F0: CoflatMap[F], G0: CoflatMap[G]): CoflatMap[Coproduct[F, G, ?]] = + new CoproductCoflatMap[F, G] { + implicit def F: CoflatMap[F] = F0 + + implicit def G: CoflatMap[G] = G0 + } +} + +private[data] sealed abstract class CoproductInstances0 extends CoproductInstances1 { + implicit def coproductTraverse[F[_], G[_]](implicit F0: Traverse[F], G0: Traverse[G]): Traverse[Coproduct[F, G, ?]] = + new CoproductTraverse[F, G] { + implicit def F: Traverse[F] = F0 + + implicit def G: Traverse[G] = G0 + } +} + +sealed abstract class CoproductInstances extends CoproductInstances0 { + + implicit def coproductComonad[F[_], G[_]](implicit F0: Comonad[F], G0: Comonad[G]): Comonad[Coproduct[F, G, ?]] = + new CoproductComonad[F, G] { + implicit def F: Comonad[F] = F0 + + implicit def G: Comonad[G] = G0 + } +} + +private[data] trait CoproductFunctor[F[_], G[_]] extends Functor[Coproduct[F, G, ?]] { + implicit def F: Functor[F] + + implicit def G: Functor[G] + + def map[A, B](a: Coproduct[F, G, A])(f: A => B): Coproduct[F, G, B] = + a map f +} + +private[data] trait CoproductContravariant[F[_], G[_]] extends Contravariant[Coproduct[F, G, ?]] { + implicit def F: Contravariant[F] + + implicit def G: Contravariant[G] + + def contramap[A, B](a: Coproduct[F, G, A])(f: B => A): Coproduct[F, G, B] = + a contramap f +} + +private[data] trait CoproductFoldable[F[_], G[_]] extends Foldable[Coproduct[F, G, ?]] { + implicit def F: Foldable[F] + + implicit def G: Foldable[G] + + def foldRight[A, B](fa: Coproduct[F, G, A], z: Eval[B])(f: (A, Eval[B]) => Eval[B]): Eval[B] = + fa.foldRight(z)(f) + + def foldLeft[A, B](fa: Coproduct[F, G, A], z: B)(f: (B, A) => B): B = + fa.foldLeft(z)(f) + + override def foldMap[A, B](fa: Coproduct[F, G, A])(f: A => B)(implicit M: Monoid[B]): B = + fa foldMap f +} + +private[data] trait CoproductTraverse[F[_], G[_]] extends CoproductFoldable[F, G] with Traverse[Coproduct[F, G, ?]] { + implicit def F: Traverse[F] + + implicit def G: Traverse[G] + + override def map[A, B](a: Coproduct[F, G, A])(f: A => B): Coproduct[F, G, B] = + a map f + + override def traverse[X[_] : Applicative, A, B](fa: Coproduct[F, G, A])(f: A => X[B]): X[Coproduct[F, G, B]] = + fa traverse f +} + +private[data] trait CoproductCoflatMap[F[_], G[_]] extends CoflatMap[Coproduct[F, G, ?]] { + implicit def F: CoflatMap[F] + + implicit def G: CoflatMap[G] + + def map[A, B](a: Coproduct[F, G, A])(f: A => B): Coproduct[F, G, B] = + a map f + + def coflatMap[A, B](a: Coproduct[F, G, A])(f: Coproduct[F, G, A] => B): Coproduct[F, G, B] = + a coflatMap f + +} + +private[data] trait CoproductComonad[F[_], G[_]] extends Comonad[Coproduct[F, G, ?]] { + implicit def F: Comonad[F] + + implicit def G: Comonad[G] + + def map[A, B](a: Coproduct[F, G, A])(f: A => B): Coproduct[F, G, B] = + a map f + + def extract[A](p: Coproduct[F, G, A]): A = + p.extract + + def coflatMap[A, B](a: Coproduct[F, G, A])(f: Coproduct[F, G, A] => B): Coproduct[F, G, B] = + a coflatMap f + + def duplicate[A](a: Coproduct[F, G, A]): Coproduct[F, G, Coproduct[F, G, A]] = + a.duplicate +} + diff --git a/core/src/main/scala/cats/syntax/all.scala b/core/src/main/scala/cats/syntax/all.scala index 093c8a9daa..c88addc5a9 100644 --- a/core/src/main/scala/cats/syntax/all.scala +++ b/core/src/main/scala/cats/syntax/all.scala @@ -32,3 +32,4 @@ trait AllSyntax with TraverseSyntax with XorSyntax with ValidatedSyntax + with CoproductSyntax diff --git a/core/src/main/scala/cats/syntax/coproduct.scala b/core/src/main/scala/cats/syntax/coproduct.scala new file mode 100644 index 0000000000..60043f9331 --- /dev/null +++ b/core/src/main/scala/cats/syntax/coproduct.scala @@ -0,0 +1,13 @@ +package cats +package syntax + +import cats.data.Coproduct + +trait CoproductSyntax { + implicit def coproductSyntax[F[_], A](a: F[A]): CoproductOps[F, A] = new CoproductOps(a) +} + +final class CoproductOps[F[_], A](val a: F[A]) extends AnyVal { + def leftc[G[_]]: Coproduct[F, G, A] = Coproduct.leftc(a) + def rightc[G[_]]: Coproduct[G, F, A] = Coproduct.rightc(a) +} diff --git a/docs/src/main/tut/freemonad.md b/docs/src/main/tut/freemonad.md index 14c139fabe..d1304b8cbe 100644 --- a/docs/src/main/tut/freemonad.md +++ b/docs/src/main/tut/freemonad.md @@ -348,6 +348,109 @@ it's not too hard to get around.) val result: Map[String, Int] = compilePure(program, Map.empty) ``` +## Composing Free monads ADTs. + +Real world applications often time combine different algebras. +The `Inject` typeclass described by Swierstra in [Data types à la carte](http://www.staff.science.uu.nl/~swier004/Publications/DataTypesALaCarte.pdf) +lets us compose different algebras in the context of `Free`. + +Let's see a trivial example of unrelated ADT's getting composed as a `Coproduct` that can form a more complex program. + +```tut +import cats.arrow.NaturalTransformation +import cats.data.{Xor, Coproduct} +import cats.free.{Inject, Free} +import cats.{Id, ~>} +import scala.collection.mutable.ListBuffer +``` + +```tut +/* Handles user interaction */ +sealed trait Interact[A] +case class Ask(prompt: String) extends Interact[String] +case class Tell(msg: String) extends Interact[Unit] + +/* Represents persistence operations */ +sealed trait DataOp[A] +case class AddCat(a: String) extends DataOp[Unit] +case class GetAllCats() extends DataOp[List[String]] +``` + +Once the ADTs are defined we can formally state that a `Free` program is the Coproduct of it's Algebras. + +```tut +type CatsApp[A] = Coproduct[DataOp, Interact, A] +``` + +In order to take advantage of monadic composition we use smart constructors to lift our Algebra to the `Free` context. + +```tut +class Interacts[F[_]](implicit I: Inject[Interact, F]) { + def tell(msg: String): Free[F, Unit] = Free.inject[Interact, F](Tell(msg)) + def ask(prompt: String): Free[F, String] = Free.inject[Interact, F](Ask(prompt)) +} + +object Interacts { + implicit def interacts[F[_]](implicit I: Inject[Interact, F]): Interacts[F] = new Interacts[F] +} + +class DataSource[F[_]](implicit I: Inject[DataOp, F]) { + def addCat(a: String): Free[F, Unit] = Free.inject[DataOp, F](AddCat(a)) + def getAllCats: Free[F, List[String]] = Free.inject[DataOp, F](GetAllCats()) +} + +object DataSource { + implicit def dataSource[F[_]](implicit I: Inject[DataOp, F]): DataSource[F] = new DataSource[F] +} +``` + +ADTs are now easily composed and trivially intertwined inside monadic contexts. + +```tut +def program(implicit I : Interacts[CatsApp], D : DataSource[CatsApp]) = { + + import I._, D._ + + for { + cat <- ask("What's the kitty's name") + _ <- addCat(cat) + cats <- getAllCats + _ <- tell(cats.toString) + } yield () +} +``` + +Finally we write one interpreter per ADT and combine them with a `NaturalTransformation` to `Coproduct` so they can be +compiled and applied to our `Free` program. + +```scala +object ConsoleCatsInterpreter extends (Interact ~> Id) { + def apply[A](i: Interact[A]) = i match { + case Ask(prompt) => + println(prompt) + scala.io.StdIn.readLine() + case Tell(msg) => + println(msg) + } +} + +object InMemoryDatasourceInterpreter extends (DataOp ~> Id) { + + private[this] val memDataSet = new ListBuffer[String] + + def apply[A](fa: DataOp[A]) = fa match { + case AddCat(a) => memDataSet.append(a); () + case GetAllCats() => memDataSet.toList + } +} + +val interpreter: CatsApp ~> Id = NaturalTransformation.or(InMemoryDatasourceInterpreter, ConsoleCatsInterpreter) + +import DataSource._, Interacts._ + +val evaled = program.foldMap(interpreter) +``` + ## For the curious ones: what is Free in theory? Mathematically-speaking, a *free monad* (at least in the programming diff --git a/free/src/main/scala/cats/free/Free.scala b/free/src/main/scala/cats/free/Free.scala index 3580344af2..7acd6be79a 100644 --- a/free/src/main/scala/cats/free/Free.scala +++ b/free/src/main/scala/cats/free/Free.scala @@ -30,6 +30,13 @@ object Free { /** Lift a pure value into Free */ def pure[S[_], A](a: A): Free[S, A] = Pure(a) + final class FreeInjectPartiallyApplied[F[_], G[_]] private[free] { + def apply[A](fa: F[A])(implicit I : Inject[F, G]): Free[G, A] = + Free.liftF(I.inj(fa)) + } + + def inject[F[_], G[_]]: FreeInjectPartiallyApplied[F, G] = new FreeInjectPartiallyApplied + /** * `Free[S, ?]` has a monad for any type constructor `S[_]`. */ diff --git a/free/src/main/scala/cats/free/Inject.scala b/free/src/main/scala/cats/free/Inject.scala new file mode 100644 index 0000000000..486ab04688 --- /dev/null +++ b/free/src/main/scala/cats/free/Inject.scala @@ -0,0 +1,49 @@ +package cats.free + +import cats.Functor +import cats.data.Coproduct + + +/** + * Inject type class as described in "Data types a la carte" (Swierstra 2008). + * + * @see [[http://www.staff.science.uu.nl/~swier004/Publications/DataTypesALaCarte.pdf]] + */ +sealed abstract class Inject[F[_], G[_]] { + def inj[A](fa: F[A]): G[A] + + def prj[A](ga: G[A]): Option[F[A]] +} + +private[free] sealed abstract class InjectInstances { + implicit def reflexiveInjectInstance[F[_]] = + new Inject[F, F] { + def inj[A](fa: F[A]): F[A] = fa + + def prj[A](ga: F[A]): Option[F[A]] = Option(ga) + } + + implicit def leftInjectInstance[F[_], G[_]] = + new Inject[F, Coproduct[F, G, ?]] { + def inj[A](fa: F[A]): Coproduct[F, G, A] = Coproduct.leftc(fa) + + def prj[A](ga: Coproduct[F, G, A]): Option[F[A]] = ga.run.fold(Option(_), _ => None) + } + + implicit def rightInjectInstance[F[_], G[_], H[_]](implicit I: Inject[F, G]) = + new Inject[F, Coproduct[H, G, ?]] { + def inj[A](fa: F[A]): Coproduct[H, G, A] = Coproduct.rightc(I.inj(fa)) + + def prj[A](ga: Coproduct[H, G, A]): Option[F[A]] = ga.run.fold(_ => None, I.prj(_)) + } +} + +object Inject extends InjectInstances { + def inject[F[_], G[_], A](ga: G[Free[F, A]])(implicit I: Inject[G, F]): Free[F, A] = + Free.liftF(I.inj(ga)) flatMap identity + + def match_[F[_], G[_], A](fa: Free[F, A])(implicit F: Functor[F], I: Inject[G, F]): Option[G[Free[F, A]]] = + fa.resume.fold(I.prj, _ => None) + + def apply[F[_], G[_]](implicit I: Inject[F, G]): Inject[F, G] = I +} diff --git a/free/src/main/scala/cats/free/package.scala b/free/src/main/scala/cats/free/package.scala index 2942a76ac4..dd7d36a8e8 100644 --- a/free/src/main/scala/cats/free/package.scala +++ b/free/src/main/scala/cats/free/package.scala @@ -4,4 +4,11 @@ package object free { /** Alias for the free monad over the `Function0` functor. */ type Trampoline[A] = Free[Function0, A] object Trampoline extends TrampolineFunctions + + /** [[cats.free.Inject]][F, G] */ + type :<:[F[_], G[_]] = Inject[F, G] + + /** [[cats.free.Inject]][F, G] */ + type :≺:[F[_], G[_]] = Inject[F, G] + } diff --git a/free/src/test/scala/cats/free/InjectTests.scala b/free/src/test/scala/cats/free/InjectTests.scala new file mode 100644 index 0000000000..7a83165bee --- /dev/null +++ b/free/src/test/scala/cats/free/InjectTests.scala @@ -0,0 +1,105 @@ +package cats +package free + +import cats.arrow.NaturalTransformation +import cats.data.{Xor, Coproduct} +import cats.laws.discipline.arbitrary +import cats.tests.CatsSuite +import org.scalacheck._ + +class InjectTests extends CatsSuite { + + import Inject._ + + sealed trait Test1Algebra[A] + + case class Test1[A](value : Int, f: Int => A) extends Test1Algebra[A] + + sealed trait Test2Algebra[A] + + case class Test2[A](value : Int, f: Int => A) extends Test2Algebra[A] + + type T[A] = Coproduct[Test1Algebra, Test2Algebra, A] + + implicit def test1AlgebraAFunctor: Functor[Test1Algebra] = + new Functor[Test1Algebra] { + def map[A, B](a: Test1Algebra[A])(f: A => B): Test1Algebra[B] = a match { + case Test1(k, h) => Test1(k, x => f(h(x))) + } + } + + implicit def test2AlgebraAFunctor: Functor[Test2Algebra] = + new Functor[Test2Algebra] { + def map[A, B](a: Test2Algebra[A])(f: A => B): Test2Algebra[B] = a match { + case Test2(k, h) => Test2(k, x => f(h(x))) + } + } + + implicit def test1Arbitrary[A](implicit seqArb: Arbitrary[Int], intAArb : Arbitrary[Int => A]): Arbitrary[Test1[A]] = + Arbitrary(for {s <- seqArb.arbitrary; f <- intAArb.arbitrary} yield Test1(s, f)) + + implicit def test2Arbitrary[A](implicit seqArb: Arbitrary[Int], intAArb : Arbitrary[Int => A]): Arbitrary[Test2[A]] = + Arbitrary(for {s <- seqArb.arbitrary; f <- intAArb.arbitrary} yield Test2(s, f)) + + object Test1Interpreter extends (Test1Algebra ~> Id) { + override def apply[A](fa: Test1Algebra[A]): Id[A] = fa match { + case Test1(k, h) => Id.pure[A](h(k)) + } + } + + object Test2Interpreter extends (Test2Algebra ~> Id) { + override def apply[A](fa: Test2Algebra[A]): Id[A] = fa match { + case Test2(k, h) => Id.pure[A](h(k)) + } + } + + val coProductInterpreter: T ~> Id = NaturalTransformation.or(Test1Interpreter, Test2Interpreter) + + val x: Free[T, Int] = Free.inject[Test1Algebra, T](Test1(1, identity)) + + test("inj") { + forAll { (x: Int, y: Int) => + def res[F[_]] + (implicit I0: Test1Algebra :<: F, + I1: Test2Algebra :<: F): Free[F, Int] = { + for { + a <- Free.inject[Test1Algebra, F](Test1(x, identity)) + b <- Free.inject[Test2Algebra, F](Test2(y, identity)) + } yield a + b + } + (res[T] foldMap coProductInterpreter) == Id.pure(x + y) should ===(true) + } + } + + test("prj") { + def distr[F[_], A](f: Free[F, A]) + (implicit + F: Functor[F], + I0: Test1Algebra :<: F, + I1: Test2Algebra :<: F): Option[Free[F, A]] = + for { + Test1(x, h) <- match_[F, Test1Algebra, A](f) + Test2(y, k) <- match_[F, Test2Algebra, A](h(x)) + } yield k(x + y) + + forAll { (x: Int, y: Int) => + val expr1: Free[T, Int] = Inject.inject[T, Test1Algebra, Int](Test1(x, Free.pure)) + val expr2: Free[T, Int] = Inject.inject[T, Test2Algebra, Int](Test2(y, Free.pure)) + val res = distr[T, Int](expr1 >> expr2) + res == Some(Free.pure(x + y)) should ===(true) + } + } + + test("apply in left") { + forAll { (y: Test1[Int]) => + Inject[Test1Algebra, T].inj(y) == Coproduct(Xor.Left(y)) should ===(true) + } + } + + test("apply in right") { + forAll { (y: Test2[Int]) => + Inject[Test2Algebra, T].inj(y) == Coproduct(Xor.Right(y)) should ===(true) + } + } + +} diff --git a/laws/src/main/scala/cats/laws/discipline/Arbitrary.scala b/laws/src/main/scala/cats/laws/discipline/Arbitrary.scala index c359327e59..f9c21465be 100644 --- a/laws/src/main/scala/cats/laws/discipline/Arbitrary.scala +++ b/laws/src/main/scala/cats/laws/discipline/Arbitrary.scala @@ -92,6 +92,15 @@ object arbitrary extends ArbitraryInstances0 { // until this is provided by scalacheck implicit def partialFunctionArbitrary[A, B](implicit F: Arbitrary[A => Option[B]]): Arbitrary[PartialFunction[A, B]] = Arbitrary(F.arbitrary.map(Function.unlift)) + + implicit def coproductArbitrary[F[_], G[_], A](implicit F: Arbitrary[F[A]], G: Arbitrary[G[A]]): Arbitrary[Coproduct[F, G, A]] = + Arbitrary(Gen.oneOf( + F.arbitrary.map(Coproduct.leftc[F, G, A]), + G.arbitrary.map(Coproduct.rightc[F, G, A]))) + + implicit def showArbitrary[A: Arbitrary]: Arbitrary[Show[A]] = + Arbitrary(Show.fromToString[A]) + } private[discipline] sealed trait ArbitraryInstances0 { diff --git a/tests/src/test/scala/cats/tests/CoproductTests.scala b/tests/src/test/scala/cats/tests/CoproductTests.scala new file mode 100644 index 0000000000..8799c59242 --- /dev/null +++ b/tests/src/test/scala/cats/tests/CoproductTests.scala @@ -0,0 +1,54 @@ +package cats.tests + +import algebra.Eq +import algebra.laws.OrderLaws +import cats._ +import cats.data.Coproduct +import cats.functor.Contravariant +import cats.laws.discipline._ +import cats.laws.discipline.arbitrary._ +import org.scalacheck.Arbitrary + +class CoproductTests extends CatsSuite { + + checkAll("Coproduct[Option, Option, ?]", TraverseTests[Coproduct[Option, Option, ?]].traverse[Int, Int, Int, Int, Option, Option]) + checkAll("Traverse[Coproduct[Option, Option, ?]]", SerializableTests.serializable(Traverse[Coproduct[Option, Option, ?]])) + + { + implicit val foldable = Coproduct.coproductFoldable[Option, Option] + checkAll("Coproduct[Option, Option, ?]", FoldableTests[Coproduct[Option, Option, ?]].foldable[Int, Int]) + checkAll("Foldable[Coproduct[Option, Option, ?]]", SerializableTests.serializable(Foldable[Coproduct[Option, Option, ?]])) + } + + checkAll("Coproduct[Eval, Eval, ?]", ComonadTests[Coproduct[Eval, Eval, ?]].comonad[Int, Int, Int]) + checkAll("Comonad[Coproduct[Eval, Eval, ?]]", SerializableTests.serializable(Comonad[Coproduct[Eval, Eval, ?]])) + + { + implicit val coflatMap = Coproduct.coproductCoflatMap[Eval, Eval] + checkAll("Coproduct[Eval, Eval, ?]", CoflatMapTests[Coproduct[Eval, Eval, ?]].coflatMap[Int, Int, Int]) + checkAll("CoflatMap[Coproduct[Eval, Eval, ?]]", SerializableTests.serializable(CoflatMap[Coproduct[Eval, Eval, ?]])) + } + + checkAll("Coproduct[Option, Option, Int]", OrderLaws[Coproduct[Option, Option, Int]].eqv) + checkAll("Eq[Coproduct[Option, Option, Int]]", SerializableTests.serializable(Eq[Coproduct[Option, Option, Int]])) + + implicit def showEq[A](implicit arbA: Arbitrary[A], stringEq: Eq[String]): Eq[Show[A]] = new Eq[Show[A]] { + def eqv(f: Show[A], g: Show[A]): Boolean = { + val samples = List.fill(100)(arbA.arbitrary.sample).collect { + case Some(a) => a + case None => sys.error("Could not generate arbitrary values to compare two Show[A]") + } + samples.forall(s => stringEq.eqv(f.show(s), g.show(s))) + } + } + + checkAll("Coproduct[Show, Show, ?]", ContravariantTests[Coproduct[Show, Show, ?]].contravariant[Int, Int, Int]) + checkAll("Contravariant[Coproduct[Show, Show, ?]]", SerializableTests.serializable(Contravariant[Coproduct[Show, Show, ?]])) + + test("double swap is identity") { + forAll { (x: Coproduct[Option, Option, Int]) => + x.swap.swap should ===(x) + } + } + +} diff --git a/tests/src/test/scala/cats/tests/NaturalTransformationTests.scala b/tests/src/test/scala/cats/tests/NaturalTransformationTests.scala index 44c26e7dfd..9669edbb75 100644 --- a/tests/src/test/scala/cats/tests/NaturalTransformationTests.scala +++ b/tests/src/test/scala/cats/tests/NaturalTransformationTests.scala @@ -2,6 +2,7 @@ package cats package tests import cats.arrow.NaturalTransformation +import cats.data.Coproduct class NaturalTransformationTests extends CatsSuite { @@ -15,6 +16,28 @@ class NaturalTransformationTests extends CatsSuite { def apply[A](fa: Option[A]): List[A] = fa.toList } + sealed trait Test1Algebra[A] { + def v : A + } + + case class Test1[A](v : A) extends Test1Algebra[A] + + sealed trait Test2Algebra[A] { + def v : A + } + + case class Test2[A](v : A) extends Test2Algebra[A] + + object Test1NT extends (Test1Algebra ~> Id) { + override def apply[A](fa: Test1Algebra[A]): Id[A] = Id.pure(fa.v) + } + + object Test2NT extends (Test2Algebra ~> Id) { + override def apply[A](fa: Test2Algebra[A]): Id[A] = Id.pure(fa.v) + } + + type T[A] = Coproduct[Test1Algebra, Test2Algebra, A] + test("compose") { forAll { (list: List[Int]) => val listToList = optionToList.compose(listToOption) @@ -34,4 +57,12 @@ class NaturalTransformationTests extends CatsSuite { NaturalTransformation.id[List].apply(list) should === (list) } } + + test("or") { + val combinedInterpreter = NaturalTransformation.or(Test1NT, Test2NT) + forAll { (a : Int, b : Int) => + (combinedInterpreter(Coproduct.left(Test1(a))) == Id.pure(a)) should ===(true) + (combinedInterpreter(Coproduct.right(Test2(b))) == Id.pure(b)) should ===(true) + } + } }