Chaining scala Try instances that contain Options

2.1k views Asked by At

I am trying to find a cleaner way to express code that looks similar to this:

def method1: Try[Option[String]] = ???
def method2: Try[Option[String]] = ???
def method3: Try[Option[String]] = ???

method1 match
{
  case f: Failure[Option[String]] => f
  case Success(None) =>
  method2 match
    {
      case f:Failure[Option[String]] => f
      case Success(None) =>
      {
        method3
      }
      case s: Success[Option[String]] => s
    }
  case s: Success[Option[String]] => s
}

As you can see, this tries each method in sequence and if one fails then execution stops and the base match resolves to that failure. If method1 or method2 succeeds but contains None then the next method in the sequence is tried. If execution gets to method3 its results are always returned regardless of Success or Failure. This works fine in code but I find it difficult to follow whats happening.

I would love to use a for comprehension

for
{
  attempt1 <- method1
  attempt2 <- method2
  attempt3 <- method3
}
  yield
{
  List(attempt1, attempt2, attempt3).find(_.isDefined)
}

because its beautiful and what its doing is quite clear. However, if all methods succeed then they are all executed every time, regardless of whether an earlier method returns a usable answer. Unfortunately I can't have that.

Any suggestions would be appreciated.

5

There are 5 answers

0
stew On BEST ANSWER

scalaz can be of help here. You'll need scalaz-contrib which adds a monad instance for Try, then you can use OptionT which has nice combinators. Here is an example:

import scalaz.OptionT
import scalaz.contrib.std.utilTry._
import scala.util.Try

def method1: OptionT[Try, String] = OptionT(Try(Some("method1")))
def method2: OptionT[Try, String] = OptionT(Try(Some("method2")))
def method3: OptionT[Try, String] = { println("method 3 is never called") ; OptionT(Try(Some("method3"))) }
def method4: OptionT[Try, String] = OptionT(Try(None))
def method5: OptionT[Try, String] = OptionT(Try(throw new Exception("fail")))

println((method1 orElse method2 orElse method3).run) // Success(Some(method1))
println((method4 orElse method2 orElse method3).run) // Success(Some(method2))
println((method5 orElse method2 orElse method3).run) // Failure(java.lang.Exception: fail)
2
Thayne On
method1.flatMap(_.map(Success _).getOrElse(method2)).flatMap(_.map(Success _).getOrElse(method3))

How this works:

The first flatMap takes a Try[Option[String]], if it is a Failure it returns the Failure, if it is a Success it returns _.map(Success _).getOrElse(method2) on the option. If the option is Some then it returns the a Success of the Some, if it is None it returns the result of method2, which could be Success[None], Success[Some[String]] or Failure.

The second map works similarly with the result it gets, which could be from method1 or method2.

Since getOrElse takes a by-name paramater method2 and method3 are only called if they need to be.

You could also use fold instead of map and getOrElse, although in my opinion that is less clear.

0
Rex Kerr On

If you don't mind creating a function for each method, you can do the following:

(Try(None: Option[String]) /: Seq(method1 _, method2 _, method3 _)){ (l,r) =>
  l match { case Success(None) => r(); case _ => l }
}

This is not at all idiomatic, but I would like to point out that there's a reasonably short imperative version also with a couple tiny methods:

def okay(tos: Try[Option[String]]) = tos.isFailure || tos.success.isDefined

val ans = {
  var m = method1
  if (okay(m)) m
  else if ({m = method2; okay(m)}) m
  method3
}
0
Jiri Kremser On

The foo method should do the same stuff as your code, I don't think it is possible to do it using the for comprehension

type tryOpt = Try[Option[String]]
def foo(m1: tryOpt, m2: tryOpt, m3: tryOpt) = m1 flatMap {
  case x: Some[String] => Try(x)
  case None => m2 flatMap {
      case y: Some[String] => Try(y)
      case None => m3
  }
}
2
YourBestBet On

From this blog:

def riskyCodeInvoked(input: String): Int = ???

def anotherRiskyMethod(firstOutput: Int): String = ???

def yetAnotherRiskyMethod(secondOutput: String): Try[String] = ???

val result: Try[String] = Try(riskyCodeInvoked("Exception Expected in certain cases"))
  .map(anotherRiskyMethod(_))
  .flatMap(yetAnotherRiskyMethod(_))

result match {
  case Success(res) => info("Operation Was successful")
  case Failure(ex: ArithmeticException) => error("ArithmeticException occurred", ex)
  case Failure(ex) => error("Some Exception occurred", ex)
}

BTW, IMO, Option is no need here?