rincewind rincewind - 4 years ago 125
Scala Question

Scala pattern matching with async (or any monad) guards

Today I came across the following problem:
I had some pattern matching going on, simplified looking like this:

object Sync {
sealed trait MatchType
case object BigType extends MatchType
case object SmallType extends MatchType
case object EvenType extends MatchType
case object UnknownType extends MatchType

def syncPatternMatch(i: Int): MatchType = i match {
case _ if i > 100 => BigType
case _ if i < 3 => SmallType
case _ if i % 2 == 0 => EvenType
case _ => UnknownType
}
}


Now unfortunately I figured out, that my guards/extractors will be
Future[Boolean]
. Imagine they call an external web service to get the result.
Obviously I cant use the guard or extractor pattern with a Future (or any monad).

So now I want to check each condition asynchronously, but break on the first successful one.

Basically I want the opposite of normal monadic flow – meaning stopping on the first success.

My implementation seems to work well, but I am curios to see if there is an easier way or what kind of pattern you would use in this case.

Remember that my example is extremely simple for the sake of being an example.

import cats.data.EitherT
import cats.implicits._
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.Future

object Async {
sealed trait MatchType
case object BigType extends MatchType
case object SmallType extends MatchType
case object EvenType extends MatchType
case object UnknownType extends MatchType

type Match[B] = EitherT[Future, MatchType, B]
def isBigEnough(i: Int): Match[Unit] = Future(if(i > 100) Left(BigType) else Right(()))
def isVerySmall(i: Int): Match[Unit] = Future(if(i < 3) Left(SmallType) else Right(()))
def isEven(i: Int): Match[Unit] = Future(if(i % 2 == 0) Left(EvenType) else Right(()))
def otherwise: Match[MatchType] = Future.successful(Right(UnknownType))

implicit def liftFutureEither[A, B](f: Future[Either[A, B]]): EitherT[Future, A, B] = EitherT(f)
implicit def extractFutureEither[A, B](e: EitherT[Future, A, B]): Future[Either[A, B]] = e.value

def asyncPatternMatch(i: Int): Match[MatchType] = for {
_ <- isBigEnough(i)
_ <- isVerySmall(i)
_ <- isEven(i)
default <- otherwise
} yield default

asyncPatternMatch(10).foreach(either => println(either.fold(identity, identity)))
// EvenType
}


(btw. it is scala 2.12)

I would be happy for suggestions :)

Answer Source

What you need is a 'monadic if' like the cats one. We can actually roll a simplified version specifically for Future:

import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits.global

sealed trait MatchType
object MatchType {
  case object Big extends MatchType
  case object Small extends MatchType
  case object Even extends MatchType
  case object Unknown extends MatchType

  def isBigEnough(i: Int): Future[Boolean] = Future successful (i > 100)
  def isVerySmall(i: Int): Future[Boolean] = Future successful (i < 3)
  def isEven(i: Int): Future[Boolean] = Future successful (i % 2 == 0)

  def ifFuture[A](
    test: Future[Boolean],
    trueCase: => A,
    falseCase: => Future[A]): Future[A] =
    test filter identity map (_ => trueCase) recoverWith {
      case _: NoSuchElementException => falseCase
    }

  def apply(i: Int): Future[MatchType] =
    ifFuture(isBigEnough(i), Big,
    ifFuture(isVerySmall(i), Small,
    ifFuture(isEven(i), Even,
    Future successful Unknown)))
}
Recommended from our users: Dynamic Network Monitoring from WhatsUp Gold from IPSwitch. Free Download