Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Chaining scala Try instances that contain Options

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.

like image 314
Spencer Stejskal Avatar asked Dec 19 '13 23:12

Spencer Stejskal


2 Answers

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)
like image 194
stew Avatar answered Sep 27 '22 23:09

stew


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
}
like image 28
Rex Kerr Avatar answered Sep 27 '22 21:09

Rex Kerr