Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add OptionT combinators for effectful Boolean #4390

Merged
merged 3 commits into from
Feb 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions core/src/main/scala/cats/data/OptionT.scala
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,23 @@ final case class OptionT[F[_], A](value: F[Option[A]]) {
def filter(p: A => Boolean)(implicit F: Functor[F]): OptionT[F, A] =
OptionT(F.map(value)(_.filter(p)))

/**
* Example:
* {{{
* scala> import cats.data.OptionT
*
* scala> val optionT: OptionT[List, Int] = OptionT[List, Int](List(Some(100), None, Some(421), Some(333)))
* scala> optionT.filterF(n => List(n % 100 == 0, n.toString.toSet.size == 1))
*
* res0: OptionT[List, Int] = OptionT(List(Some(100), None, None, None, None, None, Some(333)))
* }}}
*/
def filterF(p: A => F[Boolean])(implicit F: Monad[F]): OptionT[F, A] =
OptionT(F.flatMap(value) {
case v @ Some(a) => F.map(p(a)) { if (_) v else None }
case None => F.pure(None)
})

/**
* It is used for desugaring 'for comprehensions'. OptionT wouldn't work in 'for-comprehensions' without
* this method.
Expand Down Expand Up @@ -742,6 +759,14 @@ object OptionT extends OptionTInstances {
def whenF[F[_], A](cond: Boolean)(fa: => F[A])(implicit F: Applicative[F]): OptionT[F, A] =
if (cond) OptionT.liftF(fa) else OptionT.none[F, A]

/**
* Creates a non-empty `OptionT[F, A]` from an `F[A]` value if the given F-condition is considered `true`.
* Otherwise, `none[F, A]` is returned. Analogous to `Option.when` but for effectful conditions.
*/
def whenM[F[_], A](cond: F[Boolean])(fa: => F[A])(implicit F: Monad[F]): OptionT[F, A] = OptionT(
F.ifM(cond)(ifTrue = F.map(fa)(Some(_)), ifFalse = F.pure(None))
)
Comment on lines +766 to +768
Copy link
Contributor

@satorg satorg Feb 3, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to note: although there's nothing particularly wrong with this method, however I think it might not be as useful as it might seem at first glance. Just because it does not allow monads chaining. I.e. if a chain of F[_] ends with F[Boolean] then the entire chain has to be enwrapped into whenF to lift the result into OptionT.

I think, a better approach could be adding an extension method for F[Boolean] itself. And there's a good place for it already:
mouse / fboolean.scala. E.g. it could be a new extension method of type

F[Boolean] => F[A] => F[Option[A]]

then there's another method liftOptionT in there mouse / foption.scala
that can also be applied aferwards.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we already have when and whenF. In my personal opinion, I think it is fine to add it here.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, no objections either.


/**
* Same as `whenF`, but expressed as a FunctionK for use with mapK.
*/
Expand Down
4 changes: 4 additions & 0 deletions docs/nomenclature.md
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,11 @@ For convenience, in these types we use the symbol `OT` to abbreviate `OptionT`.
| `=> OT[F, A]` | `none` | `F: Applicative` |
| `A => OT[F, A]` | `some` or `pure` | `F: Applicative`
| `F[A] => OT[F, A]` | `liftF` | `F: Functor`
| `Boolean => F[A] => OT[F, A]` | `whenF` | `F: Applicative`
| `F[Boolean] => F[A] => OT[F, A]` | `whenM` | `F: Monad`
| `OT[F, A] => F[Option[A]]` | `value`
| `OT[F, A] => A => Boolean => OT[F, A]` | `filter` | `F: Functor`
| `OT[F, A] => A => F[Boolean] => OT[F, A]` | `filterF` | `F: Monad`
| `OT[F, A] => (A => B) => OT[F, B]` | `map` | `F: Functor`
| `OT[F, A] => (F ~> G) => OT[G, B]` | `mapK`
| `OT[F, A] => (A => Option[B]) => OT[F, B]` | `mapFilter` | `F: Functor`
Expand Down
21 changes: 21 additions & 0 deletions tests/shared/src/test/scala/cats/tests/OptionTSuite.scala
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,12 @@ class OptionTSuite extends CatsSuite {
}
}

test("OptionT[Id, A].filterF consistent with Option.filter") {
forAll { (o: Option[Int], f: Int => Boolean) =>
assert(o.filter(f) === (OptionT[Id, Int](o).filterF(f).value))
}
}

test("OptionT[Id, A].withFilter consistent with Option.withFilter") {
forAll { (o: Option[Int], f: Int => Boolean) =>
assert((for { x <- o if f(x) } yield x) === ((for { x <- OptionT[Id, Int](o) if f(x) } yield x).value))
Expand Down Expand Up @@ -378,6 +384,21 @@ class OptionTSuite extends CatsSuite {
}
}

test("OptionT.whenM[Id, A] consistent with Option.when") {
ivan-klass marked this conversation as resolved.
Show resolved Hide resolved
// Option.when is inlined here because it is not available before Scala 2.13
def when[A]: (Boolean, A) => Option[A] = (c: Boolean, a: A) => if (c) Some(a) else None

forAll { (i: Int, b: Boolean) =>
assert(OptionT.whenM[Id, Int](b)(i).value === (when(b, i)))
}
}

test("OptionT.whenF and OptionT.whenM consistent") {
forAll { (li: List[Int], bs: List[Boolean]) =>
assert(bs.flatMap(OptionT.whenF(_)(li).value) === OptionT.whenM(bs)(li).value)
}
}

test("OptionT.whenK and OptionT.whenF consistent") {
forAll { (li: List[Int], b: Boolean) =>
assert(IdT(li).mapK(OptionT.whenK(b)).value === (OptionT.whenF(b)(li)))
Expand Down