From 0d025af78e4ab3f1daba579ea140f46e1981c90b Mon Sep 17 00:00:00 2001 From: Andy Scott Date: Sun, 15 Jul 2018 22:28:42 -0700 Subject: [PATCH 1/4] Add Cofree.ana (Cofree.unfold but with fused map) --- free/src/main/scala/cats/free/Cofree.scala | 7 +++++-- free/src/test/scala/cats/free/CofreeSuite.scala | 6 ++++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/free/src/main/scala/cats/free/Cofree.scala b/free/src/main/scala/cats/free/Cofree.scala index a453143ce8..7c8fa93669 100644 --- a/free/src/main/scala/cats/free/Cofree.scala +++ b/free/src/main/scala/cats/free/Cofree.scala @@ -61,7 +61,11 @@ 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)))) + ana(a)(f, identity) + + /** Cofree anamorphism with a fused map, lazily evaluated. */ + def ana[F[_], A, B](a: A)(coalg: A => F[A], f: A => B)(implicit F: Functor[F]): Cofree[F, B] = + Cofree[F, B](f(a), Eval.later(F.map(coalg(a))(ana(_)(coalg, f)))) /** * A stack-safe algebraic recursive fold out of the cofree comonad. @@ -148,4 +152,3 @@ private trait CofreeTraverse[F[_]] extends Traverse[Cofree[F, ?]] with CofreeRed 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/CofreeSuite.scala b/free/src/test/scala/cats/free/CofreeSuite.scala index eb26c1df1f..bede609912 100644 --- a/free/src/test/scala/cats/free/CofreeSuite.scala +++ b/free/src/test/scala/cats/free/CofreeSuite.scala @@ -32,6 +32,12 @@ class CofreeSuite extends CatsSuite { cofNelToNel(unfoldedHundred) should ===(nelUnfoldedHundred) } + test("Cofree.ana") { + val unfoldedHundred: CofreeNel[String] = Cofree.ana[Option, Int, String](0)(i => if (i == 100) None else Some(i + 1), _.toString) + val nelUnfoldedHundred: NonEmptyList[String] = NonEmptyList.fromListUnsafe(List.tabulate(101)(_.toString)) + cofNelToNel(unfoldedHundred) should ===(nelUnfoldedHundred) + } + test("Cofree.tailForced") { val spooky = new Spooky val incrementor = From 6c1a555a67392d7672d31a31d9a2cdb9d6eb2223 Mon Sep 17 00:00:00 2001 From: Andy Scott Date: Sun, 15 Jul 2018 22:45:38 -0700 Subject: [PATCH 2/4] Adjust Cofree.ana test to better demonstate usage (slightly) --- free/src/test/scala/cats/free/CofreeSuite.scala | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/free/src/test/scala/cats/free/CofreeSuite.scala b/free/src/test/scala/cats/free/CofreeSuite.scala index bede609912..fc2f27512b 100644 --- a/free/src/test/scala/cats/free/CofreeSuite.scala +++ b/free/src/test/scala/cats/free/CofreeSuite.scala @@ -33,9 +33,9 @@ class CofreeSuite extends CatsSuite { } test("Cofree.ana") { - val unfoldedHundred: CofreeNel[String] = Cofree.ana[Option, Int, String](0)(i => if (i == 100) None else Some(i + 1), _.toString) - val nelUnfoldedHundred: NonEmptyList[String] = NonEmptyList.fromListUnsafe(List.tabulate(101)(_.toString)) - cofNelToNel(unfoldedHundred) should ===(nelUnfoldedHundred) + val anaHundred: CofreeNel[Int] = Cofree.ana[Option, List[Int], Int](List.tabulate(101)(identity))(l => if (l.tail.isEmpty) None else Some(l.tail), _.head) + val nelUnfoldedHundred: NonEmptyList[Int] = NonEmptyList.fromListUnsafe(List.tabulate(101)(identity)) + cofNelToNel(anaHundred) should ===(nelUnfoldedHundred) } test("Cofree.tailForced") { From 08bb591593b437f6c92e73c3efcd5eb82094d4ea Mon Sep 17 00:00:00 2001 From: Andy Scott Date: Sun, 15 Jul 2018 23:47:12 -0700 Subject: [PATCH 3/4] Generalize ana for Eval; reimplement a few methods in terms of anaE --- free/src/main/scala/cats/free/Cofree.scala | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/free/src/main/scala/cats/free/Cofree.scala b/free/src/main/scala/cats/free/Cofree.scala index 7c8fa93669..c1408a20e8 100644 --- a/free/src/main/scala/cats/free/Cofree.scala +++ b/free/src/main/scala/cats/free/Cofree.scala @@ -34,15 +34,15 @@ final case class Cofree[S[_], A](head: A, tail: Eval[S[Cofree[S, A]]]) { /** 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, tail.map(v => T.map(nat(v))(_.mapBranchingT(nat)))) + Cofree.anaE(this)(_.tail.map(nat(_)), _.head) /** 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), tail.map(S.map(_)(_.coflatMap(f)))) + Cofree.anaE(this)(_.tail, 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, tail.map(S.map(_)(_.coflatten))) + Cofree.anaE(this)(_.tail, identity) /** Alias for head. */ def extract: A = head @@ -53,7 +53,7 @@ final case class Cofree[S[_], A](head: A, tail: Eval[S[Cofree[S, A]]]) { /** Evaluate the entire Cofree tree. */ def forceAll(implicit S: Functor[S]): Cofree[S, A] = - Cofree[S, A](head, Eval.now(tail.map(S.map(_)(_.forceAll)).value)) + Cofree.anaE(this)(sa => Eval.now(sa.tail.value), _.head) } @@ -65,7 +65,16 @@ object Cofree extends CofreeInstances { /** Cofree anamorphism with a fused map, lazily evaluated. */ def ana[F[_], A, B](a: A)(coalg: A => F[A], f: A => B)(implicit F: Functor[F]): Cofree[F, B] = - Cofree[F, B](f(a), Eval.later(F.map(coalg(a))(ana(_)(coalg, f)))) + anaE(a)(a => Eval.later(coalg(a)), f) + + /** Cofree anamorphism with a fused map. */ + def anaE[F[_], A, B](a: A)(coalg: A => Eval[F[A]], f: A => B)(implicit F: Functor[F]): Cofree[F, B] = + Cofree[F, B](f(a), mapSemilazy(coalg(a))(fa => F.map(fa)(anaE(_)(coalg, f)))) + + private def mapSemilazy[A, B](fa: Eval[A])(f: A => B): Eval[B] = fa match { + case Now(a) => Now(f(a)) + case other => other.map(f) + } /** * A stack-safe algebraic recursive fold out of the cofree comonad. From a5454385fc4db051162d383ff79b55c7b1115f45 Mon Sep 17 00:00:00 2001 From: Andy Scott Date: Mon, 16 Jul 2018 10:11:10 -0700 Subject: [PATCH 4/4] Rename anaE to anaEval --- free/src/main/scala/cats/free/Cofree.scala | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/free/src/main/scala/cats/free/Cofree.scala b/free/src/main/scala/cats/free/Cofree.scala index c1408a20e8..344d68dd74 100644 --- a/free/src/main/scala/cats/free/Cofree.scala +++ b/free/src/main/scala/cats/free/Cofree.scala @@ -34,15 +34,15 @@ final case class Cofree[S[_], A](head: A, tail: Eval[S[Cofree[S, A]]]) { /** 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.anaE(this)(_.tail.map(nat(_)), _.head) + Cofree.anaEval(this)(_.tail.map(nat(_)), _.head) /** Map `f` over each subtree of the computation. */ def coflatMap[B](f: Cofree[S, A] => B)(implicit S: Functor[S]): Cofree[S, B] = - Cofree.anaE(this)(_.tail, f) + Cofree.anaEval(this)(_.tail, 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.anaE(this)(_.tail, identity) + Cofree.anaEval(this)(_.tail, identity) /** Alias for head. */ def extract: A = head @@ -53,7 +53,7 @@ final case class Cofree[S[_], A](head: A, tail: Eval[S[Cofree[S, A]]]) { /** Evaluate the entire Cofree tree. */ def forceAll(implicit S: Functor[S]): Cofree[S, A] = - Cofree.anaE(this)(sa => Eval.now(sa.tail.value), _.head) + Cofree.anaEval(this)(sa => Eval.now(sa.tail.value), _.head) } @@ -65,11 +65,11 @@ object Cofree extends CofreeInstances { /** Cofree anamorphism with a fused map, lazily evaluated. */ def ana[F[_], A, B](a: A)(coalg: A => F[A], f: A => B)(implicit F: Functor[F]): Cofree[F, B] = - anaE(a)(a => Eval.later(coalg(a)), f) + anaEval(a)(a => Eval.later(coalg(a)), f) /** Cofree anamorphism with a fused map. */ - def anaE[F[_], A, B](a: A)(coalg: A => Eval[F[A]], f: A => B)(implicit F: Functor[F]): Cofree[F, B] = - Cofree[F, B](f(a), mapSemilazy(coalg(a))(fa => F.map(fa)(anaE(_)(coalg, f)))) + def anaEval[F[_], A, B](a: A)(coalg: A => Eval[F[A]], f: A => B)(implicit F: Functor[F]): Cofree[F, B] = + Cofree[F, B](f(a), mapSemilazy(coalg(a))(fa => F.map(fa)(anaEval(_)(coalg, f)))) private def mapSemilazy[A, B](fa: Eval[A])(f: A => B): Eval[B] = fa match { case Now(a) => Now(f(a))