From 08bc6ad8cf23ab0967f2d470fbd394bb4adb5a88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ra=C3=BAl=20Raja=20Mart=C3=ADnez?= Date: Wed, 11 Nov 2015 13:26:50 +0100 Subject: [PATCH 01/18] Progress toward Coproduct and Inject --- core/src/main/scala/cats/data/Coproduct.scala | 220 ++++++++++++++++++ core/src/main/scala/cats/syntax/all.scala | 1 + .../main/scala/cats/syntax/coproduct.scala | 13 ++ free/src/main/scala/cats/free/Inject.scala | 49 ++++ .../main/scala/cats/laws/CoproductLaws.scala | 10 + .../scala/cats/tests/CoproductTests.scala | 178 ++++++++++++++ 6 files changed, 471 insertions(+) create mode 100644 core/src/main/scala/cats/data/Coproduct.scala create mode 100644 core/src/main/scala/cats/syntax/coproduct.scala create mode 100644 free/src/main/scala/cats/free/Inject.scala create mode 100644 laws/src/main/scala/cats/laws/CoproductLaws.scala create mode 100644 tests/src/test/scala/cats/tests/CoproductTests.scala 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..cd081b3e6c --- /dev/null +++ b/core/src/main/scala/cats/data/Coproduct.scala @@ -0,0 +1,220 @@ +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)) + } + + /** Like `Coproduct.leftc`, but specify only the `G` + * @example {{{ + * Coproduct.left[Option](List(1)) // Coproduct[List, Option, Int](Xor.Left(List(1))) + * }}} + */ + def left[G[_]]: CoproductLeft[G] = new CoproductLeft[G] + + /** Like `Coproduct.rightc`, but specify only the `F` */ + def right[F[_]]: CoproductRight[F] = new CoproductRight[F] +} + +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 + } +} + +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 + } +} + +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 + } +} + +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 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 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 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 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 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 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..37fcbdea94 --- /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) +} + +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/free/src/main/scala/cats/free/Inject.scala b/free/src/main/scala/cats/free/Inject.scala new file mode 100644 index 0000000000..e49cdb4635 --- /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 trait Inject[F[_], G[_]] { + def inj[A](fa: F[A]): G[A] + + def prj[A](ga: G[A]): Option[F[A]] +} + +sealed abstract class InjectInstances { + implicit def reflexiveInjectInstance[F[_]] = + new Inject[F, F] { + def inj[A](fa: F[A]) = fa + + def prj[A](ga: F[A]) = Option(ga) + } + + implicit def leftInjectInstance[F[_], G[_]] = + new Inject[F, Coproduct[F, G, ?]] { + def inj[A](fa: F[A]) = Coproduct.leftc(fa) + + def prj[A](ga: Coproduct[F, G, 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.rightc(I.inj(fa)) + + def prj[A](ga: Coproduct[H, G, 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/laws/src/main/scala/cats/laws/CoproductLaws.scala b/laws/src/main/scala/cats/laws/CoproductLaws.scala new file mode 100644 index 0000000000..4cf58d4fe9 --- /dev/null +++ b/laws/src/main/scala/cats/laws/CoproductLaws.scala @@ -0,0 +1,10 @@ +package cats.laws + +/** + * Laws that must be obeyed by any `Coproduct`. + * + * Does this makes sense so... extends MonadLaws[Coproduct[F, G, ?]] + * with ComonadLaws[Coproduct[F, G, ?]] + * with TraverseLaws[Coproduct[F, G, ?]... ? + */ +trait CoproductLaws[F[_], G[_], A] 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..5fa9559339 --- /dev/null +++ b/tests/src/test/scala/cats/tests/CoproductTests.scala @@ -0,0 +1,178 @@ +package cats.tests + +import algebra.laws.{GroupLaws, OrderLaws} +import cats.data.Xor +import cats.data.Xor._ +import cats.laws.discipline.arbitrary.xorArbitrary +import cats.laws.discipline.{BifunctorTests, MonadErrorTests, SerializableTests, TraverseTests} +import org.scalacheck.Arbitrary +import org.scalacheck.Arbitrary._ + +import scala.util.Try + +class CoproductTests extends CatsSuite { + checkAll("Xor[String, Int]", GroupLaws[Xor[String, Int]].monoid) + + checkAll("Xor[String, Int]", MonadErrorTests[Xor[String, ?], String].monadError[Int, Int, Int]) + checkAll("MonadError[Xor, String]", SerializableTests.serializable(MonadError[Xor[String, ?], String])) + + checkAll("Xor[String, Int] with Option", TraverseTests[Xor[String, ?]].traverse[Int, Int, Int, Int, Option, Option]) + checkAll("Traverse[Xor[String,?]]", SerializableTests.serializable(Traverse[Xor[String, ?]])) + + checkAll("Xor[Int, String]", OrderLaws[String Xor Int].order) + + implicit val arbitraryXor: Arbitrary[Xor[Int, String]] = Arbitrary { + for { + left <- arbitrary[Boolean] + xor <- if (left) arbitrary[Int].map(Xor.left) + else arbitrary[String].map(Xor.right) + } yield xor + } + + checkAll("? Xor ?", BifunctorTests[Xor].bifunctor[Int, Int, Int, String, String, String]) + + test("fromTryCatch catches matching exceptions") { + assert(Xor.fromTryCatch[NumberFormatException]{ "foo".toInt }.isInstanceOf[Xor.Left[NumberFormatException]]) + } + + test("fromTryCatch lets non-matching exceptions escape") { + val _ = intercept[NumberFormatException] { + Xor.fromTryCatch[IndexOutOfBoundsException]{ "foo".toInt } + } + } + + test("fromTry is left for failed Try") { + forAll { t: Try[Int] => + t.isFailure should === (Xor.fromTry(t).isLeft) + } + } + + test("fromEither isRight consistent with Either.isRight"){ + forAll { e: Either[Int, String] => + Xor.fromEither(e).isRight should === (e.isRight) + } + } + + test("fromOption isLeft consistent with Option.isEmpty") { + forAll { (o: Option[Int], s: String) => + Xor.fromOption(o, s).isLeft should === (o.isEmpty) + } + } + + test("double swap is identity") { + forAll { (x: Int Xor String) => + x.swap.swap should === (x) + } + } + + test("foreach is noop for left") { + forAll { (x: Int Xor String) => + var count = 0 + x.foreach{ _ => count += 1} + (count == 0) should === (x.isLeft) + } + } + + test("getOrElse ignores default for right") { + forAll { (x: Int Xor String, s: String, t: String) => + whenever(x.isRight) { + x.getOrElse(s) should === (x.getOrElse(t)) + } + } + } + + test("orElse") { + forAll { (x: Int Xor String, y: Int Xor String) => + val z = x.orElse(y) + (z === (x)) || (z === (y)) should === (true) + } + } + + test("recover recovers handled values") { + val xor = Xor.left[String, Int]("xor") + xor.recover { case "xor" => 5 }.isRight should === (true) + } + + test("recover ignores unhandled values") { + val xor = Xor.left[String, Int]("xor") + xor.recover { case "notxor" => 5 } should === (xor) + } + + test("recover ignores the right side") { + val xor = Xor.right[String, Int](10) + xor.recover { case "xor" => 5 } should === (xor) + } + + test("recoverWith recovers handled values") { + val xor = Xor.left[String, Int]("xor") + xor.recoverWith { case "xor" => Xor.right[String, Int](5) }.isRight should === (true) + } + + test("recoverWith ignores unhandled values") { + val xor = Xor.left[String, Int]("xor") + xor.recoverWith { case "notxor" => Xor.right[String, Int](5) } should === (xor) + } + + test("recoverWith ignores the right side") { + val xor = Xor.right[String, Int](10) + xor.recoverWith { case "xor" => Xor.right[String, Int](5) } should === (xor) + } + + test("valueOr consistent with swap then map then merge") { + forAll { (x: Int Xor String, f: Int => String) => + x.valueOr(f) should === (x.swap.map(f).merge) + } + } + + test("isLeft implies forall") { + forAll { (x: Int Xor String, p: String => Boolean) => + whenever(x.isLeft) { + x.forall(p) should === (true) + } + } + } + + test("isLeft implies exists is false") { + forAll { (x: Int Xor String, p: String => Boolean) => + whenever(x.isLeft) { + x.exists(p) should === (false) + } + } + } + + test("ensure on left is identity") { + forAll { (x: Int Xor String, i: Int, p: String => Boolean) => + whenever(x.isLeft) { + x.ensure(i)(p) should === (x) + } + } + } + + test("toIor then toXor is identity") { + forAll { (x: Int Xor String) => + x.toIor.toXor should === (x) + } + } + + test("isLeft consistency") { + forAll { (x: Int Xor String) => + x.isLeft should === (x.toEither.isLeft) + x.isLeft should === (x.toOption.isEmpty) + x.isLeft should === (x.toList.isEmpty) + x.isLeft should === (x.toValidated.isInvalid) + } + } + + test("withValidated") { + forAll { (x: Int Xor String, f: Int => Double) => + x.withValidated(_.bimap(f, identity)) should === (x.leftMap(f)) + } + } + + test("combine is right iff both operands are right") { + forAll { (x: Int Xor String, y: Int Xor String) => + x.combine(y).isRight should === (x.isRight && y.isRight) + } + } + +} From df4d72a2ebeafc517bad3a7be0c4d8099184e46a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ra=C3=BAl=20Raja=20Mart=C3=ADnez?= Date: Wed, 11 Nov 2015 13:29:04 +0100 Subject: [PATCH 02/18] Removed tests for the time being --- .../scala/cats/tests/CoproductTests.scala | 175 +----------------- 1 file changed, 1 insertion(+), 174 deletions(-) diff --git a/tests/src/test/scala/cats/tests/CoproductTests.scala b/tests/src/test/scala/cats/tests/CoproductTests.scala index 5fa9559339..c6d30b2b04 100644 --- a/tests/src/test/scala/cats/tests/CoproductTests.scala +++ b/tests/src/test/scala/cats/tests/CoproductTests.scala @@ -1,178 +1,5 @@ package cats.tests -import algebra.laws.{GroupLaws, OrderLaws} -import cats.data.Xor -import cats.data.Xor._ -import cats.laws.discipline.arbitrary.xorArbitrary -import cats.laws.discipline.{BifunctorTests, MonadErrorTests, SerializableTests, TraverseTests} -import org.scalacheck.Arbitrary -import org.scalacheck.Arbitrary._ - -import scala.util.Try - +//TODO class CoproductTests extends CatsSuite { - checkAll("Xor[String, Int]", GroupLaws[Xor[String, Int]].monoid) - - checkAll("Xor[String, Int]", MonadErrorTests[Xor[String, ?], String].monadError[Int, Int, Int]) - checkAll("MonadError[Xor, String]", SerializableTests.serializable(MonadError[Xor[String, ?], String])) - - checkAll("Xor[String, Int] with Option", TraverseTests[Xor[String, ?]].traverse[Int, Int, Int, Int, Option, Option]) - checkAll("Traverse[Xor[String,?]]", SerializableTests.serializable(Traverse[Xor[String, ?]])) - - checkAll("Xor[Int, String]", OrderLaws[String Xor Int].order) - - implicit val arbitraryXor: Arbitrary[Xor[Int, String]] = Arbitrary { - for { - left <- arbitrary[Boolean] - xor <- if (left) arbitrary[Int].map(Xor.left) - else arbitrary[String].map(Xor.right) - } yield xor - } - - checkAll("? Xor ?", BifunctorTests[Xor].bifunctor[Int, Int, Int, String, String, String]) - - test("fromTryCatch catches matching exceptions") { - assert(Xor.fromTryCatch[NumberFormatException]{ "foo".toInt }.isInstanceOf[Xor.Left[NumberFormatException]]) - } - - test("fromTryCatch lets non-matching exceptions escape") { - val _ = intercept[NumberFormatException] { - Xor.fromTryCatch[IndexOutOfBoundsException]{ "foo".toInt } - } - } - - test("fromTry is left for failed Try") { - forAll { t: Try[Int] => - t.isFailure should === (Xor.fromTry(t).isLeft) - } - } - - test("fromEither isRight consistent with Either.isRight"){ - forAll { e: Either[Int, String] => - Xor.fromEither(e).isRight should === (e.isRight) - } - } - - test("fromOption isLeft consistent with Option.isEmpty") { - forAll { (o: Option[Int], s: String) => - Xor.fromOption(o, s).isLeft should === (o.isEmpty) - } - } - - test("double swap is identity") { - forAll { (x: Int Xor String) => - x.swap.swap should === (x) - } - } - - test("foreach is noop for left") { - forAll { (x: Int Xor String) => - var count = 0 - x.foreach{ _ => count += 1} - (count == 0) should === (x.isLeft) - } - } - - test("getOrElse ignores default for right") { - forAll { (x: Int Xor String, s: String, t: String) => - whenever(x.isRight) { - x.getOrElse(s) should === (x.getOrElse(t)) - } - } - } - - test("orElse") { - forAll { (x: Int Xor String, y: Int Xor String) => - val z = x.orElse(y) - (z === (x)) || (z === (y)) should === (true) - } - } - - test("recover recovers handled values") { - val xor = Xor.left[String, Int]("xor") - xor.recover { case "xor" => 5 }.isRight should === (true) - } - - test("recover ignores unhandled values") { - val xor = Xor.left[String, Int]("xor") - xor.recover { case "notxor" => 5 } should === (xor) - } - - test("recover ignores the right side") { - val xor = Xor.right[String, Int](10) - xor.recover { case "xor" => 5 } should === (xor) - } - - test("recoverWith recovers handled values") { - val xor = Xor.left[String, Int]("xor") - xor.recoverWith { case "xor" => Xor.right[String, Int](5) }.isRight should === (true) - } - - test("recoverWith ignores unhandled values") { - val xor = Xor.left[String, Int]("xor") - xor.recoverWith { case "notxor" => Xor.right[String, Int](5) } should === (xor) - } - - test("recoverWith ignores the right side") { - val xor = Xor.right[String, Int](10) - xor.recoverWith { case "xor" => Xor.right[String, Int](5) } should === (xor) - } - - test("valueOr consistent with swap then map then merge") { - forAll { (x: Int Xor String, f: Int => String) => - x.valueOr(f) should === (x.swap.map(f).merge) - } - } - - test("isLeft implies forall") { - forAll { (x: Int Xor String, p: String => Boolean) => - whenever(x.isLeft) { - x.forall(p) should === (true) - } - } - } - - test("isLeft implies exists is false") { - forAll { (x: Int Xor String, p: String => Boolean) => - whenever(x.isLeft) { - x.exists(p) should === (false) - } - } - } - - test("ensure on left is identity") { - forAll { (x: Int Xor String, i: Int, p: String => Boolean) => - whenever(x.isLeft) { - x.ensure(i)(p) should === (x) - } - } - } - - test("toIor then toXor is identity") { - forAll { (x: Int Xor String) => - x.toIor.toXor should === (x) - } - } - - test("isLeft consistency") { - forAll { (x: Int Xor String) => - x.isLeft should === (x.toEither.isLeft) - x.isLeft should === (x.toOption.isEmpty) - x.isLeft should === (x.toList.isEmpty) - x.isLeft should === (x.toValidated.isInvalid) - } - } - - test("withValidated") { - forAll { (x: Int Xor String, f: Int => Double) => - x.withValidated(_.bimap(f, identity)) should === (x.leftMap(f)) - } - } - - test("combine is right iff both operands are right") { - forAll { (x: Int Xor String, y: Int Xor String) => - x.combine(y).isRight should === (x.isRight && y.isRight) - } - } - } From 9bec85dba37afe2bef86fd0e2ea69b1ed10efc15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ra=C3=BAl=20Raja=20Mart=C3=ADnez?= Date: Thu, 12 Nov 2015 20:34:06 +0100 Subject: [PATCH 03/18] Added Coproduct tests --- core/src/main/scala/cats/data/Coproduct.scala | 11 +++--- free/src/main/scala/cats/free/package.scala | 9 +++++ .../cats/laws/discipline/Arbitrary.scala | 12 +++++++ .../scala/cats/tests/CoproductTests.scala | 35 ++++++++++++++++++- 4 files changed, 59 insertions(+), 8 deletions(-) diff --git a/core/src/main/scala/cats/data/Coproduct.scala b/core/src/main/scala/cats/data/Coproduct.scala index cd081b3e6c..2d5a0f3420 100644 --- a/core/src/main/scala/cats/data/Coproduct.scala +++ b/core/src/main/scala/cats/data/Coproduct.scala @@ -75,15 +75,10 @@ object Coproduct extends CoproductInstances { def apply[G[_], A](ga: G[A]): Coproduct[F, G, A] = Coproduct(Xor.right(ga)) } - /** Like `Coproduct.leftc`, but specify only the `G` - * @example {{{ - * Coproduct.left[Option](List(1)) // Coproduct[List, Option, Int](Xor.Left(List(1))) - * }}} - */ def left[G[_]]: CoproductLeft[G] = new CoproductLeft[G] - /** Like `Coproduct.rightc`, but specify only the `F` */ def right[F[_]]: CoproductRight[F] = new CoproductRight[F] + } sealed abstract class CoproductInstances3 { @@ -107,6 +102,7 @@ sealed abstract class CoproductInstances3 { } 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 @@ -134,6 +130,7 @@ sealed abstract class CoproductInstances0 extends CoproductInstances1 { } 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 @@ -142,7 +139,7 @@ sealed abstract class CoproductInstances extends CoproductInstances0 { } } -private trait CoproductFunctor[F[_], G[_]] extends Functor[Coproduct[F, G, ?]] { +private[data] trait CoproductFunctor[F[_], G[_]] extends Functor[Coproduct[F, G, ?]] { implicit def F: Functor[F] implicit def G: Functor[G] diff --git a/free/src/main/scala/cats/free/package.scala b/free/src/main/scala/cats/free/package.scala index 2942a76ac4..c246fe4ad1 100644 --- a/free/src/main/scala/cats/free/package.scala +++ b/free/src/main/scala/cats/free/package.scala @@ -1,7 +1,16 @@ package cats +import _root_.scalaz.Inject + 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/laws/src/main/scala/cats/laws/discipline/Arbitrary.scala b/laws/src/main/scala/cats/laws/discipline/Arbitrary.scala index 7169c22a51..d6134c250b 100644 --- a/laws/src/main/scala/cats/laws/discipline/Arbitrary.scala +++ b/laws/src/main/scala/cats/laws/discipline/Arbitrary.scala @@ -82,4 +82,16 @@ object arbitrary { // 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]) + + + + } diff --git a/tests/src/test/scala/cats/tests/CoproductTests.scala b/tests/src/test/scala/cats/tests/CoproductTests.scala index c6d30b2b04..17fa97c8ea 100644 --- a/tests/src/test/scala/cats/tests/CoproductTests.scala +++ b/tests/src/test/scala/cats/tests/CoproductTests.scala @@ -1,5 +1,38 @@ package cats.tests -//TODO +import algebra.Eq +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, ?]])) + + checkAll("Coproduct[Eval, Eval, ?]", ComonadTests[Coproduct[Eval, Eval, ?]].comonad[Int, Int, Int]) + checkAll("Comonad[Coproduct[Eval, Eval, ?]]", SerializableTests.serializable(Comonad[Coproduct[Eval, Eval, ?]])) + + 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) + } + } + } From 2dc669068e0403dd983490eedbe3a4db676601e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ra=C3=BAl=20Raja=20Mart=C3=ADnez?= Date: Fri, 13 Nov 2015 18:23:45 +0100 Subject: [PATCH 04/18] Progress toward inject tests --- core/src/main/scala/cats/data/Coproduct.scala | 10 +- free/src/main/scala/cats/free/Inject.scala | 2 +- free/src/main/scala/cats/free/package.scala | 2 - .../test/scala/cats/free/InjectTests.scala | 96 +++++++++++++++++++ 4 files changed, 102 insertions(+), 8 deletions(-) create mode 100644 free/src/test/scala/cats/free/InjectTests.scala diff --git a/core/src/main/scala/cats/data/Coproduct.scala b/core/src/main/scala/cats/data/Coproduct.scala index 2d5a0f3420..dff431be0d 100644 --- a/core/src/main/scala/cats/data/Coproduct.scala +++ b/core/src/main/scala/cats/data/Coproduct.scala @@ -148,7 +148,7 @@ private[data] trait CoproductFunctor[F[_], G[_]] extends Functor[Coproduct[F, G, a map f } -private trait CoproductContravariant[F[_], G[_]] extends Contravariant[Coproduct[F, G, ?]] { +private[data] trait CoproductContravariant[F[_], G[_]] extends Contravariant[Coproduct[F, G, ?]] { implicit def F: Contravariant[F] implicit def G: Contravariant[G] @@ -157,7 +157,7 @@ private trait CoproductContravariant[F[_], G[_]] extends Contravariant[Coproduct a contramap f } -private trait CoproductFoldable[F[_], G[_]] extends Foldable[Coproduct[F, G, ?]] { +private[data] trait CoproductFoldable[F[_], G[_]] extends Foldable[Coproduct[F, G, ?]] { implicit def F: Foldable[F] implicit def G: Foldable[G] @@ -172,7 +172,7 @@ private trait CoproductFoldable[F[_], G[_]] extends Foldable[Coproduct[F, G, ?]] fa foldMap f } -private trait CoproductTraverse[F[_], G[_]] extends CoproductFoldable[F, G] with Traverse[Coproduct[F, G, ?]] { +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] @@ -184,7 +184,7 @@ private trait CoproductTraverse[F[_], G[_]] extends CoproductFoldable[F, G] with fa traverse f } -private trait CoproductCoflatMap[F[_], G[_]] extends CoflatMap[Coproduct[F, G, ?]] { +private[data] trait CoproductCoflatMap[F[_], G[_]] extends CoflatMap[Coproduct[F, G, ?]] { implicit def F: CoflatMap[F] implicit def G: CoflatMap[G] @@ -197,7 +197,7 @@ private trait CoproductCoflatMap[F[_], G[_]] extends CoflatMap[Coproduct[F, G, ? } -private trait CoproductComonad[F[_], G[_]] extends Comonad[Coproduct[F, G, ?]] { +private[data] trait CoproductComonad[F[_], G[_]] extends Comonad[Coproduct[F, G, ?]] { implicit def F: Comonad[F] implicit def G: Comonad[G] diff --git a/free/src/main/scala/cats/free/Inject.scala b/free/src/main/scala/cats/free/Inject.scala index e49cdb4635..507d78689d 100644 --- a/free/src/main/scala/cats/free/Inject.scala +++ b/free/src/main/scala/cats/free/Inject.scala @@ -9,7 +9,7 @@ import cats.data.Coproduct * * @see [[http://www.staff.science.uu.nl/~swier004/Publications/DataTypesALaCarte.pdf]] */ -sealed trait Inject[F[_], G[_]] { +sealed abstract class Inject[F[_], G[_]] { def inj[A](fa: F[A]): G[A] def prj[A](ga: G[A]): Option[F[A]] diff --git a/free/src/main/scala/cats/free/package.scala b/free/src/main/scala/cats/free/package.scala index c246fe4ad1..dd7d36a8e8 100644 --- a/free/src/main/scala/cats/free/package.scala +++ b/free/src/main/scala/cats/free/package.scala @@ -1,7 +1,5 @@ package cats -import _root_.scalaz.Inject - package object free { /** Alias for the free monad over the `Function0` functor. */ type Trampoline[A] = Free[Function0, A] 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..c27e382d5a --- /dev/null +++ b/free/src/test/scala/cats/free/InjectTests.scala @@ -0,0 +1,96 @@ +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._ +import org.scalactic.CanEqual +import Free._ + +class InjectTests extends CatsSuite { + + import Inject._ + + sealed trait Test1Algebra[A] + + case class Test1(keys: Seq[Int]) extends Test1Algebra[Seq[Int]] + + sealed trait Test2Algebra[A] + + case class Test2(keys: Seq[Int]) extends Test2Algebra[Seq[Int]] + + type T[A] = Coproduct[Test1Algebra, Test2Algebra, A] + + implicit def test1Arbitrary(implicit seqArb: Arbitrary[Seq[Int]]): Arbitrary[Test1] = + Arbitrary(for {s <- seqArb.arbitrary} yield Test1(s)) + + implicit def test2Arbitrary(implicit seqArb: Arbitrary[Seq[Int]]): Arbitrary[Test2] = + Arbitrary(for {s <- seqArb.arbitrary} yield Test2(s)) + + 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) + } + } + + object Test1Interpreter extends (Test1Algebra ~> Id) { + override def apply[A](fa: Test1Algebra[A]): Id[A] = fa match { + case Test1(k) => k + } + } + + object Test2Interpreter extends (Test2Algebra ~> Id) { + override def apply[A](fa: Test2Algebra[A]): Id[A] = fa match { + case Test2(k) => k + } + } + + val coProductInterpreter: T ~> Id = or(Test1Interpreter, Test2Interpreter) + + def lift[F[_], G[_], A](fa: F[A])(implicit I: Inject[F, G]): Free[G, A] = + Free.liftF(I.inj(fa)) + + class Ops[F[_]](implicit I1: Inject[Test1Algebra, F], I2: Inject[Test2Algebra, F]) { + + def test1(seq: Seq[Int]): Free[T, Seq[Int]] = lift[Test1Algebra, T, Seq[Int]](Test1(seq)) + + def test2(seq: Seq[Int]): Free[T, Seq[Int]] = lift[Test2Algebra, T, Seq[Int]](Test2(seq)) + + } + + object Ops { + + implicit def ops[F[_]](implicit I1: Inject[Test1Algebra, F], I2: Inject[Test2Algebra, F]): Ops[F] = new Ops[F] + + } + + val ops: Ops[T] = implicitly[Ops[T]] + + test("inj") { + forAll { (seq1: Seq[Int], seq2: Seq[Int]) => + val res = + for { + a <- ops.test1(seq1) + b <- ops.test2(seq2) + } yield a ++ b + ((res foldMap coProductInterpreter) == Id.pure(seq1 ++ seq2)) should ===(true) + } + } + + test("apply in left") { + forAll { (y: Test1) => + Inject[Test1Algebra, T].inj(y) == Coproduct(Xor.Left(y)) should ===(true) + } + } + + test("apply in right") { + forAll { (y: Test2) => + Inject[Test2Algebra, T].inj(y) == Coproduct(Xor.Right(y)) should ===(true) + } + } + +} From 414c942be7a3f2727c9c798929c9b60a9511db9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ra=C3=BAl=20Raja=20Mart=C3=ADnez?= Date: Tue, 17 Nov 2015 15:21:00 +0100 Subject: [PATCH 05/18] Inject tests and tutorial --- docs/src/main/tut/freemonad.md | 118 ++++++++++++++++++ .../test/scala/cats/free/InjectTests.scala | 86 +++++++------ 2 files changed, 166 insertions(+), 38 deletions(-) diff --git a/docs/src/main/tut/freemonad.md b/docs/src/main/tut/freemonad.md index 14c139fabe..fead303316 100644 --- a/docs/src/main/tut/freemonad.md +++ b/docs/src/main/tut/freemonad.md @@ -348,6 +348,124 @@ 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. +You may have heard that monads do not compose. +That is not entirely true if you think of an Application as the `Coproduct` of it's algebras. +The `Inject` typeclass described by Swierstra in [Data types à la carte](http://www.staff.science.uu.nl/~swier004/Publications/DataTypesALaCarte.pdf) +let 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 conform 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 we define our ADTs we can state that an Application is the Coproduct of it's Algebras + +```tut +type CatsApp[A] = Coproduct[DataOp, Interact, A] +``` + +We use smart constructors to lift our Algebra to the `Free` context + +```tut +def lift[F[_], G[_], A](fa: F[A])(implicit I: Inject[F, G]): Free[G, A] = + Free.liftF(I.inj(fa)) + +class Interacts[F[_]](implicit I: Inject[Interact, F]) { + def tell(msg: String): Free[F, Unit] = lift(Tell(msg)) + def ask(prompt: String): Free[F, String] = lift(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] = lift[DataOp, F, Unit](AddCat(a)) + def getAllCats: Free[F, List[String]] = lift[DataOp, F, List[String]](GetAllCats()) +} + +object DataSource { + implicit def dataSource[F[_]](implicit I: Inject[DataOp, F]): DataSource[F] = new DataSource[F] +} +``` + +We may now easily compose the ADTs into our program + +```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 when they are 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 + } +} + +def or[F[_], G[_], H[_]](f: F ~> H, g: G ~> H): Coproduct[F, G, ?] ~> H = + new NaturalTransformation[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) + } + } + +val interpreter: CatsApp ~> Id = or(InMemoryDatasourceInterpreter, ConsoleCatsInterpreter) + +import DataSource._, Interacts._ + +val evaled = program.foldMap(interpreter) +``` + +The pattern presented above allows us to compose Monads that result into Coproducts thanks to the Inject typeclass. + ## For the curious ones: what is Free in theory? Mathematically-speaking, a *free monad* (at least in the programming diff --git a/free/src/test/scala/cats/free/InjectTests.scala b/free/src/test/scala/cats/free/InjectTests.scala index c27e382d5a..6a9fdf6b6e 100644 --- a/free/src/test/scala/cats/free/InjectTests.scala +++ b/free/src/test/scala/cats/free/InjectTests.scala @@ -1,13 +1,10 @@ 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._ -import org.scalactic.CanEqual -import Free._ class InjectTests extends CatsSuite { @@ -15,19 +12,33 @@ class InjectTests extends CatsSuite { sealed trait Test1Algebra[A] - case class Test1(keys: Seq[Int]) extends Test1Algebra[Seq[Int]] + case class Test1[A](value : Int, f: Int => A) extends Test1Algebra[A] sealed trait Test2Algebra[A] - case class Test2(keys: Seq[Int]) extends Test2Algebra[Seq[Int]] + case class Test2[A](value : Int, f: Int => A) extends Test2Algebra[A] type T[A] = Coproduct[Test1Algebra, Test2Algebra, A] - implicit def test1Arbitrary(implicit seqArb: Arbitrary[Seq[Int]]): Arbitrary[Test1] = - Arbitrary(for {s <- seqArb.arbitrary} yield Test1(s)) + 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 test2Arbitrary(implicit seqArb: Arbitrary[Seq[Int]]): Arbitrary[Test2] = - Arbitrary(for {s <- seqArb.arbitrary} yield Test2(s)) + 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)) def or[F[_], G[_], H[_]](f: F ~> H, g: G ~> H): Coproduct[F, G, ?] ~> H = new (Coproduct[F, G, ?] ~> H) { @@ -39,56 +50,55 @@ class InjectTests extends CatsSuite { object Test1Interpreter extends (Test1Algebra ~> Id) { override def apply[A](fa: Test1Algebra[A]): Id[A] = fa match { - case Test1(k) => k + 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) => k + case Test2(k, h) => Id.pure[A](h(k)) } } val coProductInterpreter: T ~> Id = or(Test1Interpreter, Test2Interpreter) - def lift[F[_], G[_], A](fa: F[A])(implicit I: Inject[F, G]): Free[G, A] = - Free.liftF(I.inj(fa)) - - class Ops[F[_]](implicit I1: Inject[Test1Algebra, F], I2: Inject[Test2Algebra, F]) { - - def test1(seq: Seq[Int]): Free[T, Seq[Int]] = lift[Test1Algebra, T, Seq[Int]](Test1(seq)) - - def test2(seq: Seq[Int]): Free[T, Seq[Int]] = lift[Test2Algebra, T, Seq[Int]](Test2(seq)) - - } - - object Ops { - - implicit def ops[F[_]](implicit I1: Inject[Test1Algebra, F], I2: Inject[Test2Algebra, F]): Ops[F] = new Ops[F] - + test("inj") { + forAll { (x: Int, y: Int) => + val res = for { + a <- Inject.inject[T, Test1Algebra, Int](Test1(x, Free.pure)) + b <- Inject.inject[T, Test2Algebra, Int](Test2(y, Free.pure)) + } yield a + b + (res foldMap coProductInterpreter) == Id.pure(x + y) should ===(true) + } } - val ops: Ops[T] = implicitly[Ops[T]] - - test("inj") { - forAll { (seq1: Seq[Int], seq2: Seq[Int]) => - val res = - for { - a <- ops.test1(seq1) - b <- ops.test2(seq2) - } yield a ++ b - ((res foldMap coProductInterpreter) == Id.pure(seq1 ++ seq2)) 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.contains(Free.pure(x + y)) should ===(true) } } test("apply in left") { - forAll { (y: Test1) => + forAll { (y: Test1[Int]) => Inject[Test1Algebra, T].inj(y) == Coproduct(Xor.Left(y)) should ===(true) } } test("apply in right") { - forAll { (y: Test2) => + forAll { (y: Test2[Int]) => Inject[Test2Algebra, T].inj(y) == Coproduct(Xor.Right(y)) should ===(true) } } From 8f5a679f4ff07085b82de5961b6edd0a6f25a20a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ra=C3=BAl=20Raja=20Mart=C3=ADnez?= Date: Tue, 17 Nov 2015 15:30:36 +0100 Subject: [PATCH 06/18] Misc cleanup --- laws/src/main/scala/cats/laws/CoproductLaws.scala | 10 ---------- .../main/scala/cats/laws/discipline/Arbitrary.scala | 3 --- 2 files changed, 13 deletions(-) delete mode 100644 laws/src/main/scala/cats/laws/CoproductLaws.scala diff --git a/laws/src/main/scala/cats/laws/CoproductLaws.scala b/laws/src/main/scala/cats/laws/CoproductLaws.scala deleted file mode 100644 index 4cf58d4fe9..0000000000 --- a/laws/src/main/scala/cats/laws/CoproductLaws.scala +++ /dev/null @@ -1,10 +0,0 @@ -package cats.laws - -/** - * Laws that must be obeyed by any `Coproduct`. - * - * Does this makes sense so... extends MonadLaws[Coproduct[F, G, ?]] - * with ComonadLaws[Coproduct[F, G, ?]] - * with TraverseLaws[Coproduct[F, G, ?]... ? - */ -trait CoproductLaws[F[_], G[_], A] diff --git a/laws/src/main/scala/cats/laws/discipline/Arbitrary.scala b/laws/src/main/scala/cats/laws/discipline/Arbitrary.scala index d6134c250b..93f544265c 100644 --- a/laws/src/main/scala/cats/laws/discipline/Arbitrary.scala +++ b/laws/src/main/scala/cats/laws/discipline/Arbitrary.scala @@ -91,7 +91,4 @@ object arbitrary { implicit def showArbitrary[A: Arbitrary]: Arbitrary[Show[A]] = Arbitrary(Show.fromToString[A]) - - - } From 9450dc0788c5b9e429c227640242f60ec86203ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ra=C3=BAl=20Raja=20Mart=C3=ADnez?= Date: Tue, 17 Nov 2015 15:35:58 +0100 Subject: [PATCH 07/18] Misc fixes to tutorial --- docs/src/main/tut/freemonad.md | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/docs/src/main/tut/freemonad.md b/docs/src/main/tut/freemonad.md index fead303316..303b1cb920 100644 --- a/docs/src/main/tut/freemonad.md +++ b/docs/src/main/tut/freemonad.md @@ -351,8 +351,6 @@ val result: Map[String, Int] = compilePure(program, Map.empty) ## Composing Free monads ADTs. Real world applications often time combine different algebras. -You may have heard that monads do not compose. -That is not entirely true if you think of an Application as the `Coproduct` of it's algebras. The `Inject` typeclass described by Swierstra in [Data types à la carte](http://www.staff.science.uu.nl/~swier004/Publications/DataTypesALaCarte.pdf) let us compose different algebras in the context of `Free` @@ -378,13 +376,13 @@ case class AddCat(a: String) extends DataOp[Unit] case class GetAllCats() extends DataOp[List[String]] ``` -Once we define our ADTs we can state that an Application is the Coproduct of it's Algebras +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] ``` -We use smart constructors to lift our Algebra to the `Free` context +In order to take advantage of monadic composition ee use smart constructors to lift our Algebra to the `Free` context ```tut def lift[F[_], G[_], A](fa: F[A])(implicit I: Inject[F, G]): Free[G, A] = @@ -409,7 +407,7 @@ object DataSource { } ``` -We may now easily compose the ADTs into our program +ADTs are now easily composed and trivially intertwined inside monadic contexts ```tut def program(implicit I : Interacts[CatsApp], D : DataSource[CatsApp]) = { @@ -425,8 +423,8 @@ def program(implicit I : Interacts[CatsApp], D : DataSource[CatsApp]) = { } ``` -Finally we write one interpreter per ADT and combine them with a `NaturalTransformation` to `Coproduct` so when they are compiled and -applied to our `Free` program. +Finally we write one interpreter per ADT and combine them with a `NaturalTransformation` to `Coproduct` so when they can be +compiled and applied to our `Free` program. ```scala object ConsoleCatsInterpreter extends (Interact ~> Id) { From ecac4d445856819c5d1be025d65cff8686ae736c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ra=C3=BAl=20Raja=20Mart=C3=ADnez?= Date: Tue, 17 Nov 2015 16:13:09 +0100 Subject: [PATCH 08/18] Fixes some typos in the free monads tutorial --- docs/src/main/tut/freemonad.md | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/docs/src/main/tut/freemonad.md b/docs/src/main/tut/freemonad.md index 303b1cb920..7bc08b941d 100644 --- a/docs/src/main/tut/freemonad.md +++ b/docs/src/main/tut/freemonad.md @@ -352,7 +352,7 @@ val result: Map[String, Int] = compilePure(program, Map.empty) 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) -let us compose different algebras in the context of `Free` +let 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 conform a more complex program. @@ -376,13 +376,13 @@ 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 +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 ee use smart constructors to lift our Algebra to the `Free` context +In order to take advantage of monadic composition we use smart constructors to lift our Algebra to the `Free` context. ```tut def lift[F[_], G[_], A](fa: F[A])(implicit I: Inject[F, G]): Free[G, A] = @@ -407,7 +407,7 @@ object DataSource { } ``` -ADTs are now easily composed and trivially intertwined inside monadic contexts +ADTs are now easily composed and trivially intertwined inside monadic contexts. ```tut def program(implicit I : Interacts[CatsApp], D : DataSource[CatsApp]) = { @@ -462,8 +462,6 @@ import DataSource._, Interacts._ val evaled = program.foldMap(interpreter) ``` -The pattern presented above allows us to compose Monads that result into Coproducts thanks to the Inject typeclass. - ## For the curious ones: what is Free in theory? Mathematically-speaking, a *free monad* (at least in the programming From e9fa26249b5ac733c10a76cfdb75e99e6645d8e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ra=C3=BAl=20Raja=20Mart=C3=ADnez?= Date: Tue, 17 Nov 2015 23:45:06 +0100 Subject: [PATCH 09/18] Removed contains to also support stdlib 2.10.x --- free/src/test/scala/cats/free/InjectTests.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/free/src/test/scala/cats/free/InjectTests.scala b/free/src/test/scala/cats/free/InjectTests.scala index 6a9fdf6b6e..9a9f51fcf9 100644 --- a/free/src/test/scala/cats/free/InjectTests.scala +++ b/free/src/test/scala/cats/free/InjectTests.scala @@ -87,7 +87,7 @@ class InjectTests extends CatsSuite { 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.contains(Free.pure(x + y)) should ===(true) + res == Some(Free.pure(x + y)) should ===(true) } } From 77ccebd59a8d3add977b072f338153d2d0a01f33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ra=C3=BAl=20Raja=20Mart=C3=ADnez?= Date: Tue, 17 Nov 2015 23:47:41 +0100 Subject: [PATCH 10/18] CoProduct Ops is now final --- core/src/main/scala/cats/syntax/coproduct.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/src/main/scala/cats/syntax/coproduct.scala b/core/src/main/scala/cats/syntax/coproduct.scala index 37fcbdea94..b7e48f6de4 100644 --- a/core/src/main/scala/cats/syntax/coproduct.scala +++ b/core/src/main/scala/cats/syntax/coproduct.scala @@ -7,7 +7,7 @@ trait CoproductSyntax { implicit def coproductSyntax[F[_], A](a: F[A]): CoproductOps[F, A] = new CoproductOps(a) } -class CoproductOps[F[_], A](val a: F[A]) extends AnyVal { +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) + def rightc[G[_]]: Coproduct[G, F, Made A] = Coproduct.rightc(a) } From 2c38d37860f8ac8f9becf6756ac8e8c5dbbb1948 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ra=C3=BAl=20Raja=20Mart=C3=ADnez?= Date: Wed, 18 Nov 2015 00:11:49 +0100 Subject: [PATCH 11/18] Broken English fixes and Explici return types in Inject --- core/src/main/scala/cats/syntax/coproduct.scala | 2 +- docs/src/main/tut/freemonad.md | 6 +++--- free/src/main/scala/cats/free/Inject.scala | 12 ++++++------ 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/core/src/main/scala/cats/syntax/coproduct.scala b/core/src/main/scala/cats/syntax/coproduct.scala index b7e48f6de4..60043f9331 100644 --- a/core/src/main/scala/cats/syntax/coproduct.scala +++ b/core/src/main/scala/cats/syntax/coproduct.scala @@ -9,5 +9,5 @@ trait CoproductSyntax { 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, Made A] = Coproduct.rightc(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 7bc08b941d..4a3e8705ec 100644 --- a/docs/src/main/tut/freemonad.md +++ b/docs/src/main/tut/freemonad.md @@ -352,9 +352,9 @@ val result: Map[String, Int] = compilePure(program, Map.empty) 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) -let us compose different algebras in the context of `Free`. +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 conform a more complex program. +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 @@ -423,7 +423,7 @@ def program(implicit I : Interacts[CatsApp], D : DataSource[CatsApp]) = { } ``` -Finally we write one interpreter per ADT and combine them with a `NaturalTransformation` to `Coproduct` so when they can be +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 diff --git a/free/src/main/scala/cats/free/Inject.scala b/free/src/main/scala/cats/free/Inject.scala index 507d78689d..1742d32347 100644 --- a/free/src/main/scala/cats/free/Inject.scala +++ b/free/src/main/scala/cats/free/Inject.scala @@ -18,23 +18,23 @@ sealed abstract class Inject[F[_], G[_]] { sealed abstract class InjectInstances { implicit def reflexiveInjectInstance[F[_]] = new Inject[F, F] { - def inj[A](fa: F[A]) = fa + def inj[A](fa: F[A]): F[A] = fa - def prj[A](ga: F[A]) = Option(ga) + 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.leftc(fa) + def inj[A](fa: F[A]): Coproduct[F, G, A] = Coproduct.leftc(fa) - def prj[A](ga: Coproduct[F, G, A]) = ga.run.fold(Option(_), _ => None) + 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.rightc(I.inj(fa)) + def inj[A](fa: F[A]): Coproduct[H, G, A] = Coproduct.rightc(I.inj(fa)) - def prj[A](ga: Coproduct[H, G, A]) = ga.run.fold(_ => None, I.prj(_)) + def prj[A](ga: Coproduct[H, G, A]): Option[F[A]] = ga.run.fold(_ => None, I.prj(_)) } } From fa8110185f2a2536d4ad939c62ef6a6a9ed1ddbb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ra=C3=BAl=20Raja=20Mart=C3=ADnez?= Date: Wed, 18 Nov 2015 00:26:12 +0100 Subject: [PATCH 12/18] Added Eq instance tests --- tests/src/test/scala/cats/tests/CoproductTests.scala | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/src/test/scala/cats/tests/CoproductTests.scala b/tests/src/test/scala/cats/tests/CoproductTests.scala index 17fa97c8ea..273ffe1c21 100644 --- a/tests/src/test/scala/cats/tests/CoproductTests.scala +++ b/tests/src/test/scala/cats/tests/CoproductTests.scala @@ -1,6 +1,7 @@ package cats.tests import algebra.Eq +import algebra.laws.OrderLaws import cats._ import cats.data.Coproduct import cats.functor.Contravariant @@ -16,6 +17,9 @@ class CoproductTests extends CatsSuite { checkAll("Coproduct[Eval, Eval, ?]", ComonadTests[Coproduct[Eval, Eval, ?]].comonad[Int, Int, Int]) checkAll("Comonad[Coproduct[Eval, Eval, ?]]", SerializableTests.serializable(Comonad[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 { From 4f06759b35fff2100d01fc062be6e1d8ad063bbf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ra=C3=BAl=20Raja=20Mart=C3=ADnez?= Date: Wed, 18 Nov 2015 13:32:49 +0100 Subject: [PATCH 13/18] added private[localpackage] to instances in Coproduct and Inject --- core/src/main/scala/cats/data/Coproduct.scala | 8 ++++---- free/src/main/scala/cats/free/Inject.scala | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/core/src/main/scala/cats/data/Coproduct.scala b/core/src/main/scala/cats/data/Coproduct.scala index dff431be0d..1c4b8c64a0 100644 --- a/core/src/main/scala/cats/data/Coproduct.scala +++ b/core/src/main/scala/cats/data/Coproduct.scala @@ -81,7 +81,7 @@ object Coproduct extends CoproductInstances { } -sealed abstract class CoproductInstances3 { +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) @@ -101,7 +101,7 @@ sealed abstract class CoproductInstances3 { } } -sealed abstract class CoproductInstances2 extends CoproductInstances3 { +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] { @@ -111,7 +111,7 @@ sealed abstract class CoproductInstances2 extends CoproductInstances3 { } } -sealed abstract class CoproductInstances1 extends CoproductInstances2 { +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 @@ -120,7 +120,7 @@ sealed abstract class CoproductInstances1 extends CoproductInstances2 { } } -sealed abstract class CoproductInstances0 extends CoproductInstances1 { +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 diff --git a/free/src/main/scala/cats/free/Inject.scala b/free/src/main/scala/cats/free/Inject.scala index 1742d32347..486ab04688 100644 --- a/free/src/main/scala/cats/free/Inject.scala +++ b/free/src/main/scala/cats/free/Inject.scala @@ -15,7 +15,7 @@ sealed abstract class Inject[F[_], G[_]] { def prj[A](ga: G[A]): Option[F[A]] } -sealed abstract class InjectInstances { +private[free] sealed abstract class InjectInstances { implicit def reflexiveInjectInstance[F[_]] = new Inject[F, F] { def inj[A](fa: F[A]): F[A] = fa From f50c15b6a1adc159175f6b2e557565816d31fa90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ra=C3=BAl=20Raja=20Mart=C3=ADnez?= Date: Wed, 18 Nov 2015 16:52:57 +0100 Subject: [PATCH 14/18] Promoted `or` to `NaturalTransformation.or` + tests + tutorial fixes --- .../cats/arrow/NaturalTransformation.scala | 10 ++++++ docs/src/main/tut/freemonad.md | 10 +----- .../test/scala/cats/free/InjectTests.scala | 11 ++----- .../tests/NaturalTransformationTests.scala | 31 +++++++++++++++++++ 4 files changed, 44 insertions(+), 18 deletions(-) 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/docs/src/main/tut/freemonad.md b/docs/src/main/tut/freemonad.md index 4a3e8705ec..3a746ac214 100644 --- a/docs/src/main/tut/freemonad.md +++ b/docs/src/main/tut/freemonad.md @@ -447,15 +447,7 @@ object InMemoryDatasourceInterpreter extends (DataOp ~> Id) { } } -def or[F[_], G[_], H[_]](f: F ~> H, g: G ~> H): Coproduct[F, G, ?] ~> H = - new NaturalTransformation[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) - } - } - -val interpreter: CatsApp ~> Id = or(InMemoryDatasourceInterpreter, ConsoleCatsInterpreter) +val interpreter: CatsApp ~> Id = NaturalTransformation.or(InMemoryDatasourceInterpreter, ConsoleCatsInterpreter) import DataSource._, Interacts._ diff --git a/free/src/test/scala/cats/free/InjectTests.scala b/free/src/test/scala/cats/free/InjectTests.scala index 9a9f51fcf9..096ef89049 100644 --- a/free/src/test/scala/cats/free/InjectTests.scala +++ b/free/src/test/scala/cats/free/InjectTests.scala @@ -1,6 +1,7 @@ package cats package free +import cats.arrow.NaturalTransformation import cats.data.{Xor, Coproduct} import cats.laws.discipline.arbitrary import cats.tests.CatsSuite @@ -40,14 +41,6 @@ class InjectTests extends CatsSuite { 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)) - 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) - } - } - 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)) @@ -60,7 +53,7 @@ class InjectTests extends CatsSuite { } } - val coProductInterpreter: T ~> Id = or(Test1Interpreter, Test2Interpreter) + val coProductInterpreter: T ~> Id = NaturalTransformation.or(Test1Interpreter, Test2Interpreter) test("inj") { forAll { (x: Int, y: Int) => 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) + } + } } From d0efd8d558be85b371c52d99f73e80cc54504557 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ra=C3=BAl=20Raja=20Mart=C3=ADnez?= Date: Wed, 18 Nov 2015 21:01:04 +0100 Subject: [PATCH 15/18] Adds Free.inject[F[_], G[_]] to easily lift Free algebras to a Free[Coproduct, A] where the coproduct can be used to compose dispair Free Algebras --- free/src/main/scala/cats/free/Free.scala | 7 +++++++ free/src/test/scala/cats/free/InjectTests.scala | 16 +++++++++++----- 2 files changed, 18 insertions(+), 5 deletions(-) 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/test/scala/cats/free/InjectTests.scala b/free/src/test/scala/cats/free/InjectTests.scala index 096ef89049..7a83165bee 100644 --- a/free/src/test/scala/cats/free/InjectTests.scala +++ b/free/src/test/scala/cats/free/InjectTests.scala @@ -55,13 +55,19 @@ class InjectTests extends CatsSuite { 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) => - val res = for { - a <- Inject.inject[T, Test1Algebra, Int](Test1(x, Free.pure)) - b <- Inject.inject[T, Test2Algebra, Int](Test2(y, Free.pure)) - } yield a + b - (res foldMap coProductInterpreter) == Id.pure(x + y) should ===(true) + 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) } } From 1b40683789bb5e3c9e69b80c84e284c2f2a84445 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ra=C3=BAl=20Raja=20Mart=C3=ADnez?= Date: Wed, 18 Nov 2015 21:19:45 +0100 Subject: [PATCH 16/18] Fixed tutorial to reflect changes to Free.inject --- docs/src/main/tut/freemonad.md | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/docs/src/main/tut/freemonad.md b/docs/src/main/tut/freemonad.md index 3a746ac214..d1304b8cbe 100644 --- a/docs/src/main/tut/freemonad.md +++ b/docs/src/main/tut/freemonad.md @@ -385,12 +385,9 @@ 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 -def lift[F[_], G[_], A](fa: F[A])(implicit I: Inject[F, G]): Free[G, A] = - Free.liftF(I.inj(fa)) - class Interacts[F[_]](implicit I: Inject[Interact, F]) { - def tell(msg: String): Free[F, Unit] = lift(Tell(msg)) - def ask(prompt: String): Free[F, String] = lift(Ask(prompt)) + 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 { @@ -398,8 +395,8 @@ object Interacts { } class DataSource[F[_]](implicit I: Inject[DataOp, F]) { - def addCat(a: String): Free[F, Unit] = lift[DataOp, F, Unit](AddCat(a)) - def getAllCats: Free[F, List[String]] = lift[DataOp, F, List[String]](GetAllCats()) + 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 { From 743b75fceedbba8318c07b3a0b01df6e27b7d068 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ra=C3=BAl=20Raja=20Mart=C3=ADnez?= Date: Wed, 18 Nov 2015 21:51:13 +0100 Subject: [PATCH 17/18] Added Foldable and CoflatMap tests --- tests/src/test/scala/cats/tests/CoproductTests.scala | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/src/test/scala/cats/tests/CoproductTests.scala b/tests/src/test/scala/cats/tests/CoproductTests.scala index 273ffe1c21..b320586bf9 100644 --- a/tests/src/test/scala/cats/tests/CoproductTests.scala +++ b/tests/src/test/scala/cats/tests/CoproductTests.scala @@ -14,9 +14,15 @@ 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, ?]])) + 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, ?]])) + 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]])) From 5ea5313877c0743c35325b727ae908aa866e9e78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ra=C3=BAl=20Raja=20Mart=C3=ADnez?= Date: Wed, 18 Nov 2015 22:14:22 +0100 Subject: [PATCH 18/18] Added Foldable and CoflatMap tests explicit implicits in the local scope so they don't pick up those from Traverse and Comonad --- .../src/test/scala/cats/tests/CoproductTests.scala | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/tests/src/test/scala/cats/tests/CoproductTests.scala b/tests/src/test/scala/cats/tests/CoproductTests.scala index b320586bf9..8799c59242 100644 --- a/tests/src/test/scala/cats/tests/CoproductTests.scala +++ b/tests/src/test/scala/cats/tests/CoproductTests.scala @@ -14,14 +14,20 @@ 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, ?]])) - checkAll("Coproduct[Option, Option, ?]", FoldableTests[Coproduct[Option, Option, ?]].foldable[Int, Int]) - checkAll("Foldable[Coproduct[Option, Option, ?]]", SerializableTests.serializable(Foldable[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, ?]])) - checkAll("Coproduct[Eval, Eval, ?]", CoflatMapTests[Coproduct[Eval, Eval, ?]].coflatMap[Int, Int, Int]) - checkAll("CoflatMap[Coproduct[Eval, Eval, ?]]", SerializableTests.serializable(CoflatMap[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]]))