Using for-comprehension, Try and sequences in Scal

2019-02-16 12:51发布

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.

3条回答
淡お忘
2楼-- · 2019-02-16 13:33

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.

查看更多
SAY GOODBYE
3楼-- · 2019-02-16 13:41

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)
查看更多
ら.Afraid
4楼-- · 2019-02-16 13:41

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.

查看更多
登录 后发表回答