jonderry
jonderry

Reputation: 23633

How to best implement "first success" in Scala (i.e., return the first success from a sequence of failure-prone operations)

Per the title, there are a couple of reasonable and idiomatic ways that I know of to return the first successful computation, though I'm most interested here in how to handle the case when we want to know the specific failure of the last attempt when all attempts fail. As a first attempt, we can use collectFirst and do something like the following:

  def main(args: Array[String]) {
    val xs = (1 to 5)
    def check(i: Int): Try[Int] = {
      println(s"checking: $i")
      Try(if (i < 3) throw new RuntimeException(s"small: $i") else i)
    }
    val z = xs.collectFirst { i => check(i) match { case s @ Success(x) => s } }
    println(s"final val: $z")
  }

This seems like a reasonable solution if we don't care about the failures (actually, since we're always returning a success, we never return a Failure, only a None in the case there is no successful computation).

On the other hand, to handle the case when all attempts fail, we can capture the last failure by using the following:

  def main2(args: Array[String]) {
    val xs = (1 to 5)
    def check(i: Int): Try[Int] = {
      println(s"checking: $i")
      Try(if (i < 3) throw new RuntimeException(s"small: $i") else i)
    }
    val empty: Try[Int] = Failure(new RuntimeException("empty"))
    val z = xs.foldLeft(empty)((e, i) => e.recoverWith { case _ => check(i) })
    println(s"final val: $z")
  }

The disadvantages here are that you create a "fake" Throwable representing empty, and if the list is very long, we iterate over the whole list, even though we may have succeeded very early on, even if later iterations are essentially no-ops.

Is there a better way to implement main2 that is idiomatic and doesn't suffer from the aforementioned disadvantages?

Upvotes: 4

Views: 519

Answers (2)

som-snytt
som-snytt

Reputation: 39577

There's a previous answer:

https://stackoverflow.com/a/20665337/1296806

with the caveat that your question asks for the last failure, if all have failed.

I guess that's why this isn't a duplicate?

That's trivial to add to the code from that answer:

def bad(f: Failure) = if (count.decrementAndGet == 0) { p tryComplete new Failure(new RuntimeException("All bad", f.exception)) }

or more simply

p tryComplete f

Upvotes: 1

Noah
Noah

Reputation: 13959

You could do something like this:

  @tailrec
  def collectFirstOrFailure[T](l: List[T], f: T => Try[T]): Try[T] = {
    l match {
      case h :: Nil =>  f(h)
      case h :: t =>  // f(h) orElse collectFirstOrFailure(t, f) //wish I could do this but not tailrec approved!
        val res = f(h)
        if (res.isFailure){
          collectFirstOrFailure(t, f)
        }
        else {
          res
        }
      case Nil => Failure(new RuntimeException("empty"))
    }
  }

  val y = collectFirstOrFailure(xs.toList, check)
  println(s"final val: $y")

This isn't very pretty, and we do still have to handle the empty list case, but we're not creating a new Failure(new RuntimeException("empty")) with every run (unless it's an empty list) and we stop short if there's a success. I feel like scalaz has some better way to do this but I can't figure it out right now. The returning the last failure requirement is making this a bit complex.

UPDATE

There's always iterator...

  def collectFirstOrFailureI[T](i: Iterator[T], f: T => Try[T]): Try[T] = {
    while (i.hasNext){
      val res = f(i.next())
      if (res.isSuccess || !i.hasNext){
        return res
      }
    }
    Failure(new RuntimeException("empty"))
  }

  xs.toIterator

  val x = collectFirstOrFailureI(xs.iterator, check)
  println(s"final val: $x")

Upvotes: 2

Related Questions