Reputation: 9325
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
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
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 SignupResult
type.
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
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