Say I have three database access functions foo
, bar
, and baz
that can each return Option[A]
where A
is some model class, and the calls depend on each other.
I would like to call the functions sequentially and in each case, return an appropriate error message if the value is not found (None
).
My current code looks like this:
Input is a URL: /x/:xID/y/:yID/z/:zID
foo(xID) match {
case None => Left(s"$xID is not a valid id")
case Some(x) =>
bar(yID) match {
case None => Left(s"$yID is not a valid id")
case Some(y) =>
baz(zID) match {
case None => Left(s"$zID is not a valid id")
case Some(z) => Right(process(x, y, z))
}
}
}
As can be seen, the code is badly nested.
If instead, I use a for
comprehension, I cannot give specific error messages, because I do not know which step failed:
(for {
x <- foo(xID)
y <- bar(yID)
z <- baz(zID)
} yield {
Right(process(x, y, z))
}).getOrElse(Left("One of the IDs was invalid, but we do not know which one"))
If I use map
and getOrElse
, I end up with code almost as nested as the first example.
Is these some better way to structure this to avoid the nesting while allowing specific error messages?
I came up with this solution (based on @Rex's solution and his comments):
def ifTrue[A](boolean: Boolean)(isFalse: => A): RightProjection[A, Unit.type] =
Either.cond(boolean, Unit, isFalse).right
def none[A](option: Option[_])(isSome: => A): RightProjection[A, Unit.type] =
Either.cond(option.isEmpty, Unit, isSome).right
def some[A, B](option: Option[A])(ifNone: => B): RightProjection[B, A] =
option.toRight(ifNone).right
They do the following:
ifTrue
is used when a function returns a Boolean
, with true
being the "success" case (e.g.: isAllowed(userId)
). It actually returns Unit
so should be used as _ <- ifTrue(...) { error }
in a for
comprehension.none
is used when a function returns an Option
with None
being the "success" case (e.g.: findUser(email)
for creating accounts with unique email addresses). It actually returns Unit
so should be used as _ <- none(...) { error }
in a for
comprehension.some
is used when a function returns an Option
with Some()
being the "success" case (e.g.: findUser(userId)
for a GET /users/userId
). It returns the contents of the Some
: user <- some(findUser(userId)) { s"user $userId not found" }
.They are used in a for
comprehension:
for {
x <- some(foo(xID)) { s"$xID is not a valid id" }
y <- some(bar(yID)) { s"$yID is not a valid id" }
z <- some(baz(zID)) { s"$zID is not a valid id" }
} yield {
process(x, y, z)
}
This returns an Either[String, X]
where the String
is an error message and the X
is the result of calling process
.