Teimuraz
Teimuraz

Reputation: 9325

How to convert following flatMap/map snippet to for-comprehension in Scala?

What can be best (liner/call-back free, less boilerplate) form of for-comprehension of following code snippet in Scala?

val result = emailTakenFuture.flatMap { emailTaken =>
  if (emailTaken) {
    Future.successful(SignUpResult.EmailAlreadyTaken)
  } else {
    usernameTakenFuture.flatMap { usernameTaken =>
      if (usernameTaken) {
        Future.successful(SignUpResult.UsernameAlreadyTaken)
      } else {
        nextIdFuture.flatMap { userId =>
          storeUserFuture(userId).map(user => SignUpResult.Success(user))
        }
      }
    }
  }
}

Upvotes: 3

Views: 87

Answers (3)

Mario Galic
Mario Galic

Reputation: 48420

Consider EitherT refactoring

type SignupResult[A] = EitherT[Future, SignupError, A]

where SignupError is the following ADT:

sealed trait SignupError
case object EmailAlreadyTaken    extends SignupError
case object UsernameAlreadyTaken extends SignupError
case object UserIdError          extends SignupError
case object UserCreationError    extends SignupError

then given the following method signatures

def validateEmail(email: String): SignupResult[Unit] = ???
def validateUsername(username: String): SignupResult[Unit] = ???
def nextId(): SignupResult[String] = ???
def storeUser(userId: String): SignupResult[User] = ???

flow flattens to a clean for-comprehension

(for {
  _      <- validateEmail("[email protected]")
  _      <- validateUsername("picard")
  userId <- nextId()
  user   <- storeUser(userId)
} yield user).value

Here is a working example

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

object EitherTExample extends App {
  sealed trait SignupError
  case object EmailAlreadyTaken extends SignupError
  case object UsernameAlreadyTaken extends SignupError
  case object UserIdError extends SignupError
  case object UserCreationError extends SignupError

  final case class User(id: String, username: String)

  type SignupResult[A] = EitherT[Future, SignupError, A]

  def validateEmail(email: String): SignupResult[Unit] = EitherT.rightT(())
  def validateUsername(username: String): SignupResult[Unit] = EitherT.leftT(UsernameAlreadyTaken)
  def nextId(): SignupResult[String] = EitherT.rightT("42424242")
  def storeUser(userId: String): SignupResult[User] = EitherT.rightT(User("42424242", "picard"))

  val result: Future[Either[SignupError, User]] =
    (for {
      _      <- validateEmail("[email protected]")
      _      <- validateUsername("picard")
      userId <- nextId()
      user   <- storeUser(userId)
    } yield user).value

  result.map(v => println(v))
}

which outputs

Left(UsernameAlreadyTaken)

Note how instead of true/false for validation purposes we have Right/Left.

Upvotes: 2

Markus Rother
Markus Rother

Reputation: 434

You may want to consider wrapping the intermediary results in Throwables. Then you can later recover on your future -- pattern matching for only those exceptions.

I included the "boilerplate" to make the example compilable:

import scala.concurrent.Future
import scala.concurrent.ExecutionContext

implicit val executionContext: ExecutionContext = ExecutionContext.global

case class User()

def emailTakenFuture: Future[Boolean] = ???
def usernameTakenFuture: Future[Boolean] = ???
def nextIdFuture: Future[String] = ???
def storeUserFuture(userId: String): Future[User]

For brevity, I extended Throwable. You may want to wrap the signup results in custom exceptions, in order to not expose them together with the SignupResulttype.

trait SignUpResult

case object SignUpResult {
  case object EmailAlreadyTaken extends Throwable with SignUpResult
  case object UsernameAlreadyTaken extends Throwable with SignUpResult
  case class Success(user: User) extends SignUpResult
}

val result: Future[SignUpResult] = {
  (for {
    emailTaken <- emailTakenFuture
    _ <- if (emailTaken) Future.failed(SignUpResult.EmailAlreadyTaken) else Future.successful(Unit)
    userNameTaken <- usernameTakenFuture
    _ <- if (userNameTaken) Future.failed(SignUpResult.UsernameAlreadyTaken) else Future.successful(Unit)
    userId <- nextIdFuture
    user <- storeUserFuture(userId)
  } yield SignUpResult.Success(user)).recoverWith {
    case (SignUpResult.EmailAlreadyTaken) => Future.successful(SignUpResult.EmailAlreadyTaken)
    case (SignUpResult.UsernameAlreadyTaken) => Future.successful(SignUpResult.UsernameAlreadyTaken)
  }
}

Upvotes: 2

Alexey Romanov
Alexey Romanov

Reputation: 170745

Only the part after the last else is really a good fit for a for-comprehension:

for {
  userId <- nextIdFuture
  user <- storeUserFuture(userId)
} yield SignUpResult.Success(user)

I'd just write a helper function for the rest:

def condFlatMap[T](future: Future[Boolean], ifTrue: T)(ifFalse: => Future[T]): Future[T] = 
  future.flatMap(x => if (x) Future.successful(ifTrue) else ifFalse)

val result = 
  condFlatMap(emailTakenFuture, SignUpResult.EmailAlreadyTaken) {
    condFlatMap(usernameTakenFuture, SignUpResult.UsernameAlreadyTaken) {
      for {
        userId <- nextIdFuture
        user <- storeUserFuture(userId)
      } yield SignUpResult.Success(user)
    }
  }

(not tested, but should be approximately correct)

Upvotes: 2

Related Questions