Используя для понимания, Try и последовательности в Scala

Скажем, у вас есть куча методов:

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

и вы хотите сделать следующее:

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

конечно, это не скомпилируется, поскольку Seq не может использоваться с Try в этом контексте.

У кого-нибудь есть хорошее решение, как написать это чистое, не разбивая его на отдельные два для?

Я столкнулся с этой проблемой синтаксиса в течение трети времени и подумал, что настало время спросить об этом.

Ответ 1

IMHO: Попробовать и Seq больше, чем вам нужно, чтобы определить трансформатор монады:

Код для библиотеки:

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
} 

и после того, как вы сможете использовать for-comrehension (просто импортируйте свою библиотеку):

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)

и работает для:

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

Ответ 2

Вы можете воспользоваться тем, что Try можно преобразовать в Option и Option в 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

Это вернет (возможно, пуст, если сбой Try) Seq.

Если вы хотите сохранить все детали исключения, вам понадобится Try[Seq[Try[String]]]. Это невозможно сделать с помощью сингла для понимания, поэтому лучше всего придерживаться простого map:

foo map {_ map bar}

Если вы хотите смешивать ваши Try и Seq по-другому, все становится странно, так как нет естественного способа сгладить a Try[Seq[Try[String]]]. Ответ @Yury демонстрирует то, что вам нужно сделать.

Или, если вас интересуют только побочные эффекты вашего кода, вы можете просто:

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

Это работает, потому что foreach имеет менее строгую подпись типа.

Ответ 3

Попытка может быть преобразована в Опцию, которую вы можете использовать, чтобы использовать ее для понимания. Например.

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

Первый раз я ввел "w", затем второй раз 1234.