1

Consider a nested structure in which the relevant attributes are as follows:

case class Validation { sql: Option[SqlDataSource] }

case class SqlDataSource { dfh: Option[DataFrameHolder] }

case class DataFrameHolder { sql: Option[String] }

The naive way that I am working with this presently is:

    val ssql = expVal.sql.getOrElse(
        vc.dfh.map(_.sql
          .getOrElse(throw new IllegalStateException(s"$logMsg CompareDF: Missing sql container"))
        ).getOrElse(throw new IllegalStateException(s"$logMsg CompareDF: dfh missing sql"))
      .sql.getOrElse(throw new IllegalStateException(s"$logMsg CompareDF: Missing sql")))

While this does get the job done it is also reader-unfriendly and developer unfriendly (tough to get the nesting correctly). Any thoughts on better ways to handle this?

Update thanks for the great answers - this will help clean up and simplify the exception handling code moving forward.

WestCoastProjects
  • 58,982
  • 91
  • 316
  • 560

4 Answers4

3

You might want to use the cats lib to make your code more functional. You can convert Option to Either as below:

import cats.implicits._

 def toEither[T](s: Option[T], error: String) = {
    s.liftTo[Either[String, ?]](error)
  }

  def runEither = {

    val result =
      for {
        sqlDataSource   <- toEither(validation.sql, s"Missing sql container")
        dataFrameHolder <- toEither(sqlDataSource.dfh, s"dfh missing sql")
        sql             <- toEither(dataFrameHolder.sql, s"Missing sql")
      } yield sql
    result match {
      case Right(r) => r
      case Left(e)  => throw new Exception(e)
    }
  }
Binzi Cao
  • 1,075
  • 5
  • 14
3

I would suggest a very similar solution to Binzi Cao's answer but only using the standard library with the scala.util.Try monad:

def toTry[T](x: Option[T], message: String): Try[T] =
  x.map(Success(_)).getOrElse(Failure(new IllegalStateException(message)))

(for {
  sqlDataSource   <- toTry(validation.sql, s"Missing sql container")
  dataFrameHolder <- toTry(sqlDataSource.dfh, s"dfh missing sql")
  sql             <- toTry(dataFrameHolder.sql, s"Missing sql")
} yield sql)
.get

The for-comprehension produces a Try. Applying .get on the produced Try will either return the content of the Try (the String within dataFrameHolder) if i's a Success[String] or throw the exception if it's a Failure[IllegalStateException].

Xavier Guihot
  • 54,987
  • 21
  • 291
  • 190
3

The answers from Luis Miguel Mejía Suárez and Xavier Guihot are both quite good, but there's no need to build your own toTry() method. Either already offers one and, since Either is also right-biased, it can be used in the for-comprehension.

import util.Try

val ssql: Try[String] = (for {
  ql  <- expVal.ql.toRight(new IllegalStateException("yada-yada"))
  dfh <- ql.dfh   .toRight(new IllegalStateException("yoda-yoda"))
  sql <- dfh.sql  .toRight(new IllegalStateException("yeah-yeah"))
} yield sql).toTry
jwvh
  • 50,871
  • 7
  • 38
  • 64
  • Given this one stays the course with vanilla scala (a *plus* though not a *must*) and does exactly what I was looking for - am re-assigning awarding here. – WestCoastProjects Sep 25 '18 at 14:48
  • This is a good one actually. I just noticed the `Either` is now right-biased since 2.12, which was not the case in earlier versions. By the way, I think it should be `Either` instead of `Try` in the answer, which might be a typo? – Binzi Cao Sep 25 '18 at 23:37
  • @BinziCao; The `Either` is cast to a `Try` (via `toTry`) so that the return type is consistent with the other answers that I reference. The OP is free to leave it as an `Either` if that works out better. – jwvh Sep 28 '18 at 16:39
2

If you want fail fast semantics, I would use for comprehension with Trys.

final case class Validation(ql: Option[SqlDataSource])
final case class SqlDataSource(dfh: Option[DataFrameHolder])
final case class DataFrameHolder(sql: Option[String])

val expVal = Validation(
    ql = Some(
      SqlDataSource(
        dfh = Some(
          DataFrameHolder(
            sql = Some("Hello, World!")
          )
        )
      )
    )
  )


implicit class OptionOps[T](private val op: Option[T]) {
  def toTry(ex: => Throwable): Try[T] = op match {
    case Some(t) => Success(t)
    case None    => Failure(ex)
  }
}

val ssql: Try[String] = for {
  ql <- expVal.ql.toTry(new IllegalStateException("CompareDF: Missing sql container"))
  dfh <- ql.dfh.toTry(new IllegalStateException("CompareDF: dfh missing sql"))
  sql <- dfh.sql.toTry(new IllegalStateException("CompareDF: Missing sql"))
} yield sql
  • useful as well - and not requiring an additional library – WestCoastProjects Sep 25 '18 at 01:13
  • @javadba Yeah, but to be honest I would prefer the Cats solution, specially because you could use ValidatedNel to accumulate all errors, and Either is better than Try. Also, just as an advice try to avoid throwing exceptions. – Luis Miguel Mejía Suárez Sep 25 '18 at 01:29
  • "avoid throwing exceptions" no agreement on that. Throwing exceptions works well in our production environment to bubble up to the logging and alerting mechanisms. *worst* thing to do is *EAT* exceptions – WestCoastProjects Sep 25 '18 at 02:03
  • Oh sorry, I should had added a better explanation about that - but I was in a hurry, my fault. Ok so, I'm not saying that shouldn't report errors in your program, even less that you shouldn't handle them. What I'm saying is that you shouldn't throw them - as a side effectual thing, but rather use `Either` or `Try` to encapsulate your errors in a type safe way - the only exceptions that should be throw are real _FATAL_ errors like **StackOverflow** or **OutOfMemory**, that BTW shouldn't be catched, but instead leave them crash the app. See [this](https://stackoverflow.com/a/12886474/4111404) – Luis Miguel Mejía Suárez Sep 25 '18 at 02:39