WestCoastProjects
WestCoastProjects

Reputation: 63201

Better handling of nested Options

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.

Upvotes: 1

Views: 334

Answers (4)

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

Upvotes: 2

jwvh
jwvh

Reputation: 51271

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

Upvotes: 3

Xavier Guihot
Xavier Guihot

Reputation: 61736

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].

Upvotes: 3

Binzi Cao
Binzi Cao

Reputation: 1085

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)
    }
  }

Upvotes: 3

Related Questions