Skip to content

Commit

Permalink
Remove unsafe Free methods, make Monad stack safety test JVM only
Browse files Browse the repository at this point in the history
  • Loading branch information
adelbertc committed Sep 13, 2016
1 parent 02c7e3e commit bb927c7
Show file tree
Hide file tree
Showing 5 changed files with 31 additions and 31 deletions.
5 changes: 4 additions & 1 deletion core/src/main/scala/cats/Eval.scala
Original file line number Diff line number Diff line change
Expand Up @@ -301,7 +301,10 @@ private[cats] trait EvalInstances extends EvalInstances0 {
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] = defaultTailRecM(a)(f)
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 def catsOrderForEval[A: Order]: Order[Eval[A]] =
Expand Down
12 changes: 0 additions & 12 deletions core/src/main/scala/cats/Monad.scala
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,4 @@ import simulacrum.typeclass
@typeclass trait Monad[F[_]] extends FlatMap[F] with Applicative[F] {
override def map[A, B](fa: F[A])(f: A => B): F[B] =
flatMap(fa)(a => pure(f(a)))

/**
* This is not stack safe if the monad is not trampolined, but it
* is always lawful. It it better if you can find a stack safe way
* to write this method (all cats types have a stack safe version
* of this). When this method is safe you can find an `implicit r: RecursiveTailRecM`.
*/
protected def defaultTailRecM[A, B](a: A)(fn: A => F[Either[A, B]]): F[B] =
flatMap(fn(a)) {
case Right(b) => pure(b)
case Left(nextA) => defaultTailRecM(nextA)(fn)
}
}
29 changes: 23 additions & 6 deletions docs/src/main/tut/monad.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,13 +60,30 @@ implicit def optionMonad(implicit app: Applicative[Option]) =
follows this tradition by providing implementations of `flatten` and `map`
derived from `flatMap` and `pure`.

In addition to requiring `flatMap` and pure`, Cats has chosen to require
`tailRecM` which encodes stack safe monadic recursion, as described in
[Stack Safety for Free](http://functorial.com/stack-safety-for-free/index.pdf) by
Phil Freeman. Because monadic recursion is so common in functional programming but
is not stack safe on the JVM, Cats has chosen to require this method of all monad implementations
as opposed to just a subset. All functions requiring monadic recursion in Cats is done via
`tailRecM`.

An example `Monad` implementation for `Option` is shown below. Note the tail recursive
and therefore stack safe implementation of `tailRecM`.

```tut:silent
implicit val listMonad = new Monad[List] {
def flatMap[A, B](fa: List[A])(f: A => List[B]): List[B] = fa.flatMap(f)
def pure[A](a: A): List[A] = List(a)
def tailRecM[A, B](a: A)(f: A => List[Either[A, B]]): List[B] =
defaultTailRecM(a)(f)
}
import scala.annotation.tailrec
implicit val optionMonad = new Monad[Option] {
def flatMap[A, B](fa: Option[A])(f: A => Option[B]): Option[B] = fa.flatMap(f)
def pure[A](a: A): Option[A] = Some(a)
@tailrec
def tailRecM[A, B](a: A)(f: A => Option[Either[A, B]]): Option[B] = f(a) match {
case None => None
case Some(Left(nextA)) => tailRecM(nextA)(f) // continue the recursion
case Some(Right(b)) => Some(b) // recursion done
}
```

Part of the reason for this is that name `flatMap` has special significance in
Expand Down
10 changes: 1 addition & 9 deletions free/src/main/scala/cats/free/Free.scala
Original file line number Diff line number Diff line change
Expand Up @@ -127,14 +127,6 @@ sealed abstract class Free[S[_], A] extends Product with Serializable {
case FlatMapped(c, g) => M.map(c.foldMap(f))(cc => Left(g(cc)))
})

/**
* Same as foldMap but without a guarantee of stack safety. If the recursion is shallow
* enough, this will work
*/
final def foldMapUnsafe[M[_]](f: FunctionK[S, M])(implicit M: Monad[M]): M[A] =
foldMap[M](f)


/**
* Compile your free monad into another language by changing the
* suspension functor using the given natural transformation `f`.
Expand All @@ -143,7 +135,7 @@ sealed abstract class Free[S[_], A] extends Product with Serializable {
* effects will be applied by `compile`.
*/
final def compile[T[_]](f: FunctionK[S, T]): Free[T, A] =
foldMapUnsafe[Free[T, ?]] { // this is safe because Free is stack safe
foldMap[Free[T, ?]] { // this is safe because Free is stack safe
new FunctionK[S, Free[T, ?]] {
def apply[B](fa: S[B]): Free[T, B] = Suspend(f(fa))
}
Expand Down
6 changes: 3 additions & 3 deletions laws/src/main/scala/cats/laws/discipline/MonadTests.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package cats
package laws
package discipline

import catalysts.Platform
import cats.laws.discipline.CartesianTests.Isomorphisms
import org.scalacheck.Arbitrary
import org.scalacheck.Prop
Expand Down Expand Up @@ -30,9 +31,8 @@ trait MonadTests[F[_]] extends ApplicativeTests[F] with FlatMapTests[F] {
def props: Seq[(String, Prop)] = Seq(
"monad left identity" -> forAll(laws.monadLeftIdentity[A, B] _),
"monad right identity" -> forAll(laws.monadRightIdentity[A] _),
"map flatMap coherence" -> forAll(laws.mapFlatMapCoherence[A, B] _),
"tailRecM stack safety" -> laws.tailRecMStackSafety
)
"map flatMap coherence" -> forAll(laws.mapFlatMapCoherence[A, B] _)
) ++ (if (Platform.isJvm) Seq[(String, Prop)]("tailRecM stack safety" -> laws.tailRecMStackSafety) else Seq.empty)
}
}
}
Expand Down

0 comments on commit bb927c7

Please sign in to comment.