Home > Software engineering >  How to return upon encountering first "true" in a List[IO[Boolean]] in Scala Cats Effect
How to return upon encountering first "true" in a List[IO[Boolean]] in Scala Cats Effect

Time:12-06

Say I have a set of rules that have a validation function that returns IO[Boolean] at runtime.

case class Rule1() {
  def validate(): IO[Boolean] = IO.pure(false)
}
case class Rule2() {
  def validate(): IO[Boolean] = IO.pure(false)
}
case class Rule3() {
  def validate(): IO[Boolean] = IO.pure(true)
}

val rules = List(Rule1(), Rule2(), Rule3())

Now I have to iterate through these rules and see "if any of these rules" hold valid and if not then throw exception!

for {
  i <- rules.map(_.validate()).sequence
  _ <- if (i.contains(true)) IO.unit else IO.raiseError(new RuntimeException("Failed"))
} yield ()

The problem with the code snippet above is that it is trying to evaluate all the rules! What I really want is to exit at the encounter of the first true validation.

Not sure how to achieve this using cats effects in Scala.

CodePudding user response:

I claim that existsM is the most direct way to achieve what you want. It behaves pretty much the same as exists, but for monadic predicates:

for {
  t <- rules.existsM(_.validate())
  _ <- IO.raiseUnless(t)(new RuntimeException("Failed"))
} yield ()

It also stops the search as soon as it finds the first true.

The raiseUnless is just some syntactic sugar that's equivalent to the if-else from your question.

CodePudding user response:

If you take a look at list of available extension methods in your IDE, you can find findM:

for {
  opt <- rules.findM(_.validate())
  _ <- opt match {
    case Some(_) => IO.unit
    case None    => IO.raiseError(new RuntimeException("Failed")
  }
} yield ()

Doing it manually could be done with foldLeft and flatMap:

rules.foldLeft(IO.pure(false)) { (valueSoFar, nextValue) =>
  valueSoFar.flatMap {
    case true  => IO.pure(true)        // can skip evaluating nextValue 
    case false => nextValue.validate() // need to find the first true IO yet
  }
}.flatMap {
  case true  => IO.unit
  case false => IO.raiseError(new RuntimeException("Failed")
}

The former should have the additional advantage that it doesn't have to iterate over whole collection when it finds the first match, while the latter will still go through all items, even if will start discarding them at some point. findM solves that by using tailRecM internally to terminate the iteration on first met condition.

CodePudding user response:

You can try recursive

def firstTrue(rules: List[{def validate(): IO[Boolean]}]): IO[Unit] = rules match {
  case r :: rs => for {
    b <- r.validate()
    res <- if (b) IO.unit else firstTrue(rs)
  } yield res
  case _ => IO.raiseError(new RuntimeException("Failed"))
}

CodePudding user response:

Another approach is not using booleans at all, but the monad capabilities of IO

def validateRules(rules: List[Rule]): IO[Unit] =
  rules.traverse_ { rule =>
    rule.validate().flatMap { flag =>
      IO.raiseUnless(flag)(new RuntimeException("Failed"))
    }
  }
  • Related