diff --git a/core/src/main/scala/cats/Foldable.scala b/core/src/main/scala/cats/Foldable.scala index 8549d70e7b..838cad03fa 100644 --- a/core/src/main/scala/cats/Foldable.scala +++ b/core/src/main/scala/cats/Foldable.scala @@ -326,6 +326,78 @@ import simulacrum.typeclass if (p(a)) lb else Eval.False }.value + /** + * Check whether at least one element satisfies the effectful predicate. + * + * If there are no elements, the result is `false`. `existsM` short-circuits, + * i.e. once a `true` result is encountered, no further effects are produced. + * + * For example: + * + * {{{ + * scala> import cats.implicits._ + * scala> val F = Foldable[List] + * scala> F.existsM(List(1,2,3,4))(n => Option(n <= 4)) + * res0: Option[Boolean] = Some(true) + * + * scala> F.existsM(List(1,2,3,4))(n => Option(n > 4)) + * res1: Option[Boolean] = Some(false) + * + * scala> F.existsM(List(1,2,3,4))(n => if (n <= 2) Option(true) else Option(false)) + * res2: Option[Boolean] = Some(true) + * + * scala> F.existsM(List(1,2,3,4))(n => if (n <= 2) Option(true) else None) + * res3: Option[Boolean] = Some(true) + * + * scala> F.existsM(List(1,2,3,4))(n => if (n <= 2) None else Option(true)) + * res4: Option[Boolean] = None + * }}} + */ + def existsM[G[_], A](fa: F[A])(p: A => G[Boolean])(implicit G: Monad[G]): G[Boolean] = { + G.tailRecM(Foldable.Source.fromFoldable(fa)(self)) { + src => src.uncons match { + case Some((a, src)) => G.map(p(a))(bb => if (bb) Right(true) else Left(src.value)) + case None => G.pure(Right(false)) + } + } + } + + /** + * Check whether all elements satisfy the effectful predicate. + * + * If there are no elements, the result is `true`. `forallM` short-circuits, + * i.e. once a `false` result is encountered, no further effects are produced. + * + * For example: + * + * {{{ + * scala> import cats.implicits._ + * scala> val F = Foldable[List] + * scala> F.forallM(List(1,2,3,4))(n => Option(n <= 4)) + * res0: Option[Boolean] = Some(true) + * + * scala> F.forallM(List(1,2,3,4))(n => Option(n <= 1)) + * res1: Option[Boolean] = Some(false) + * + * scala> F.forallM(List(1,2,3,4))(n => if (n <= 2) Option(true) else Option(false)) + * res2: Option[Boolean] = Some(false) + * + * scala> F.forallM(List(1,2,3,4))(n => if (n <= 2) Option(false) else None) + * res3: Option[Boolean] = Some(false) + * + * scala> F.forallM(List(1,2,3,4))(n => if (n <= 2) None else Option(false)) + * res4: Option[Boolean] = None + * }}} + */ + def forallM[G[_], A](fa: F[A])(p: A => G[Boolean])(implicit G: Monad[G]): G[Boolean] = { + G.tailRecM(Foldable.Source.fromFoldable(fa)(self)) { + src => src.uncons match { + case Some((a, src)) => G.map(p(a))(bb => if (!bb) Right(false) else Left(src.value)) + case None => G.pure(Right(true)) + } + } + } + /** * Convert F[A] to a List[A]. */ diff --git a/tests/src/test/scala/cats/tests/FoldableTests.scala b/tests/src/test/scala/cats/tests/FoldableTests.scala index d4cf19e688..c5d84cdb22 100644 --- a/tests/src/test/scala/cats/tests/FoldableTests.scala +++ b/tests/src/test/scala/cats/tests/FoldableTests.scala @@ -35,11 +35,13 @@ abstract class FoldableCheck[F[_]: Foldable](name: String)(implicit ArbFInt: Arb } } - test(s"Foldable[$name].find/exists/forall/filter_/dropWhile_") { + test(s"Foldable[$name].find/exists/forall/existsM/forallM/filter_/dropWhile_") { forAll { (fa: F[Int], n: Int) => fa.find(_ > n) should === (iterator(fa).find(_ > n)) fa.exists(_ > n) should === (iterator(fa).exists(_ > n)) fa.forall(_ > n) should === (iterator(fa).forall(_ > n)) + fa.existsM(k => Option(k > n)) should === (Option(iterator(fa).exists(_ > n))) + fa.forallM(k => Option(k > n)) should === (Option(iterator(fa).forall(_ > n))) fa.filter_(_ > n) should === (iterator(fa).filter(_ > n).toList) fa.dropWhile_(_ > n) should === (iterator(fa).dropWhile(_ > n).toList) fa.takeWhile_(_ > n) should === (iterator(fa).takeWhile(_ > n).toList) @@ -239,6 +241,13 @@ class FoldableTestsAdditional extends CatsSuite { assert(concatUntil("Zero" #:: "One" #:: "STOP" #:: boom, "STOP") == Left("ZeroOne")) } + test(".existsM/.forallM short-circuiting") { + implicit val F = foldableStreamWithDefaultImpl + def boom: Stream[Boolean] = sys.error("boom") + assert(F.existsM[Id, Boolean](true #:: boom)(identity) == true) + assert(F.forallM[Id, Boolean](false #:: boom)(identity) == false) + } + test("Foldable[List] doesn't break substitution") { val result = List.range(0,10).foldM(List.empty[Int])((accum, elt) => Eval.always(elt :: accum))