Knows Not Much Knows Not Much - 1 month ago 10
Scala Question

Dealing with multiple options and logging not found scenarios

When I have multiple Options and I have to process something only when all of them have a value, the

for comprehension
provides a great way to write code

for {
a <- aOption
b <- bOption
c <- cOption
d <- dOption
} yield {...process...}


Although this is very useful and elegant and concise way of writing code, I miss the ability to log if say "cOption" got a none value and thus the processing did not happen.

Is there a nice way in the code above, to be able to log the missing value without resorting to nested ifs.

Answer

You can write a simple function, but it's gonna log only first absence of value inside Option (due to sequential nature of for-comprehension):

def logEmpty[T](opt: Option[T], msgIfNone: String) = {
   if (opt.isEmpty) println(msgIfNone) //or something like logger.warn
   opt
}

Usage:

for {
  a <- logEmpty(aOption, "Sorry no a")
  b <- logEmpty(bOption, "Sorry no b") 
  c <- logEmpty(cOption, "Sorry no c")
  d <- logEmpty(dOption, "Sorry no d")
} yield {...process...}

DSL-like:

implicit class LogEmpty[T](opt: Option[T]) {
  def reportEmpty(msg: String) = {
    if (opt.isEmpty) println(msg)
    opt
  }
}

Usage:

for {
  a <- aOption reportEmpty "Sorry no a"
  b <- bOption reportEmpty "Sorry no b"
  c <- cOption reportEmpty "Sorry no c"
  d <- dOption reportEmpty "Sorry no d"
} yield {a + b + c + d}

Example:

scala> for {
     |   a <- Some("a") reportEmpty "Sorry no a"
     |   b <- None reportEmpty "Sorry no b"
     |   c <- Some("c") reportEmpty "Sorry no c"
     |   d <- None reportEmpty "Sorry no d"
     | } yield {a + b + c + d}
Sorry no b
res19: Option[String] = None

If you need to report more - the best way is to use Validation from scalaz or Validated from cats, so your message about the abscence is gonna be represented as invalid state of Validated. You can always convert Validated to Option.

Solution:

import cats._
import cats.data.Validated
import cats.data.Validated._
import cats.implicits._

implicit class RichOption[T](opt: Option[T]) {
  def validOr(msg: String) = 
    opt.map(Valid(_)).getOrElse(Invalid(msg)).toValidatedNel

}

Example:

val aOption = Some("a")
val bOption: Option[String] = None
val cOption: Option[String] = None

scala> aOption.validOr("no a") |+| bOption.validOr("no b") |+| cOption.validOr("no c")
res12: cats.data.Validated[cats.data.NonEmptyList[String],String] = Invalid(NonEmptyList(no b, no c))

scala> aOption.validateOr("no a") |+| aOption.validateOr("no a again")
res13: cats.data.Validated[cats.data.NonEmptyList[String],String] = Valid(aa)

I used |+| operator assuming concatenation, but you can use applicative builders (or just zip) as well in order to implement other operation over option's content:

scala> (aOption.validOr("no a") |@| aOption.validOr("no a again")) map {_ + "!" + _}
res18: cats.data.Validated[cats.data.NonEmptyList[String],String] = Valid(a!a)

scala> (aOption.validOr("no a") |@| bOption.validOr("no b") |@| cOption.validOr("no c")) map {_ + _ + _}
res27: cats.data.Validated[cats.data.NonEmptyList[String],String] = Invalid(NonEmptyList(no b, no c))

Both cat's Xor and Validated are variations of scala's Either , but the difference between Xor and Validated is that Xor (and Either) is more adopted for "fail-fast" monadic approach (for comprehensions aka do-notation) in contrast to Validated that is using applicative approach (which allows |@| and zip). flatMap is considered as sequential operator, |@|/zip are considered as parallel operator (don't confuse with execution model - it's orthogonal to the nature of operator). You can read more in cats documentation: Validated, Xor.