diff --git a/core/src/main/scala/cats/Inject.scala b/core/src/main/scala/cats/Inject.scala new file mode 100644 index 0000000000..7963fd9cd7 --- /dev/null +++ b/core/src/main/scala/cats/Inject.scala @@ -0,0 +1,46 @@ +package cats + +import cats.arrow.FunctionK +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/2008-jfp.pdf]] + */ +sealed abstract class Inject[F[_], G[_]] { + def inj: FunctionK[F, G] + + def prj: FunctionK[G, λ[α => Option[F[α]]]] + + def apply[A](fa: F[A]): G[A] = inj(fa) + + def unapply[A](ga: G[A]): Option[F[A]] = prj(ga) +} + +private[cats] sealed abstract class InjectInstances { + implicit def catsReflexiveInjectInstance[F[_]]: Inject[F, F] = + new Inject[F, F] { + val inj = λ[FunctionK[F, F]](identity(_)) + + val prj = λ[FunctionK[F, λ[α => Option[F[α]]]]](Some(_)) + } + + implicit def catsLeftInjectInstance[F[_], G[_]]: Inject[F, Coproduct[F, G, ?]] = + new Inject[F, Coproduct[F, G, ?]] { + val inj = λ[FunctionK[F, Coproduct[F, G, ?]]](Coproduct.leftc(_)) + + val prj = λ[FunctionK[Coproduct[F, G, ?], λ[α => Option[F[α]]]]](_.run.left.toOption) + } + + implicit def catsRightInjectInstance[F[_], G[_], H[_]](implicit I: Inject[F, G]): Inject[F, Coproduct[H, G, ?]] = + new Inject[F, Coproduct[H, G, ?]] { + val inj = λ[FunctionK[G, Coproduct[H, G, ?]]](Coproduct.rightc(_)) compose I.inj + + val prj = λ[FunctionK[Coproduct[H, G, ?], λ[α => Option[F[α]]]]](_.run.right.toOption.flatMap(I.prj(_))) + } +} + +object Inject extends InjectInstances { + def apply[F[_], G[_]](implicit I: Inject[F, G]): Inject[F, G] = I +} diff --git a/core/src/main/scala/cats/package.scala b/core/src/main/scala/cats/package.scala index 8fefc3a42b..509536c8a6 100644 --- a/core/src/main/scala/cats/package.scala +++ b/core/src/main/scala/cats/package.scala @@ -10,6 +10,11 @@ package object cats { type ⊥ = Nothing type ⊤ = Any + /** [[cats.Inject]][F, G] */ + type :<:[F[_], G[_]] = Inject[F, G] + + /** [[cats.Inject]][F, G] */ + type :≺:[F[_], G[_]] = Inject[F, G] /** * Identity, encoded as `type Id[A] = A`, a convenient alias to make diff --git a/docs/src/main/tut/datatypes/freemonad.md b/docs/src/main/tut/datatypes/freemonad.md index c201556173..8cfa876959 100644 --- a/docs/src/main/tut/datatypes/freemonad.md +++ b/docs/src/main/tut/datatypes/freemonad.md @@ -305,8 +305,8 @@ Let's see a trivial example of unrelated ADT's getting composed as a `Coproduct` ```tut:silent import cats.data.Coproduct -import cats.free.{Inject, Free} -import cats.{Id, ~>} +import cats.free.Free +import cats.{Id, Inject, ~>} import scala.collection.mutable.ListBuffer ``` diff --git a/free/src/main/scala/cats/free/Free.scala b/free/src/main/scala/cats/free/Free.scala index a79a01aa5d..e4e9a8e124 100644 --- a/free/src/main/scala/cats/free/Free.scala +++ b/free/src/main/scala/cats/free/Free.scala @@ -184,6 +184,12 @@ object Free { */ def liftF[F[_], A](value: F[A]): Free[F, A] = Suspend(value) + /** + * Absorb a step into the free monad. + */ + def roll[F[_], A](value: F[Free[F, A]]): Free[F, A] = + liftF(value).flatMap(identity) + /** * Suspend the creation of a `Free[F, A]` value. */ @@ -221,6 +227,12 @@ object Free { Free.liftF(I.inj(fa)) } + def injectRoll[F[_], G[_], A](ga: G[Free[F, A]])(implicit I: Inject[G, F]): Free[F, A] = + Free.roll(I.inj(ga)) + + 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) + /** * `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 deleted file mode 100644 index efa1fdc36e..0000000000 --- a/free/src/main/scala/cats/free/Inject.scala +++ /dev/null @@ -1,49 +0,0 @@ -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/2008-jfp.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 catsFreeReflexiveInjectInstance[F[_]]: Inject[F, F] = - new Inject[F, F] { - def inj[A](fa: F[A]): F[A] = fa - - def prj[A](ga: F[A]): Option[F[A]] = Some(ga) - } - - implicit def catsFreeLeftInjectInstance[F[_], G[_]]: Inject[F, Coproduct[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(Some(_), _ => None) - } - - implicit def catsFreeRightInjectInstance[F[_], G[_], H[_]](implicit I: Inject[F, G]): Inject[F, Coproduct[H, 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 dd7d36a8e8..2942a76ac4 100644 --- a/free/src/main/scala/cats/free/package.scala +++ b/free/src/main/scala/cats/free/package.scala @@ -4,11 +4,4 @@ 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/FreeTests.scala b/free/src/test/scala/cats/free/FreeTests.scala index cfc98a475b..4054805cca 100644 --- a/free/src/test/scala/cats/free/FreeTests.scala +++ b/free/src/test/scala/cats/free/FreeTests.scala @@ -1,10 +1,11 @@ package cats package free -import cats.tests.CatsSuite import cats.arrow.FunctionK +import cats.data.Coproduct import cats.laws.discipline.{CartesianTests, MonadTests, SerializableTests} import cats.laws.discipline.arbitrary.catsLawsArbitraryForFn0 +import cats.tests.CatsSuite import org.scalacheck.{Arbitrary, Gen, Cogen} import Arbitrary.arbFunction1 @@ -107,6 +108,89 @@ class FreeTests extends CatsSuite { } assert(res == Either.left(100000)) } + + sealed trait Test1Algebra[A] + + case class Test1[A](value : Int, f: Int => A) extends Test1Algebra[A] + + object Test1Algebra { + 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 test1AlgebraArbitrary[A](implicit seqArb: Arbitrary[Int], intAArb : Arbitrary[Int => A]): Arbitrary[Test1Algebra[A]] = + Arbitrary(for {s <- seqArb.arbitrary; f <- intAArb.arbitrary} yield Test1(s, f)) + } + + sealed trait Test2Algebra[A] + + case class Test2[A](value : Int, f: Int => A) extends Test2Algebra[A] + + object Test2Algebra { + 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 test2AlgebraArbitrary[A](implicit seqArb: Arbitrary[Int], intAArb : Arbitrary[Int => A]): Arbitrary[Test2Algebra[A]] = + Arbitrary(for {s <- seqArb.arbitrary; f <- intAArb.arbitrary} yield Test2(s, f)) + } + + type T[A] = Coproduct[Test1Algebra, Test2Algebra, A] + + object Test1Interpreter extends FunctionK[Test1Algebra,Id] { + override def apply[A](fa: Test1Algebra[A]): Id[A] = fa match { + case Test1(k, h) => h(k) + } + } + + object Test2Interpreter extends FunctionK[Test2Algebra,Id] { + override def apply[A](fa: Test2Algebra[A]): Id[A] = fa match { + case Test2(k, h) => h(k) + } + } + + val coProductInterpreter: FunctionK[T,Id] = Test1Interpreter or Test2Interpreter + + test(".inject") { + 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) == (x + y) should ===(true) + } + } + + val x: Free[T, Int] = Free.inject[Test1Algebra, T](Test1(1, identity)) + + test(".injectRoll") { + 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) <- Free.match_[F, Test1Algebra, A](f) + Test2(y, k) <- Free.match_[F, Test2Algebra, A](h(x)) + } yield k(x + y) + + forAll { (x: Int, y: Int) => + val expr1: Free[T, Int] = Free.injectRoll[T, Test1Algebra, Int](Test1(x, Free.pure)) + val expr2: Free[T, Int] = Free.injectRoll[T, Test2Algebra, Int](Test2(y, Free.pure)) + val res = distr[T, Int](expr1 >> expr2) + res == Some(Free.pure(x + y)) should ===(true) + } + } } object FreeTests extends FreeTestsInstances { diff --git a/free/src/test/scala/cats/free/InjectTests.scala b/free/src/test/scala/cats/free/InjectTests.scala deleted file mode 100644 index 8e1a97dcaf..0000000000 --- a/free/src/test/scala/cats/free/InjectTests.scala +++ /dev/null @@ -1,104 +0,0 @@ -package cats -package free - -import cats.arrow.FunctionK -import cats.tests.CatsSuite -import cats.data.Coproduct -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 FunctionK[Test1Algebra,Id] { - override def apply[A](fa: Test1Algebra[A]): Id[A] = fa match { - case Test1(k, h) => h(k) - } - } - - object Test2Interpreter extends FunctionK[Test2Algebra,Id] { - override def apply[A](fa: Test2Algebra[A]): Id[A] = fa match { - case Test2(k, h) => h(k) - } - } - - val coProductInterpreter: FunctionK[T,Id] = Test1Interpreter or 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) == (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(Left(y)) should ===(true) - } - } - - test("apply in right") { - forAll { (y: Test2[Int]) => - Inject[Test2Algebra, T].inj(y) == Coproduct(Right(y)) should ===(true) - } - } - -} diff --git a/tests/src/test/scala/cats/tests/InjectTests.scala b/tests/src/test/scala/cats/tests/InjectTests.scala new file mode 100644 index 0000000000..943634283e --- /dev/null +++ b/tests/src/test/scala/cats/tests/InjectTests.scala @@ -0,0 +1,99 @@ +package cats + +import cats.data.Coproduct +import cats.tests.CatsSuite +import org.scalacheck._ + +class InjectTests extends CatsSuite { + + sealed trait Test1Algebra[A] + + case class Test1[A](value : Int, f: Int => A) extends Test1Algebra[A] + + object Test1Algebra { + 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 test1AlgebraArbitrary[A](implicit seqArb: Arbitrary[Int], intAArb : Arbitrary[Int => A]): Arbitrary[Test1Algebra[A]] = + Arbitrary(for {s <- seqArb.arbitrary; f <- intAArb.arbitrary} yield Test1(s, f)) + } + + sealed trait Test2Algebra[A] + + case class Test2[A](value : Int, f: Int => A) extends Test2Algebra[A] + + object Test2Algebra { + 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 test2AlgebraArbitrary[A](implicit seqArb: Arbitrary[Int], intAArb : Arbitrary[Int => A]): Arbitrary[Test2Algebra[A]] = + Arbitrary(for {s <- seqArb.arbitrary; f <- intAArb.arbitrary} yield Test2(s, f)) + } + + type T[A] = Coproduct[Test1Algebra, Test2Algebra, A] + + test("inj & prj") { + def distr[F[_], A](f1: F[A], f2: F[A]) + (implicit + F: Functor[F], + I0: Test1Algebra :<: F, + I1: Test2Algebra :<: F): Option[Int] = + for { + Test1(x, _) <- I0.prj(f1) + Test2(y, _) <- I1.prj(f2) + } yield x + y + + forAll { (x: Int, y: Int) => + val expr1: T[Int] = Inject[Test1Algebra, T].inj(Test1(x, _ + 1)) + val expr2: T[Int] = Inject[Test2Algebra, T].inj(Test2(y, _ * 2)) + val res = distr[T, Int](expr1, expr2) + res should ===(Some(x + y)) + } + } + + test("apply & unapply") { + def distr[F[_], A](f1: F[A], f2: F[A]) + (implicit + F: Functor[F], + I0: Test1Algebra :<: F, + I1: Test2Algebra :<: F): Option[Int] = + for { + Test1(x, _) <- I0.unapply(f1) + Test2(y, _) <- I1.unapply(f2) + } yield x + y + + forAll { (x: Int, y: Int) => + val expr1: T[Int] = Inject[Test1Algebra, T].apply(Test1(x, _ + 1)) + val expr2: T[Int] = Inject[Test2Algebra, T].apply(Test2(y, _ * 2)) + val res = distr[T, Int](expr1, expr2) + res should ===(Some(x + y)) + } + } + + test("apply in left") { + forAll { (y: Test1Algebra[Int]) => + Inject[Test1Algebra, T].inj(y) == Coproduct(Left(y)) should ===(true) + } + } + + test("apply in right") { + forAll { (y: Test2Algebra[Int]) => + Inject[Test2Algebra, T].inj(y) == Coproduct(Right(y)) should ===(true) + } + } + + test("null identity") { + val listIntNull = null.asInstanceOf[List[Int]] + Inject.catsReflexiveInjectInstance[List].inj[Int](listIntNull) should ===(listIntNull) + Inject.catsReflexiveInjectInstance[List].prj[Int](listIntNull) should ===(Some(listIntNull)) + } + +}