Early success seems uncommon to me, but anyway here’s my take at simulating early returns in for comprehensions:
object EarlyReturn {
val db = ""
def handleTransfer(req: TransferRequest): Either[Err, Result] = {
val resultNested = for {
u1 <- req.user1.lookup(db).left.map(Left(_))
u2 <- req.user2.lookup(db).left.map { e =>
Right(Result(s"Cannot find ${req.user2}: $e"))
}
depositor <- u2.checkAccess(u1).left.map { _ =>
Right(Result(s"$u2 has not given you deposit access"))
}
withdrawal <- u1.withdraw(req.amount).left.map(Left(_))
conf <- depositor.accept(withdrawal).left.map(Left(_))
} yield {
Result(s"You transferred $withdrawal to $u2, confirmation number $conf")
}
resultNested.joinLeft
}
}
case class Err(msg: String)
case class Result(msg: String)
class User() {
def checkAccess(that: User): Either[Err, User] = ???
def withdraw(amount: Double): Either[Err, Double] = ???
def accept(amount: Double): Either[Err, Int] = ???
}
class UserName() {
def lookup(db: Any): Either[Err, User] = ???
}
case class TransferRequest(user1: UserName, user2: UserName, amount: Double)
Should be correct. I’ll check if I can factor out the .left.map(e => Left(???)) and .left.map(e => Right(???)).
Update: I managed to do so. Here’s the result:
object EarlyReturn {
val db = ""
def handleTransfer(req: TransferRequest): Either[Err, Result] = {
val resultNested = for {
u1 <- req.user1.lookup(db).nestError
u2 <- req.user2.lookup(db).earlyResult { e =>
Result(s"Cannot find ${req.user2}: $e")
}
depositor <- u2.checkAccess(u1).earlyResult { _ =>
Result(s"$u2 has not given you deposit access")
}
withdrawal <- u1.withdraw(req.amount).nestError
conf <- depositor.accept(withdrawal).nestError
} yield {
Result(s"You transferred $withdrawal to $u2, confirmation number $conf")
}
resultNested.joinLeft
}
implicit class LeftNestingEither[A, B](either: Either[A, B]) {
def nestError: Either[Either[A, Nothing], B] =
either.left.map(Left(_))
def earlyResult[C](errorToResult: A => C): Either[Either[Nothing, C], B] =
either.left.map(a => Right(errorToResult(a)))
}
}
case class Err(msg: String)
case class Result(msg: String)
class User() {
def checkAccess(that: User): Either[Err, User] = ???
def withdraw(amount: Double): Either[Err, Double] = ???
def accept(amount: Double): Either[Err, Int] = ???
}
class UserName() {
def lookup(db: Any): Either[Err, User] = ???
}
case class TransferRequest(user1: UserName, user2: UserName, amount: Double)