Skip to content

Commit

Permalink
add ifElseM (#3553)
Browse files Browse the repository at this point in the history
* add ifElseM

* move `ifElseM` to `Monad` and implement it in terms of `tailRecM`
  • Loading branch information
mtomko authored Aug 12, 2020
1 parent 0640b67 commit 94eb6e2
Show file tree
Hide file tree
Showing 2 changed files with 60 additions and 2 deletions.
29 changes: 29 additions & 0 deletions core/src/main/scala/cats/Monad.scala
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,35 @@ import scala.annotation.implicitNotFound
def iterateUntilM[A](init: A)(f: A => F[A])(p: A => Boolean): F[A] =
iterateWhileM(init)(f)(!p(_))

/**
* Simulates an if/else-if/else in the context of an F. It evaluates conditions until
* one evaluates to true, and returns the associated F[A]. If no condition is true,
* returns els.
*
* {{{
* scala> import cats._
* scala> Monad[Eval].ifElseM(Eval.later(false) -> Eval.later(1), Eval.later(true) -> Eval.later(2))(Eval.later(5)).value
* res0: Int = 2
* }}}
*
* Based on a [[https://gist.github.com/1b92a6e338f4e1537692e748c54b9743 gist]] by Daniel Spiewak with a stack-safe
* [[https://github.com/typelevel/cats/pull/3553#discussion_r468121480 implementation]] due to P. Oscar Boykin
* @see See [[https://gitter.im/typelevel/cats-effect?at=5f297e4314c413356f56d230]] for the discussion.
*/
@noop
def ifElseM[A](branches: (F[Boolean], F[A])*)(els: F[A]): F[A] = {
type Branches = List[(F[Boolean], F[A])]

def step(branches: Branches): F[Either[Branches, A]] =
branches match {
case (cond, conseq) :: tail =>
flatMap(cond) { b => if (b) map(conseq)(Right(_)) else pure(Left(tail)) }
case Nil =>
map(els)(Right(_))
}

tailRecM(branches.toList)(step)
}
}

object Monad {
Expand Down
33 changes: 31 additions & 2 deletions tests/src/test/scala/cats/tests/MonadSuite.scala
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
package cats.tests

import cats.{Id, Monad}
import cats.{Eval, Id, Monad}
import cats.data.{IndexedStateT, StateT}
import cats.syntax.apply._
import cats.syntax.monad._
import org.scalacheck.Gen
import org.scalacheck.{Arbitrary, Gen}
import cats.syntax.eq._
import org.scalacheck.Prop._

Expand Down Expand Up @@ -104,4 +104,33 @@ class MonadSuite extends CatsSuite {
assert(sum === (n * (n + 1) / 2))
}

test("ifElseM") {
val tupleGen =
for {
b <- Arbitrary.arbitrary[Boolean]
i <- smallPosInt
} yield b -> i

forAll(Gen.listOf(tupleGen)) { xs: List[(Boolean, Int)] =>
val expected = xs.collectFirst { case (true, x) => x }.getOrElse(-1)
val branches = xs.map { case (b, x) => (Eval.now(b), Eval.now(x)) }
assert(Monad[Eval].ifElseM(branches: _*)(Eval.now(-1)).value === expected)
}
}

test("ifElseM resorts to default") {
forAll(Gen.listOf(smallPosInt)) { xs: List[Int] =>
val branches = xs.map(x => (Eval.now(false), Eval.now(x)))
assert(Monad[Eval].ifElseM(branches: _*)(Eval.now(-1)).value === -1)
}
}

// this example is in the doctest and I'm not sure how useful it is to have here as well except as a way
// to play with
test("ifElseM example") {
val actual =
Monad[Eval].ifElseM(Eval.later(false) -> Eval.later(1), Eval.later(true) -> Eval.later(2))(Eval.later(5))
assert(actual.value === 2)
}

}

0 comments on commit 94eb6e2

Please sign in to comment.