Skip to content

Commit

Permalink
Stack-safe Coyoneda
Browse files Browse the repository at this point in the history
  • Loading branch information
edmundnoble committed Jun 11, 2017
1 parent 1931c93 commit 19c6561
Show file tree
Hide file tree
Showing 2 changed files with 33 additions and 7 deletions.
24 changes: 17 additions & 7 deletions free/src/main/scala/cats/free/Coyoneda.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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 for stack-safety.
*/
sealed abstract class Coyoneda[F[_], A] extends Serializable { self =>

Expand All @@ -17,10 +18,13 @@ 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 list of transformer functions, to be composed and lifted into `F` by `run`. */
private[cats] val ks: List[Any => Any]

import Coyoneda.{Aux, apply}
/** The list of transformer functions composed into a single function, to be lifted into `F` by `run`. */
final def k: Pivot => A = Function.chain(ks.reverse)(_).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)
Expand All @@ -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] :: ks)

final def transform[G[_]](f: FunctionK[F, G]): Aux[G, A, Pivot] =
apply(f(fi))(k)
unsafeApply(f(fi))(ks)

}

Expand All @@ -53,11 +57,17 @@ 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)`. */
/** Like `lift(fa).map(k0)`. */
def apply[F[_], A, B](fa: F[A])(k0: A => B): Aux[F, B, A] =
unsafeApply(fa)(k0.asInstanceOf[Any => Any] :: Nil)

/** Creates a `Coyoneda[F, A]` for any `F`, taking an `F[A]`
* and a list of [[Functor.map]]ped functions to apply later
*/
private[cats] def unsafeApply[F[_], A, B](fa: F[A])(ks0: List[Any => Any]): Aux[F, B, A] =
new Coyoneda[F, B] {
type Pivot = A
val k = k0
val ks = ks0
val fi = fa
}

Expand Down
16 changes: 16 additions & 0 deletions free/src/test/scala/cats/free/CoyonedaTests.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}

0 comments on commit 19c6561

Please sign in to comment.