Skip to content

Commit

Permalink
Stack-safe Coyoneda
Browse files Browse the repository at this point in the history
  • Loading branch information
edmundnoble committed Apr 12, 2017
1 parent ec623e4 commit 29e23ea
Show file tree
Hide file tree
Showing 2 changed files with 35 additions and 9 deletions.
28 changes: 19 additions & 9 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
*/
sealed abstract class Coyoneda[F[_], A] extends Serializable { self =>

Expand All @@ -17,29 +18,32 @@ 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)
}

/**
* Simple function composition. Allows map fusion without touching
* 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)

}

Expand All @@ -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`.
*/
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 29e23ea

Please sign in to comment.