3

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.

Knows Not Much
  • 30,395
  • 60
  • 197
  • 373

4 Answers4

4

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.

dk14
  • 22,206
  • 4
  • 51
  • 88
1

Functional programming often works a lot more cleanly if you forgo built-in syntax and DSLs in favor of just doing simple operations on simple data structures:

val options = List((aOption, "a"),
                   (bOption, "b"), 
                   (cOption, "c"), 
                   (dOption, "d"))

val undefinedOptions = options filterNot (_._1.isDefined)

if (undefinedOptions.isEmpty)
  println("Process...")
else
  undefinedOptions map {case (_,name) => s"$name is not defined"} foreach println

Sometimes monads simplify your code and sometimes they don't. Don't forget you can treat Options like plain old boring objects too.

Karl Bielefeldt
  • 47,314
  • 10
  • 60
  • 94
  • just to mention that this approach will not extract options to values. You'll have to do one more step: `val List(a,b,c,d) = options .map(_._1.get)`, which is not much safe - as there is a risk of matching exception (in runtime!!!) if you don't match extractor with `List`'s size. Besides `List` isn't a good structure for heterogeneous data - OP didn't say that all options are of the same type (that you've assumed here) - it might be `aOption: Option[String]`, `bOption: Option[Int]` etc. – dk14 Oct 16 '16 at 15:41
  • Minor: strictly saying when you're using logging directly (without Writer monad which is unpractical) - it's not functional programming anymore - nothing bad about it as FP isn't best approach for logging and debugging, but still I wouldn't call it "functional", maybe just "scala". – dk14 Oct 16 '16 at 15:46
  • The `printlns` weren't intended to be representative of a final solution, just a placeholder example. And no, you don't have a risk of a runtime exception, because you've checked for that condition and the list of `Options` is immutable. Also for this purpose, it doesn't matter if the `List` is homogenous. It can be a `List[(Option[Any], String)]` and work just fine. The `process` step doesn't have to extract them from my list. – Karl Bielefeldt Oct 16 '16 at 18:57
  • No, `Option[Any]` will not work just fine as you'll have to use `asInstanceOf` to recover types. The `process` step has to (!!!) because they were extracted in original question (that basically was the point of using `for`!!!!). And again you have risk of runtime exception during extraction as you'll have to be sure that size of option's list is same as number of extracted values - or more generally you'll have to "parse" `options` value (which can be changed in the code) – dk14 Oct 17 '16 at 04:19
  • Just imagine `options = list("a", "b", "c"). val List(a, b, c) = options` (you have to do the last step - that's what `for` is needed for) and someone removed "c" (or `c`) from the list during refactoring - `MatchException`, right? you can imagine any other way of extraction (than pattern matching) - but all of them are gonna have one thing in common - risk of runtime exception – dk14 Oct 17 '16 at 04:22
  • And again `Option[Any]` is never good for type-safety. Do I really have to prove it? – dk14 Oct 17 '16 at 04:23
  • And, talking about FP - I meant that the OP's question itself doesn't ask for an FP solution and the problem itself is not related to FP much. – dk14 Oct 17 '16 at 04:31
  • "list of Options is immutable" - it doesn't mean that developer can't accidentally change it in the code - again, I wasn't talking about immutability and FP, I was talking about type-safety which is a different matter and your solution affects it significantly – dk14 Oct 17 '16 at 04:33
1

When you are dealing with calculations which may fail and would like to get a reason why one of them failed, you can use an Either monad or Validation from Scalaz and alike. I've asked such question (Using Either to process failures in Scala code), so I advise you to take a look at it, because it has some good answers. I asked it a while ago and the answers were written before Scala 2.10 release where the standard library got another nice monad - scala.util.Try[T], it (quoting the documentation) represents a computation that may either result in an exception, or return a successfully computed value.

A scenario when one of the computations fails:

scala> for { a <- Try( Some(1).getOrElse(sys.error("a is none")) )
             b <- Try( Option.empty[Int].getOrElse(sys.error("b is none")) ) }
       yield a+b
res1: scala.util.Try[Int] = Failure(java.lang.RuntimeException: b is none)

A scenario when all the computations succeed:

scala> for { a <- Try(Some(1).get)
             b <- Try(Some(2).get) }
       yield a+b
res2: scala.util.Try[Int] = Success(3)
Community
  • 1
  • 1
Alexander Azarov
  • 12,971
  • 2
  • 50
  • 54
  • Thanks. in my case I don't want to raise errors if one of the values was none. I just want to log that value was none and return Option.None as a result. – Knows Not Much Oct 16 '16 at 21:53
  • 1
    @KnowsNotMuch it's not about "raising errors", it's more about knowing more information. `Either` may contain either `T` or anything else (e.g. `String` for validation purposes), `Try` may contain `T` or an exception. Thus you may get a formal reason why a computation failed instead of rather cryptic "variable `a` was `None`". – Alexander Azarov Oct 17 '16 at 12:13
0

Using foldLeft

using foldLeft maintain index and result list so that index can help log and list is the resultant list after retrieving values from options.

note that process gives empty list if any of the options is none

val options = List(Some(1), Some(2), Some(3), None, None, Some(4), None)

 def process[A, B](options: List[Option[A]])(f: (Int, Option[A]) => Option[B]): List[B] = {
  val result =
    options.foldLeft(List.empty[B] -> 0) { (r, c) =>
      val (result, index) = r
      f(index, c).map(result ++ List(_) -> (index + 1)).getOrElse(result -> (index + 1))
    }
  if (result._1.length == options.length) result._1 else List.empty[B]
}


process[Int, Int](options) { (index, current) =>
  current.orElse {
    println(s"$index is none.")
    current
  }
}.foreach(println)
Nagarjuna Pamu
  • 14,737
  • 3
  • 22
  • 40