Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What is the Scala way of using guard clauses to exit a function early?

Tags:

scala

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?

like image 699
Selnay Avatar asked Mar 03 '23 00:03

Selnay


2 Answers

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
}
like image 123
Mario Galic Avatar answered May 09 '23 04:05

Mario Galic


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.

like image 21
Mateusz Kubuszok Avatar answered May 09 '23 05:05

Mateusz Kubuszok