Skip to content
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

Added StackSafeMonad mixin #1736

Merged
merged 1 commit into from
Jun 20, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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