From c9d9795bed69a4995c83b3928eebf5eab5105b6e Mon Sep 17 00:00:00 2001 From: Sarunas Valaskevicius Date: Fri, 27 Nov 2015 21:50:58 +0000 Subject: [PATCH 1/3] fix foldMap stack safety --- free/src/main/scala/cats/free/Free.scala | 15 ++++++++++++- free/src/test/scala/cats/free/FreeTests.scala | 22 +++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/free/src/main/scala/cats/free/Free.scala b/free/src/main/scala/cats/free/Free.scala index 7acd6be79a..294319c4e0 100644 --- a/free/src/main/scala/cats/free/Free.scala +++ b/free/src/main/scala/cats/free/Free.scala @@ -130,7 +130,20 @@ sealed abstract class Free[S[_], A] extends Product with Serializable { * Run to completion, mapping the suspension with the given transformation at each step and * accumulating into the monad `M`. */ - final def foldMap[M[_]](f: S ~> M)(implicit M: Monad[M]): M[A] = + @tailrec + final def foldMap[M[_]](f: S ~> M)(implicit M: Monad[M]): M[A] = { + step match { + case Free.Pure(a) => M.pure(a) + case Free.Suspend(s) => f(s) + case Free.Gosub(c, g) => c match { + case Free.Pure(a) => g(M.pure(a)).foldMap(f) + case Free.Suspend(s) => g(f(s)).foldMap(f) + case Free.Gosub(c1, g1) => g(M.flatMap(c1.foldMapRecursiveStep(f))(cc => g1(cc).foldMapRecursiveStep(f))).foldMap(f) + } + } + } + + private def foldMapRecursiveStep[M[_]](f: S ~> M)(implicit M: Monad[M]): M[A] = step match { case Pure(a) => M.pure(a) case Suspend(s) => f(s) diff --git a/free/src/test/scala/cats/free/FreeTests.scala b/free/src/test/scala/cats/free/FreeTests.scala index 57f26ef781..01770147e6 100644 --- a/free/src/test/scala/cats/free/FreeTests.scala +++ b/free/src/test/scala/cats/free/FreeTests.scala @@ -28,4 +28,26 @@ class FreeTests extends CatsSuite { x.mapSuspension(NaturalTransformation.id[List]) should === (x) } } + + test("foldMap is stack safe") { + trait FTestApi[A] + case class TB(i: Int) extends FTestApi[Int] + + type FTest[A] = Free[FTestApi, A] + + def tb(i: Int): FTest[Int] = Free.liftF(TB(i)) + + def a(i: Int): FTest[Int] = for { + j <- tb(i) + z <- if (j<10000) a(j) else Free.pure[FTestApi, Int](j) + } yield z + + def runner: FTestApi ~> Id = new (FTestApi ~> Id) { + def apply[A](fa: FTestApi[A]): Id[A] = fa match { + case TB(i) => i+1 + } + } + + assert(10000 == a(0).foldMap(runner)) + } } From 75e07fb914b211b956526ef52f1179ae61d0ae08 Mon Sep 17 00:00:00 2001 From: Sarunas Valaskevicius Date: Fri, 27 Nov 2015 22:59:42 +0000 Subject: [PATCH 2/3] simplify fix --- free/src/main/scala/cats/free/Free.scala | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/free/src/main/scala/cats/free/Free.scala b/free/src/main/scala/cats/free/Free.scala index 294319c4e0..40bcefcf25 100644 --- a/free/src/main/scala/cats/free/Free.scala +++ b/free/src/main/scala/cats/free/Free.scala @@ -132,23 +132,15 @@ sealed abstract class Free[S[_], A] extends Product with Serializable { */ @tailrec final def foldMap[M[_]](f: S ~> M)(implicit M: Monad[M]): M[A] = { - step match { - case Free.Pure(a) => M.pure(a) - case Free.Suspend(s) => f(s) - case Free.Gosub(c, g) => c match { - case Free.Pure(a) => g(M.pure(a)).foldMap(f) - case Free.Suspend(s) => g(f(s)).foldMap(f) - case Free.Gosub(c1, g1) => g(M.flatMap(c1.foldMapRecursiveStep(f))(cc => g1(cc).foldMapRecursiveStep(f))).foldMap(f) - } - } - } - - private def foldMapRecursiveStep[M[_]](f: S ~> M)(implicit M: Monad[M]): M[A] = step match { case Pure(a) => M.pure(a) case Suspend(s) => f(s) - case Gosub(c, g) => M.flatMap(c.foldMap(f))(cc => g(cc).foldMap(f)) + case Gosub(c, g) => c match { + case Suspend(s) => g(f(s)).foldMap(f) + case _ => throw new Error("Unexpected operation. The case should have been eliminated by `step`.") + } } + } /** * Compile your Free into another language by changing the suspension functor From 9ffaade82c6f7b10cfc263acecc842894b6ed198 Mon Sep 17 00:00:00 2001 From: Sarunas Valaskevicius Date: Sun, 29 Nov 2015 15:57:20 +0000 Subject: [PATCH 3/3] combine two functions to leverage compiler completencess checks in a case match --- free/src/main/scala/cats/free/Free.scala | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/free/src/main/scala/cats/free/Free.scala b/free/src/main/scala/cats/free/Free.scala index 40bcefcf25..9ed9f7c174 100644 --- a/free/src/main/scala/cats/free/Free.scala +++ b/free/src/main/scala/cats/free/Free.scala @@ -116,14 +116,6 @@ sealed abstract class Free[S[_], A] extends Product with Serializable { runM2(this) } - /** Takes one evaluation step in the Free monad, re-associating left-nested binds in the process. */ - @tailrec - final def step: Free[S, A] = this match { - case Gosub(Gosub(c, f), g) => c.flatMap(cc => f(cc).flatMap(g)).step - case Gosub(Pure(a), f) => f(a).step - case x => x - } - /** * Catamorphism for `Free`. * @@ -131,16 +123,16 @@ sealed abstract class Free[S[_], A] extends Product with Serializable { * accumulating into the monad `M`. */ @tailrec - final def foldMap[M[_]](f: S ~> M)(implicit M: Monad[M]): M[A] = { - step match { + final def foldMap[M[_]](f: S ~> M)(implicit M: Monad[M]): M[A] = + this match { case Pure(a) => M.pure(a) case Suspend(s) => f(s) case Gosub(c, g) => c match { case Suspend(s) => g(f(s)).foldMap(f) - case _ => throw new Error("Unexpected operation. The case should have been eliminated by `step`.") + case Gosub(cSub, h) => cSub.flatMap(cc => h(cc).flatMap(g)).foldMap(f) + case Pure(a) => g(a).foldMap(f) } } - } /** * Compile your Free into another language by changing the suspension functor