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..74f2863bd93 --- /dev/null +++ b/free/src/main/scala/cats/free/Cofree.scala @@ -0,0 +1,135 @@ +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)))) + +} + +sealed abstract class CofreeInstances2 { + /** low priority `Reducible` instance */ + implicit def catsReducibleForCofree[F[_] : Foldable]: Reducible[Cofree[F, ?]] = + new CofreeReducible[F] { + def F = implicitly + } +} + +sealed abstract class CofreeInstances1 extends CofreeInstances2 { + /** low priority `Traverse` instance */ + implicit def catsTraverseForCofree[F[_] : Traverse]: Traverse[Cofree[F, ?]] = + new CofreeTraverse[F] { + def F = implicitly + } +} + +sealed 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..376df15e4f6 --- /dev/null +++ b/free/src/test/scala/cats/free/CofreeTests.scala @@ -0,0 +1,102 @@ +package cats +package free + +import cats.data.NonEmptyList +import cats.tests.CatsSuite +import cats.laws.discipline.{CartesianTests, ComonadTests, ReducibleTests, SerializableTests, TraverseTests} +import cats.syntax.list._ +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](1)(i => if (i == 100) None else Some(i + 1)) + val nelUnfoldedHundred: NonEmptyList[Int] = NonEmptyList.fromListUnsafe(List.tabulate(99)(identity)) + cofNelToNel(unfoldedHundred) === nelUnfoldedHundred + } + + test("Cofree.mapBranchingRoot") { + val unfoldedHundred: CofreeNel[Int] = Cofree.unfold[Option, Int](1)(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(1) + cofNelToNel(withNoneRoot) === nelUnfoldedOne + } + + test("Cofree.mapBranchingS/T") { + val unfoldedHundred: Cofree[List, Int] = Cofree.unfold[List, Int](1)(i => if (i == 100) Nil else List(i + 1)) + val toNelS = unfoldedHundred.mapBranchingS(new (List ~> Option) { + override def apply[A](lst: List[A]): Option[A] = lst.headOption + }) + val toNelT = unfoldedHundred.mapBranchingT(new (List ~> Option) { + override def apply[A](lst: List[A]): Option[A] = lst.headOption + }) + val nelUnfoldedOne: NonEmptyList[Int] = NonEmptyList.fromListUnsafe(List.tabulate(99)(identity)) + cofNelToNel(toNelS) === nelUnfoldedOne && cofNelToNel(toNelT) === nelUnfoldedOne + } + +} + +object CofreeTests extends CofreeTestsInstances + +sealed trait CofreeTestsInstances { + + type CofreeNel[A] = Cofree[Option, 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)) + } + +}