I am wondering if there is a idiomatic way of using guard clauses to leave a method early if a condition is met.
In other languages, I would do something like this:
def myFunction(): Either[String, String] = {
if (someErrorCondition)
return Left("error msg")
// Rest of the code
}
However, this seems wrong to me, since using return
does not seem to be the idiomatic way of doing it. I have considered this:
def myFunction(): Either[String, String] = {
if (someErrorCondition)
Left("error msg")
else
// Rest of the code
}
But it is ugly and would imply many if-else
where multiple guards are needed.
Any suggestions on how to do this properly?
Seconding others, IMHO, the idea that conditional expression are not idiomatic is a fallacy, hence the following seem OK to me
if (errorCondition1) Left("error msg 1")
else if (errorCondition2) Left("error msg 2")
else Right(42)
The idea of early returns or short-circuiting is inherent in for-comprehensions so here is an unconventional approach which converts Booleans
to Eithers
where true
converts to Left
via extension method
implicit class GuardToLeft(p: Boolean) {
def toGuard[L](left: => L): Either[L, Unit] =
if (p) Left(left) else Right(())
}
so now we can simulate traditional early return guards like so
for {
_ <- errorCondition1.toGuard("error msg 1")
_ <- errorCondition2.toGuard("error msg 2")
} yield {
41 + 1
}
The FP way is to use a type which has some form of error handling build in, e.g. Either
.
Let's say that String
is your error format. Then you can return early ("circuit break") using flatMaps
- for Either
Left
is the error format, and Right
is used for threading ongoing computation:
import scala.util._
def sqrtFromString(argument: String): Either[String, Double] = {
val start: Either[String, String] = Right(argument) // just for upcasting Right to Either
start
// if start is Right, then value inside it will be passed as string variable
// Right will continue computation
// Left will circuit break them
.flatMap { string =>
Try(string.toInt) match {
case Success(int) => Right(int)
case Failure(_) => Left("Not integer")
}
}
// if so far it is still Right, the value inside will be passed as int variable
.flatMap { int =>
if (int > 0) Right(int)
else Left("Negative int")
}
// if Right, it still will be Right - no ability to circuit break using map
.map { int =>
Math.sqrt(int)
}
}
Since map
and flatMap
are handled by for
comprehension you could rewrite it to:
import scala.util._
def sqrtFromString(argument: String): Either[String, Double] =
for {
string <- (Right(argument): Either[String, String])
int <- Try(string.toInt) match {
case Success(int) => Right(int)
case Failure(_) => Left("Not integer")
}
positive <- {
if (int > 0) Right(int)
else Left("Negative int")
}
} yield Math.sqrt(positive)
which could be further shortened to:
def sqrtFromString(argument: String): Either[String, Double] =
for {
string <- Right(argument)
int <- Try(string.toInt).toEither.left.map(_ => "Not integer")
_ <- (if (int > 0) Right(()) else Left("Negative int"))
} yield Math.sqrt(int)
The same principle works whether you are using Try
or Future
(they are like sync and async Either
but with Left
hardcoded to Throwable
and build-in exception catching), IO
, Task
, ZIO
, etc.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With