diff --git a/core/src/main/scala/cats/Foldable.scala b/core/src/main/scala/cats/Foldable.scala index 676c80f5ac..aa83ebf49e 100644 --- a/core/src/main/scala/cats/Foldable.scala +++ b/core/src/main/scala/cats/Foldable.scala @@ -76,6 +76,12 @@ import simulacrum.typeclass def foldMap[A, B](fa: F[A])(f: A => B)(implicit B: Monoid[B]): B = foldLeft(fa, B.empty)((b, a) => B.combine(b, f(a))) + /** + * Left associative monadic folding on `F`. + */ + def foldM[G[_], A, B](fa: F[A], z: B)(f: (B, A) => G[B])(implicit G: Monad[G]): G[B] = + foldLeft(fa, G.pure(z))((gb, a) => G.flatMap(gb)(f(_, a))) + /** * Traverse `F[A]` using `Applicative[G]`. * diff --git a/tests/src/test/scala/cats/tests/FoldableTests.scala b/tests/src/test/scala/cats/tests/FoldableTests.scala index 8d4efefd24..fda51a443a 100644 --- a/tests/src/test/scala/cats/tests/FoldableTests.scala +++ b/tests/src/test/scala/cats/tests/FoldableTests.scala @@ -63,6 +63,12 @@ class FoldableTestsAdditional extends CatsSuite { // more basic checks val names = List("Aaron", "Betty", "Calvin", "Deirdra") F.foldMap(names)(_.length) should === (names.map(_.length).sum) + val sumM = F.foldM(names, "") { (acc, x) => (Some(acc + x): Option[String]) } + assert(sumM == Some("AaronBettyCalvinDeirdra")) + val notCalvin = F.foldM(names, "") { (acc, x) => + if (x == "Calvin") (None: Option[String]) + else (Some(acc + x): Option[String]) } + assert(notCalvin == None) // test trampolining val large = (1 to 10000).toList @@ -73,6 +79,16 @@ class FoldableTestsAdditional extends CatsSuite { larger.value should === (large.map(_ + 1)) } + test("Foldable[List].foldM stack safety") { + def nonzero(acc: Long, x: Long): Option[Long] = + if (x == 0) None else Some(acc + x) + + val n = 100000L + val expected = n*(n+1)/2 + val actual = Foldable[List].foldM((1L to n).toList, 0L)(nonzero) + assert(actual.get == expected) + } + test("Foldable[Stream]") { val F = Foldable[Stream]