diff --git a/core/src/main/scala/cats/syntax/foldable.scala b/core/src/main/scala/cats/syntax/foldable.scala index f32d8bb5c4..86c50c62ce 100644 --- a/core/src/main/scala/cats/syntax/foldable.scala +++ b/core/src/main/scala/cats/syntax/foldable.scala @@ -92,4 +92,32 @@ final class FoldableOps[F[_], A](val fa: F[A]) extends AnyVal { b.toString.dropRight(delim.length) } + suffix } + + /** + * Monadic version of `collectFirstSome`. + * {{{ + * scala> import cats.implicits._ + * scala> def parseInt(s: String): Either[String, Int] = Either.catchOnly[NumberFormatException](s.toInt).leftMap(_.getMessage) + * scala> val keys1 = List("1", "2", "4", "5") + * scala> val map1 = Map(4 -> "Four", 5 -> "Five") + * scala> keys1.collectFirstSomeM(parseInt(_) map map1.get) + * res1: scala.util.Either[String,Option[String]] = Right(Some(Four)) + * scala> val map2 = Map(6 -> "Six", 7 -> "Seven") + * scala> keys1.collectFirstSomeM(parseInt(_) map map2.get) + * res2: scala.util.Either[String,Option[String]] = Right(None) + * scala> val keys2 = List("1", "x", "4", "5") + * scala> keys2.collectFirstSomeM(parseInt(_) map map1.get) + * res3: scala.util.Either[String,Option[String]] = Left(For input string: "x") + * scala> val keys3 = List("1", "2", "4", "x") + * scala> keys3.collectFirstSomeM(parseInt(_) map map1.get) + * res4: scala.util.Either[String,Option[String]] = Right(Some(Four)) + * }}} + */ + def collectFirstSomeM[G[_], B](f: A => G[Option[B]])(implicit F: Foldable[F], G: Monad[G]): G[Option[B]] = + F.foldRight(fa, Eval.now(G.pure(Option.empty[B])))((a, lb) => + Eval.now(G.flatMap(f(a)) { + case None => lb.value + case s => G.pure(s) + }) + ).value } diff --git a/tests/src/test/scala/cats/tests/FoldableSuite.scala b/tests/src/test/scala/cats/tests/FoldableSuite.scala index 09fab7b113..800929f2da 100644 --- a/tests/src/test/scala/cats/tests/FoldableSuite.scala +++ b/tests/src/test/scala/cats/tests/FoldableSuite.scala @@ -140,12 +140,20 @@ abstract class FoldableSuite[F[_]: Foldable](name: String)( fa.toList should === (iterator(fa).toList) } } - + test(s"Foldable[$name] mkString_") { forAll { (fa: F[Int]) => fa.mkString_("L[", ";", "]") should === (fa.toList.mkString("L[", ";", "]")) } } + + test(s"Foldable[$name].collectFirstSomeM") { + forAll { (fa: F[Int], n: Int) => + fa.collectFirstSomeM(x => (x > n).guard[Option].as(x).asRight[String]) should === (fa.toList.collectFirst { + case x if x > n => x + }.asRight[String]) + } + } } class FoldableSuiteAdditional extends CatsSuite {