-
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Cofree comonad #1446
Cofree comonad #1446
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,151 @@ | ||
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, tail: Eval[S[Cofree[S, A]]]) { | ||
|
||
/** Evaluates and returns the tail of the computation. */ | ||
def tailForced: S[Cofree[S, A]] = tail.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), tail.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, tail.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, tail.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, tail.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), tail.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, tail.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(tail.value)) | ||
|
||
/** 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)) | ||
|
||
} | ||
|
||
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)))) | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It would be nice to have the monadic ana as well. def unfoldM[M[_]: Monad, F[_]: Traverse, A](a: A)(f: A => M[F[A]]): M[Cofree[F, A]] There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sure :) |
||
/** | ||
* 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] = | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I am unhappy that There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let the record show that I am also made unhappy by this. |
||
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 { | ||
implicit def catsReducibleForCofree[F[_] : Foldable]: Reducible[Cofree[F, ?]] = | ||
new CofreeReducible[F] { | ||
def F = implicitly | ||
} | ||
} | ||
|
||
sealed private[free] abstract class CofreeInstances1 extends CofreeInstances2 { | ||
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.tail.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] = { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why did you do this vs something like: f(ta.head, fa.tailEval.map { reduceRightTo(_)(z)(f) }) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Try doing that yourself. Doesn't typecheck as far as I can see; you're expecting |
||
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))) | ||
|
||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. woah what's this There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
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)) | ||
} | ||
|
||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Was this derived/based on Scalaz's Cofree or from scratch? If former could you add attribution to Scalaz, similar to what we have here ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Totally derived from the one at Scalaz. I did not know there was an example of such an attribution somewhere in the code to crib from, I'll add it in.