diff --git a/free/src/main/scala/cats/free/Coyoneda.scala b/free/src/main/scala/cats/free/Coyoneda.scala index a4e50fc0d39..976f53deb8e 100644 --- a/free/src/main/scala/cats/free/Coyoneda.scala +++ b/free/src/main/scala/cats/free/Coyoneda.scala @@ -8,6 +8,7 @@ import cats.arrow.FunctionK * This is isomorphic to `F` as long as `F` itself is a functor. * The homomorphism from `F[A]` to `Coyoneda[F,A]` exists even when * `F` is not a functor. + * Implemented using a List of functions */ sealed abstract class Coyoneda[F[_], A] extends Serializable { self => @@ -17,18 +18,21 @@ sealed abstract class Coyoneda[F[_], A] extends Serializable { self => /** The underlying value. */ val fi: F[Pivot] - /** The transformer function, to be lifted into `F` by `run`. */ - val k: Pivot => A + /** The transformer functions, to be lifted into `F` by `run`. */ + private[cats] val k: List[Any => Any] - import Coyoneda.{Aux, apply} + def execute(a: Pivot): A = + k.reverse.foldLeft[Any](a)((a, f) => f(a)).asInstanceOf[A] + + import Coyoneda.{Aux, unsafeApply} /** Converts to `F[A]` given that `F` is a functor */ - final def run(implicit F: Functor[F]): F[A] = F.map(fi)(k) + final def run(implicit F: Functor[F]): F[A] = F.map(fi)(execute) /** Converts to `Yoneda[F,A]` given that `F` is a functor */ final def toYoneda(implicit F: Functor[F]): Yoneda[F, A] = new Yoneda[F, A] { - def apply[B](f: A => B): F[B] = F.map(fi)(k andThen f) + def apply[B](f: A => B): F[B] = F.map(fi)(execute _ andThen f) } /** @@ -36,10 +40,10 @@ sealed abstract class Coyoneda[F[_], A] extends Serializable { self => * the underlying `F`. */ final def map[B](f: A => B): Aux[F, B, Pivot] = - apply(fi)(f compose k) + unsafeApply(fi)(f.asInstanceOf[Any => Any] :: k) final def transform[G[_]](f: FunctionK[F, G]): Aux[G, A, Pivot] = - apply(f(fi))(k) + unsafeApply(f(fi))(k) } @@ -53,14 +57,20 @@ object Coyoneda { /** `F[A]` converts to `Coyoneda[F,A]` for any `F` */ def lift[F[_], A](fa: F[A]): Coyoneda[F, A] = apply(fa)(identity[A]) - /** Like `lift(fa).map(_k)`. */ - def apply[F[_], A, B](fa: F[A])(k0: A => B): Aux[F, B, A] = + /** Creates a `Coyoneda[F, A]` for any `F`, taking an `F[A]` + * and a list of [[Functor.map]]ped functions to apply later + */ + def unsafeApply[F[_], A, B](fa: F[A])(k0: List[Any => Any]): Aux[F, B, A] = new Coyoneda[F, B] { type Pivot = A val k = k0 val fi = fa } + /** Like `lift(fa).map(_k)`. */ + def apply[F[_], A, B](fa: F[A])(k0: A => B): Aux[F, B, A] = + unsafeApply(fa)(k0.asInstanceOf[Any => Any] :: Nil) + /** * As the free functor, `Coyoneda[F, ?]` provides a functor for any `F`. */ diff --git a/free/src/test/scala/cats/free/CoyonedaTests.scala b/free/src/test/scala/cats/free/CoyonedaTests.scala index ed6bfe2ee55..379c3580bb2 100644 --- a/free/src/test/scala/cats/free/CoyonedaTests.scala +++ b/free/src/test/scala/cats/free/CoyonedaTests.scala @@ -31,4 +31,20 @@ class CoyonedaTests extends CatsSuite { val c = Coyoneda.lift(o) c.transform(nt).run should === (nt(o)) } + + test("map order") { + Coyoneda + .lift[Option, Int](Some(0)) + .map(_ + 1) + .map(_ * 3) + .run === Some(3) + } + + test("stack-safe map") { + def loop(n: Int, acc: Coyoneda[Option, Int]): Coyoneda[Option, Int] = + if (n <= 0) acc + else loop(n - 1, acc.map(_ + 1)) + + loop(20000, Coyoneda.lift[Option, Int](Some(1))).run + } }