14

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.

Jonik
  • 74,291
  • 66
  • 249
  • 356
almendar
  • 1,793
  • 12
  • 22

3 Answers3

5

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)
Eddy
  • 1,247
  • 1
  • 16
  • 31
Yuriy
  • 2,713
  • 13
  • 22
  • I had a feeling in my guts it has to do with some sort of monad-magic. I need to spend some time learning this stuff. Seem really beneficial. – almendar Apr 28 '14 at 07:56
  • 1
    Perfect answer. The only thing to advice - to move 'sequence' method to companion object – mulya Feb 10 '20 at 08:12
4

You can take advantage of the fact that Try can be converted to Option, and Option to Seq:

for {
  list <- foo.toOption.toSeq // toSeq needed here, as otherwise Option.flatMap will be used, rather than Seq.flatMap
  item <- list
  result <- bar(item).toOption // toSeq not needed here (but allowed), as it is implicitly converted
} yield result

This will return a (possibly empty, if the Trys failed) Seq.

If you want to keep all the exception detail, you'll need a Try[Seq[Try[String]]]. This can't be done with a single for comprehension, so you're best sticking with plain map:

foo map {_ map bar}

If you want to mingle your Trys and Seqs in a different way, things get fiddlier, as there's no natural way to flatten a Try[Seq[Try[String]]]. @Yury's answer demonstrates the sort of thing you'd have to do.

Or, if you're only interested in the side effects of your code, you can just do:

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

This works because foreach has a less restrictive type signature.

James_pic
  • 3,051
  • 16
  • 23
  • 1
    The problem is that I will lose the exception details. What I would like to achieve is to process all elements from the Seq and stop if any will throw an exception. Besides from that thanks for the idea with Option. A nice trick. – almendar Apr 24 '14 at 11:35
  • 1
    I've updated with a little discussion about options that keep exception detail. – James_pic Apr 24 '14 at 11:57
2

A Try can be converted to an Option, which you can than use in a for-comprehension. E.g.

scala> def testIt() = {
     |   val dividend = Try(Console.readLine("Enter an Int that you'd like to divide:\n").toInt)
     |   dividend.toOption
     | }
testIt: ()Option[Int]

scala> for (x <- testIt()) println (x * x)
Enter an Int that you'd like to divide:

scala> for (x <- testIt()) println (x * x)
Enter an Int that you'd like to divide:
1522756

First time I entered "w", then second time 1234.

Haris Osmanagić
  • 1,052
  • 9
  • 22
  • Try can albo be used in a for-comprehension. But it cannot be mixed with SeqLike's. The problem it that there will be type miss match since it will need Try[...] and will find SeqLike. – almendar Apr 24 '14 at 10:12
  • "warning: method readLine in class DeprecatedConsole is deprecated: Use the method in scala.io.StdIn" –  Apr 24 '14 at 10:29