Skip to content

Commit

Permalink
Added StackSafeMonad mixin
Browse files Browse the repository at this point in the history
  • Loading branch information
djspiewak committed Jun 20, 2017
1 parent ca7c8c0 commit 312ce51
Show file tree
Hide file tree
Showing 5 changed files with 24 additions and 29 deletions.
6 changes: 1 addition & 5 deletions core/src/main/scala/cats/Eval.scala
Original file line number Diff line number Diff line change
Expand Up @@ -329,16 +329,12 @@ object Eval extends EvalInstances {
private[cats] trait EvalInstances extends EvalInstances0 {

implicit val catsBimonadForEval: Bimonad[Eval] =
new Bimonad[Eval] {
new Bimonad[Eval] with StackSafeMonad[Eval] {
override def map[A, B](fa: Eval[A])(f: A => B): Eval[B] = fa.map(f)
def pure[A](a: A): Eval[A] = Now(a)
def flatMap[A, B](fa: Eval[A])(f: A => Eval[B]): Eval[B] = fa.flatMap(f)
def extract[A](la: Eval[A]): A = la.value
def coflatMap[A, B](fa: Eval[A])(f: Eval[A] => B): Eval[B] = Later(f(fa))
def tailRecM[A, B](a: A)(f: A => Eval[Either[A, B]]): Eval[B] = f(a).flatMap { // OK because Eval is trampolined
case Left(nextA) => tailRecM(nextA)(f)
case Right(b) => pure(b)
}
}

implicit val catsReducibleForEval: Reducible[Eval] =
Expand Down
19 changes: 19 additions & 0 deletions core/src/main/scala/cats/StackSafeMonad.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package cats

import scala.util.{Either, Left, Right}

/**
* A mix-in for inheriting tailRecM on monads which define a stack-safe flatMap. This is
* ''not'' an appropriate trait to use unless you are 100% certain your monad is stack-safe
* by definition! If your monad is not stack-safe, then the tailRecM implementation you
* will inherit will not be sound, and will result in unexpected stack overflows. This
* trait is only provided because a large number of monads ''do'' define a stack-safe
* flatMap, and so this particular implementation was being repeated over and over again.
*/
trait StackSafeMonad[F[_]] extends Monad[F] {

override def tailRecM[A, B](a: A)(f: A => F[Either[A, B]]): F[B] = flatMap(f(a)) {
case Left(a) => tailRecM(a)(f)
case Right(b) => pure(b)
}
}
12 changes: 1 addition & 11 deletions core/src/main/scala/cats/instances/future.scala
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,11 @@ import scala.concurrent.{ExecutionContext, Future}
trait FutureInstances extends FutureInstances1 {

implicit def catsStdInstancesForFuture(implicit ec: ExecutionContext): MonadError[Future, Throwable] with CoflatMap[Future] with Monad[Future] =
new FutureCoflatMap with MonadError[Future, Throwable] with Monad[Future] {
new FutureCoflatMap with MonadError[Future, Throwable] with Monad[Future] with StackSafeMonad[Future] {
def pure[A](x: A): Future[A] = Future.successful(x)

def flatMap[A, B](fa: Future[A])(f: A => Future[B]): Future[B] = fa.flatMap(f)

/**
* Note that while this implementation will not compile with `@tailrec`,
* it is in fact stack-safe.
*/
final def tailRecM[B, C](b: B)(f: B => Future[Either[B, C]]): Future[C] =
f(b).flatMap {
case Left(b1) => tailRecM(b1)(f)
case Right(c) => Future.successful(c)
}

def handleErrorWith[A](fea: Future[A])(f: Throwable => Future[A]): Future[A] = fea.recoverWith { case t => f(t) }

def raiseError[A](e: Throwable): Future[A] = Future.failed(e)
Expand Down
7 changes: 1 addition & 6 deletions free/src/main/scala/cats/free/Free.scala
Original file line number Diff line number Diff line change
Expand Up @@ -244,15 +244,10 @@ object Free {
* `Free[S, ?]` has a monad for any type constructor `S[_]`.
*/
implicit def catsFreeMonadForFree[S[_]]: Monad[Free[S, ?]] =
new Monad[Free[S, ?]] {
new Monad[Free[S, ?]] with StackSafeMonad[Free[S, ?]] {
def pure[A](a: A): Free[S, A] = Free.pure(a)
override def map[A, B](fa: Free[S, A])(f: A => B): Free[S, B] = fa.map(f)
def flatMap[A, B](a: Free[S, A])(f: A => Free[S, B]): Free[S, B] = a.flatMap(f)
def tailRecM[A, B](a: A)(f: A => Free[S, Either[A, B]]): Free[S, B] =
f(a).flatMap {
case Left(a1) => tailRecM(a1)(f) // recursion OK here, since Free is lazy
case Right(b) => pure(b)
}
}

/**
Expand Down
9 changes: 2 additions & 7 deletions tests/src/test/scala/cats/tests/RegressionTests.scala
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,10 @@ class RegressionTests extends CatsSuite {
}

object State {
implicit def instance[S]: Monad[State[S, ?]] = new Monad[State[S, ?]] {
implicit def instance[S]: Monad[State[S, ?]] = new Monad[State[S, ?]] with StackSafeMonad[State[S, ?]] { // lies!
def pure[A](a: A): State[S, A] = State(s => (a, s))
def flatMap[A, B](sa: State[S, A])(f: A => State[S, B]): State[S, B] = sa.flatMap(f)
def tailRecM[A, B](a: A)(fn: A => State[S, Either[A, B]]): State[S, B] =
flatMap(fn(a)) {
case Left(a) => tailRecM(a)(fn)
case Right(b) => pure(b)
}
}
}
}

// used to test side-effects
Expand Down

0 comments on commit 312ce51

Please sign in to comment.