diff --git a/core/src/main/scala/cats/data/OptionT.scala b/core/src/main/scala/cats/data/OptionT.scala index 71774d6b34..1104c65fb3 100644 --- a/core/src/main/scala/cats/data/OptionT.scala +++ b/core/src/main/scala/cats/data/OptionT.scala @@ -787,6 +787,14 @@ object OptionT extends OptionTInstances { def unlessF[F[_], A](cond: Boolean)(fa: => F[A])(implicit F: Applicative[F]): OptionT[F, A] = OptionT.whenF(!cond)(fa) + /** + * Creates a non-empty `OptionT[F, A]` from an `F[A]` value if the given F-condition is considered `false`. + * Otherwise, `none[F, A]` is returned. Analogous to `Option.unless` but for effectful conditions. + */ + def unlessM[F[_], A](cond: F[Boolean])(fa: => F[A])(implicit F: Monad[F]): OptionT[F, A] = OptionT( + F.ifM(cond)(ifTrue = F.pure(None), ifFalse = F.map(fa)(Some(_))) + ) + /** * Same as `unlessF`, but expressed as a FunctionK for use with mapK. */ diff --git a/tests/shared/src/test/scala/cats/tests/OptionTSuite.scala b/tests/shared/src/test/scala/cats/tests/OptionTSuite.scala index b5b9575800..bb6ae51483 100644 --- a/tests/shared/src/test/scala/cats/tests/OptionTSuite.scala +++ b/tests/shared/src/test/scala/cats/tests/OptionTSuite.scala @@ -421,6 +421,21 @@ class OptionTSuite extends CatsSuite { } } + test("OptionT.unlessM[Id, A] consistent with Option.unless") { + // Option.unless is inlined here because it is not available before Scala 2.13 + def unless[A]: (Boolean, A) => Option[A] = (c: Boolean, a: A) => if (!c) Some(a) else None + + forAll { (i: Int, b: Boolean) => + assert(OptionT.unlessM[Id, Int](b)(i).value === (unless(b, i))) + } + } + + test("OptionT.unlessF and OptionT.unlessM consistent") { + forAll { (li: List[Int], bs: List[Boolean]) => + assert(bs.flatMap(OptionT.unlessF(_)(li).value) === OptionT.unlessM(bs)(li).value) + } + } + test("OptionT.unlessK and OptionT.unlessF consistent") { forAll { (li: List[Int], b: Boolean) => assert(IdT(li).mapK(OptionT.unlessK(b)).value === (OptionT.unlessF(b)(li)))