Skip to content

Commit

Permalink
fix foldMap stack safety
Browse files Browse the repository at this point in the history
  • Loading branch information
xuwei-k committed Dec 1, 2015
1 parent cf0282d commit c7ec575
Show file tree
Hide file tree
Showing 2 changed files with 26 additions and 3 deletions.
10 changes: 7 additions & 3 deletions core/src/main/scala/scalaz/Free.scala
Original file line number Diff line number Diff line change
Expand Up @@ -193,12 +193,16 @@ sealed abstract class Free[S[_], A] {
* Runs to completion, mapping the suspension with the given transformation at each step and
* accumulating into the monad `M`.
*/
@annotation.tailrec
final def foldMap[M[_]](f: S ~> M)(implicit M: Monad[M]): M[A] =
step match {
this match {
case Return(a) => M.pure(a)
case Suspend(s) => f(s)
// This is stack safe because `step` ensures right-associativity of Gosub
case Gosub(x, g) => M.bind(x foldMap f)(c => g(c) foldMap f)
case Gosub(x, g) => x match {
case Suspend(s) => g(f(s)).foldMap(f)
case Gosub(cSub, h) => cSub.flatMap(cc => h(cc).flatMap(g)).foldMap(f)
case Return(a) => g(a).foldMap(f)
}
}

import Id._
Expand Down
19 changes: 19 additions & 0 deletions tests/src/test/scala/scalaz/FreeTest.scala
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,25 @@ object FreeTest extends SpecLite {
checkAll(bindRec.laws[FreeOption])
}

"foldMap is stack safe" ! {
val n = 1000000
trait FTestApi[A]
case class TB(i: Int) extends FTestApi[Int]

def a(i: Int): Free[FTestApi, Int] = for {
j <- Free.liftF(TB(i))
z <- if (j < n) a(j) else Free.pure[FTestApi, Int](j)
} yield z

val runner = new (FTestApi ~> Id.Id) {
def apply[A](fa: FTestApi[A]) = fa match {
case TB(i) => i + 1
}
}

a(0).foldMap(runner) must_=== n
}

"List" should {
"not stack overflow with 50k binds" in {
val expected = Applicative[FreeList].point(())
Expand Down

0 comments on commit c7ec575

Please sign in to comment.