Using for-comprehension, Try and sequences in Scala

almendar picture almendar · Apr 24, 2014 · Viewed 7.2k times · Source

Let's say you've got a bunch of methods:

def foo() : Try[Seq[String]]
def bar(s:String) : Try[String]

and you want to make a for-comprhension:

for {
  list <- foo
  item <- list
  result <- bar(item)
} yield result

of course this won't compile since Seq cannot be used with Try in this context.

Anyone has a nice solution how to write this clean without breaking it into separate two for's?

I've came across this syntax problem for the thirds time and thought that it's about time to ask about this.

Answer

Yuriy picture Yuriy · Apr 24, 2014

IMHO: Try and Seq is more than what you need to define a monad transformer:

Code for library:

case class trySeq[R](run : Try[Seq[R]]) {
  def map[B](f : R => B): trySeq[B] = trySeq(run map { _ map f })
  def flatMap[B](f : R => trySeq[B]): trySeq[B] = trySeq {
    run match {
      case Success(s) => sequence(s map f map { _.run }).map { _.flatten }
      case Failure(e) => Failure(e)
    }
  }

  def sequence[R](seq : Seq[Try[R]]): Try[Seq[R]] = {
    seq match {
      case Success(h) :: tail =>
        tail.foldLeft(Try(h :: Nil)) {
          case (Success(acc), Success(elem)) => Success(elem :: acc)
          case (e : Failure[R], _) => e
          case (_, Failure(e)) => Failure(e)
        }
      case Failure(e) :: _  => Failure(e)
      case Nil => Try { Nil }
    }
  }
}

object trySeq {
  def withTry[R](run : Seq[R]): trySeq[R] = new trySeq(Try { run })
  def withSeq[R](run : Try[R]): trySeq[R] = new trySeq(run map (_ :: Nil))

  implicit def toTrySeqT[R](run : Try[Seq[R]]) = trySeq(run)
  implicit def fromTrySeqT[R](trySeqT : trySeq[R]) = trySeqT.run
} 

and after you can use for-comrehension (just import your library):

def foo : Try[Seq[String]] = Try { List("hello", "world") } 
def bar(s : String) : Try[String] = Try { s + "! " }

val x = for {
  item1  <- trySeq { foo }
  item2  <- trySeq { foo }
  result <- trySeq.withSeq { bar(item2) }
} yield item1 + result

println(x.run)

and it works for:

def foo() = Try { List("hello", throw new IllegalArgumentException()) } 
// x = Failure(java.lang.IllegalArgumentException)