From 56fc0c42e4a87d9e487c89cc1a1fa6b5af19521e Mon Sep 17 00:00:00 2001 From: Edmund Noble Date: Tue, 25 Oct 2016 00:01:30 -0400 Subject: [PATCH] Cofree comonad Removed Cofree[List, A] tests and added attribution Remove unused imports Add Reducible instance, separate tests, fix Traverse1 comment Removed unfoldStrategy, added a test for unfold Tests for Cofree.mapBranchingRoot/mapBranchingS/T Add Cofree.tailForced and Cofree.forceAll tests, fix notrunning tests Make instances classes package-private Added Cofree.cata/cataM Add docs Add test for forceTail --- free/src/main/scala/cats/free/Cofree.scala | 153 ++++++++++++++++ .../test/scala/cats/free/CofreeTests.scala | 172 ++++++++++++++++++ .../src/test/scala/cats/tests/EvalTests.scala | 11 -- tests/src/test/scala/cats/tests/Spooky.scala | 13 ++ 4 files changed, 338 insertions(+), 11 deletions(-) create mode 100644 free/src/main/scala/cats/free/Cofree.scala create mode 100644 free/src/test/scala/cats/free/CofreeTests.scala create mode 100644 tests/src/test/scala/cats/tests/Spooky.scala diff --git a/free/src/main/scala/cats/free/Cofree.scala b/free/src/main/scala/cats/free/Cofree.scala new file mode 100644 index 00000000000..537be91f5a7 --- /dev/null +++ b/free/src/main/scala/cats/free/Cofree.scala @@ -0,0 +1,153 @@ +package cats +package free + +/** + * A free comonad for some branching functor `S`. Branching is done lazily using Eval. + * A tree with data at the branches, as opposed to Free which is a tree with data at the leaves. + * Not an instruction set functor made into a program monad as in Free, but an instruction set's outputs as a + * functor made into a tree of the possible worlds reachable using the instruction set. + * + * This Scala implementation of `Cofree` and its usages are derived from + * [[https://github.com/scalaz/scalaz/blob/series/7.3.x/core/src/main/scala/scalaz/Cofree.scala Scalaz's Cofree]], + * originally written by RĂșnar Bjarnason. + */ +final case class Cofree[S[_], A](head: A, tailEval: Eval[S[Cofree[S, A]]]) { + + /** Evaluates and returns the tail of the computation. */ + def tailForced: S[Cofree[S, A]] = tailEval.value + + /** Applies `f` to the head and `g` to the tail. */ + def transform[B](f: A => B, g: Cofree[S, A] => Cofree[S, B])(implicit S: Functor[S]): Cofree[S, B] = + Cofree[S, B](f(head), tailEval.map(S.map(_)(g))) + + /** Map over head and inner `S[_]` branches. */ + def map[B](f: A => B)(implicit S: Functor[S]): Cofree[S, B] = + transform(f, _.map(f)) + + /** Transform the branching functor at the root of the Cofree tree. */ + def mapBranchingRoot(nat: S ~> S)(implicit S: Functor[S]): Cofree[S, A] = + Cofree[S, A](head, tailEval.map(nat(_))) + + /** Transform the branching functor, using the S functor to perform the recursion. */ + def mapBranchingS[T[_]](nat: S ~> T)(implicit S: Functor[S]): Cofree[T, A] = + Cofree[T, A](head, tailEval.map(v => nat(S.map(v)(_.mapBranchingS(nat))))) + + /** Transform the branching functor, using the T functor to perform the recursion. */ + def mapBranchingT[T[_]](nat: S ~> T)(implicit T: Functor[T]): Cofree[T, A] = + Cofree[T, A](head, tailEval.map(v => T.map(nat(v))(_.mapBranchingT(nat)))) + + /** Map `f` over each subtree of the computation. */ + def coflatMap[B](f: Cofree[S, A] => B)(implicit S: Functor[S]): Cofree[S, B] = + Cofree[S, B](f(this), tailEval.map(S.map(_)(_.coflatMap(f)))) + + /** Replace each node in the computation with the subtree from that node downwards */ + def coflatten(implicit S: Functor[S]): Cofree[S, Cofree[S, A]] = + Cofree[S, Cofree[S, A]](this, tailEval.map(S.map(_)(_.coflatten))) + + /** Alias for head. */ + def extract: A = head + + /** Evaluate just the tail. */ + def forceTail: Cofree[S, A] = + Cofree[S, A](head, Eval.now(tailEval.value)) + + /** Evaluate the entire Cofree tree. */ + def forceAll(implicit S: Functor[S]): Cofree[S, A] = + Cofree[S, A](head, Eval.now(tailEval.map(S.map(_)(_.forceAll)).value)) + +} + +object Cofree extends CofreeInstances { + + /** Cofree anamorphism, lazily evaluated. */ + def unfold[F[_], A](a: A)(f: A => F[A])(implicit F: Functor[F]): Cofree[F, A] = + Cofree[F, A](a, Eval.later(F.map(f(a))(unfold(_)(f)))) + + /** + * A stack-safe algebraic recursive fold out of the cofree comonad. + */ + def cata[F[_], A, B](cof: Cofree[F, A])(folder: (A, F[B]) => Eval[B])(implicit F: Traverse[F]): Eval[B] = + F.traverse(cof.tailForced)(cata(_)(folder)).flatMap(folder(cof.head, _)) + + /** + * A monadic recursive fold out of the cofree comonad into a monad which can express Eval's stack-safety. + */ + def cataM[F[_], M[_], A, B](cof: Cofree[F, A])(folder: (A, F[B]) => M[B])(inclusion: Eval ~> M)(implicit F: Traverse[F], M: Monad[M]): M[B] = { + def loop(fr: Cofree[F, A]): Eval[M[B]] = { + val looped: M[F[B]] = F.traverse[M, Cofree[F, A], B](fr.tailForced)(fr => M.flatten(inclusion(Eval.defer(loop(fr))))) + val folded: M[B] = M.flatMap(looped)(fb => folder(fr.head, fb)) + Eval.now(folded) + } + M.flatten(inclusion(loop(cof))) + } + +} + +sealed private[free] abstract class CofreeInstances2 { + /** low priority `Reducible` instance */ + implicit def catsReducibleForCofree[F[_] : Foldable]: Reducible[Cofree[F, ?]] = + new CofreeReducible[F] { + def F = implicitly + } +} + +sealed private[free] abstract class CofreeInstances1 extends CofreeInstances2 { + /** low priority `Traverse` instance */ + implicit def catsTraverseForCofree[F[_] : Traverse]: Traverse[Cofree[F, ?]] = + new CofreeTraverse[F] { + def F = implicitly + } +} + +sealed private[free] abstract class CofreeInstances extends CofreeInstances1 { + implicit def catsFreeComonadForCofree[S[_] : Functor]: Comonad[Cofree[S, ?]] = new CofreeComonad[S] { + def F = implicitly + } +} + +private trait CofreeComonad[S[_]] extends Comonad[Cofree[S, ?]] { + implicit def F: Functor[S] + + override final def extract[A](p: Cofree[S, A]): A = p.extract + + override final def coflatMap[A, B](a: Cofree[S, A])(f: Cofree[S, A] => B): Cofree[S, B] = a.coflatMap(f) + + override final def coflatten[A](a: Cofree[S, A]): Cofree[S, Cofree[S, A]] = a.coflatten + + override final def map[A, B](a: Cofree[S, A])(f: A => B): Cofree[S, B] = a.map(f) +} + +private trait CofreeReducible[F[_]] extends Reducible[Cofree[F, ?]] { + implicit def F: Foldable[F] + + override final def foldMap[A, B](fa: Cofree[F, A])(f: A => B)(implicit M: Monoid[B]): B = + M.combine(f(fa.head), F.foldMap(fa.tailForced)(foldMap(_)(f))) + + override final def foldRight[A, B](fa: Cofree[F, A], z: Eval[B])(f: (A, Eval[B]) => Eval[B]): Eval[B] = + f(fa.head, fa.tailEval.flatMap(F.foldRight(_, z)(foldRight(_, _)(f)))) + + override final def foldLeft[A, B](fa: Cofree[F, A], z: B)(f: (B, A) => B): B = + F.foldLeft(fa.tailForced, f(z, fa.head))((b, cof) => foldLeft(cof, b)(f)) + + override final def reduceLeftTo[A, B](fa: Cofree[F, A])(z: A => B)(f: (B, A) => B): B = + F.foldLeft(fa.tailForced, z(fa.head))((b, cof) => foldLeft(cof, b)(f)) + + override def reduceRightTo[A, B](fa: Cofree[F, A])(z: A => B)(f: (A, Eval[B]) => Eval[B]): Eval[B] = { + foldRight(fa, Eval.now((None: Option[B]))) { + case (l, e) => e.flatMap { + case None => Eval.now(Some(z(l))) + case Some(r) => f(l, Eval.now(r)).map(Some(_)) + } + }.map(_.getOrElse(sys.error("reduceRightTo"))) + } + +} + +private trait CofreeTraverse[F[_]] extends Traverse[Cofree[F, ?]] with CofreeReducible[F] with CofreeComonad[F] { + implicit def F: Traverse[F] + + override final def traverse[G[_], A, B](fa: Cofree[F, A])(f: A => G[B])(implicit G: Applicative[G]): G[Cofree[F, B]] = + G.map2(f(fa.head), F.traverse(fa.tailForced)(traverse(_)(f)))((h, t) => Cofree[F, B](h, Eval.now(t))) + +} + diff --git a/free/src/test/scala/cats/free/CofreeTests.scala b/free/src/test/scala/cats/free/CofreeTests.scala new file mode 100644 index 00000000000..d0c49d6aab1 --- /dev/null +++ b/free/src/test/scala/cats/free/CofreeTests.scala @@ -0,0 +1,172 @@ +package cats +package free + +import cats.data.{NonEmptyList, OptionT} +import cats.laws.discipline.{CartesianTests, ComonadTests, ReducibleTests, SerializableTests, TraverseTests} +import cats.syntax.list._ +import cats.tests.{CatsSuite, Spooky} +import org.scalacheck.{Arbitrary, Cogen, Gen} + +class CofreeTests extends CatsSuite { + + import CofreeTests._ + + implicit val iso = CartesianTests.Isomorphisms.invariant[Cofree[Option, ?]] + + checkAll("Cofree[Option, ?]", ComonadTests[Cofree[Option, ?]].comonad[Int, Int, Int]) + locally { + implicit val instance = Cofree.catsTraverseForCofree[Option] + checkAll("Cofree[Option, ?]", TraverseTests[Cofree[Option, ?]].traverse[Int, Int, Int, Int, Option, Option]) + checkAll("Traverse[Cofree[Option, ?]]", SerializableTests.serializable(Traverse[Cofree[Option, ?]])) + } + locally { + implicit val instance = Cofree.catsReducibleForCofree[Option] + checkAll("Cofree[Option, ?]", ReducibleTests[Cofree[Option, ?]].reducible[Option, Int, Int]) + checkAll("Reducible[Cofree[Option, ?]]", SerializableTests.serializable(Reducible[Cofree[Option, ?]])) + } + checkAll("Comonad[Cofree[Option, ?]]", SerializableTests.serializable(Comonad[Cofree[Option, ?]])) + + test("Cofree.unfold") { + val unfoldedHundred: CofreeNel[Int] = Cofree.unfold[Option, Int](0)(i => if (i == 100) None else Some(i + 1)) + val nelUnfoldedHundred: NonEmptyList[Int] = NonEmptyList.fromListUnsafe(List.tabulate(101)(identity)) + cofNelToNel(unfoldedHundred) should ===(nelUnfoldedHundred) + } + + test("Cofree.tailForced") { + val spooky = new Spooky + val incrementor = + Cofree.unfold[Id, Int](spooky.counter) { _ => spooky.increment(); spooky.counter } + spooky.counter should ===(0) + incrementor.tailForced + spooky.counter should ===(1) + } + + test("Cofree.forceTail") { + val spooky = new Spooky + val incrementor = + Cofree.unfold[Id, Int](spooky.counter) { _ => spooky.increment(); spooky.counter } + spooky.counter should ===(0) + incrementor.forceTail + spooky.counter should ===(1) + } + + test("Cofree.forceAll") { + val spooky = new Spooky + val incrementor = + Cofree.unfold[Option, Int](spooky.counter)(i => + if (i == 5) { + None + } else { + spooky.increment() + Some(spooky.counter) + }) + spooky.counter should ===(0) + incrementor.forceAll + spooky.counter should ===(5) + } + + test("Cofree.mapBranchingRoot") { + val unfoldedHundred: CofreeNel[Int] = Cofree.unfold[Option, Int](0)(i => if (i == 100) None else Some(i + 1)) + val withNoneRoot = unfoldedHundred.mapBranchingRoot(new (Option ~> Option) { + override def apply[A](opt: Option[A]): Option[A] = None + }) + val nelUnfoldedOne: NonEmptyList[Int] = NonEmptyList.of(0) + cofNelToNel(withNoneRoot) should ===(nelUnfoldedOne) + } + + val unfoldedHundred: Cofree[Option, Int] = Cofree.unfold[Option, Int](0)(i => if (i == 100) None else Some(i + 1)) + test("Cofree.mapBranchingS/T") { + val toList = new (Option ~> List) { + override def apply[A](lst: Option[A]): List[A] = lst.fold[List[A]](Nil)(_ :: Nil) + } + val toNelS = unfoldedHundred.mapBranchingS(toList) + val toNelT = unfoldedHundred.mapBranchingT(toList) + val nelUnfoldedOne: NonEmptyList[Int] = NonEmptyList.fromListUnsafe(List.tabulate(101)(identity)) + cofRoseTreeToNel(toNelS) should ===(nelUnfoldedOne) + cofRoseTreeToNel(toNelT) should ===(nelUnfoldedOne) + } + + val nelUnfoldedHundred: NonEmptyList[Int] = NonEmptyList.fromListUnsafe(List.tabulate(101)(identity)) + + test("Cofree.cata") { + val cata = + Cofree.cata[Option, Int, NonEmptyList[Int]](unfoldedHundred)( + (i, lb) => Eval.now(NonEmptyList(i, lb.fold[List[Int]](Nil)(_.toList))) + ).value + cata should ===(nelUnfoldedHundred) + } + + test("Cofree.cataM") { + + type EvalOption[A] = OptionT[Eval, A] + + val folder: (Int, Option[NonEmptyList[Int]]) => EvalOption[NonEmptyList[Int]] = + (i, lb) => if (i > 100) OptionT.none else OptionT.some(NonEmptyList(i, lb.fold[List[Int]](Nil)(_.toList))) + val inclusion = new (Eval ~> EvalOption) { + override def apply[A](fa: Eval[A]): EvalOption[A] = OptionT.liftF(fa) + } + + val cataHundred = + Cofree.cataM[Option, EvalOption, Int, NonEmptyList[Int]](unfoldedHundred)(folder)(inclusion).value.value + val cataHundredOne = + Cofree.cataM[Option, EvalOption, Int, NonEmptyList[Int]]( + Cofree[Option, Int](101, Eval.now(Some(unfoldedHundred))) + )(folder)(inclusion).value.value + cataHundred should ===(Some(nelUnfoldedHundred)) + cataHundredOne should ===(None) + } + +} + +object CofreeTests extends CofreeTestsInstances + +sealed trait CofreeTestsInstances { + + type CofreeNel[A] = Cofree[Option, A] + type CofreeRoseTree[A] = Cofree[List, A] + + implicit def cofNelEq[A](implicit e: Eq[A]): Eq[CofreeNel[A]] = new Eq[CofreeNel[A]] { + override def eqv(a: CofreeNel[A], b: CofreeNel[A]): Boolean = { + def tr(a: CofreeNel[A], b: CofreeNel[A]): Boolean = + (a.tailForced, b.tailForced) match { + case (Some(at), Some(bt)) if e.eqv(a.head, b.head) => tr(at, bt) + case (None, None) if e.eqv(a.head, b.head) => true + case _ => false + } + tr(a, b) + } + } + + + implicit def CofreeOptionCogen[A: Cogen]: Cogen[CofreeNel[A]] = + implicitly[Cogen[List[A]]].contramap[CofreeNel[A]](cofNelToNel(_).toList) + + implicit def CofreeOptionArb[A: Arbitrary]: Arbitrary[CofreeNel[A]] = { + val arb = Arbitrary { + Gen.resize(20, Gen.nonEmptyListOf(implicitly[Arbitrary[A]].arbitrary)) + } + Arbitrary { + arb.arbitrary.map(l => (l.head, l.tail) match { + case (h, Nil) => nelToCofNel(NonEmptyList(h, Nil)) + case (h, t) => nelToCofNel(NonEmptyList(h, t)) + }) + } + } + + val nelToCofNel = new (NonEmptyList ~> CofreeNel) { + override def apply[A](fa: NonEmptyList[A]): CofreeNel[A] = + Cofree[Option, A](fa.head, Eval.later(fa.tail.toNel.map(apply))) + } + + val cofNelToNel = new (CofreeNel ~> NonEmptyList) { + override def apply[A](fa: CofreeNel[A]): NonEmptyList[A] = + NonEmptyList[A](fa.head, fa.tailForced.fold[List[A]](Nil)(apply(_).toList)) + } + + val cofRoseTreeToNel = new (CofreeRoseTree ~> NonEmptyList) { + override def apply[A](fa: CofreeRoseTree[A]): NonEmptyList[A] = + NonEmptyList[A](fa.head, fa.tailForced.flatMap(apply(_).toList)) + } + + +} diff --git a/tests/src/test/scala/cats/tests/EvalTests.scala b/tests/src/test/scala/cats/tests/EvalTests.scala index d7c41002531..8db0b06b2a6 100644 --- a/tests/src/test/scala/cats/tests/EvalTests.scala +++ b/tests/src/test/scala/cats/tests/EvalTests.scala @@ -8,17 +8,6 @@ import cats.laws.discipline.arbitrary._ import cats.kernel.laws.{GroupLaws, OrderLaws} class EvalTests extends CatsSuite { - - /** - * Class for spooky side-effects and action-at-a-distance. - * - * It is basically a mutable counter that can be used to measure how - * many times an otherwise pure function is being evaluted. - */ - class Spooky(var counter: Int = 0) { - def increment(): Unit = counter += 1 - } - /** * This method creates a Eval[A] instance (along with a * corresponding Spooky instance) from an initial `value` using the diff --git a/tests/src/test/scala/cats/tests/Spooky.scala b/tests/src/test/scala/cats/tests/Spooky.scala new file mode 100644 index 00000000000..c6008cf61a2 --- /dev/null +++ b/tests/src/test/scala/cats/tests/Spooky.scala @@ -0,0 +1,13 @@ +package cats +package tests + +/** + * Class for spooky side-effects and action-at-a-distance. + * + * It is basically a mutable counter that can be used to measure how + * many times an otherwise pure function is being evaluted. + */ +class Spooky(var counter: Int = 0) { + def increment(): Unit = counter += 1 +} +