Scala `map`, но рано завершается при `сбое`

#scala

#scala

Вопрос:

Если у меня есть Seq , я могу map изменить его.

 val ss = Seq("1", "2", "3")
println(ss.map(s => s.toInt))  // List(1, 2, 3)
  

Но иногда функция, которую вы передаете map , может завершиться сбоем.

 val ss = Seq("1", "2", "c")
println(ss.map(s => try { Success(s.toInt) } catch { case e: Throwable  => Failure(e) }))  // List(Success(1), Success(2), Failure(java.lang.NumberFormatException: For input string: "c"))
  

Этот последний вернет Seq[Try[Int]] . Однако чего я действительно хочу, так это a Try[Seq[Int]] , где, если какое-либо из отображений является a Failure , оно останавливает итерацию и вместо этого возвращает Failure . Если ошибки нет, я хочу, чтобы он просто вернул все преобразованные элементы, распакованные из Try .

Каков идиоматический способ Scala сделать это?

Ответ №1:

Возможно, вы слишком много думаете об этом. Анонимная функция в вашем map по сути такая же, как Try.apply . Если вы хотите в конечном итоге получить Try[Seq[Int]] , то вы можете обернуть Seq в Try.apply и map внутри:

 scala> val ss = Try(Seq("1", "2", "c").map(_.toInt))
ss: scala.util.Try[Seq[Int]] = Failure(java.lang.NumberFormatException: For input string: "c")
  

Если какой-либо из toInt -х завершится с ошибкой, он выдаст исключение, остановит выполнение и станет Failure .

Комментарии:

1. Ах, это круто, спасибо. Но что, если бы вместо _.toInt я использовал функцию, которая возвращала бы Try[B] вместо фактического генерирования исключения? Это тоже сработает? Я новичок в Scala и, похоже, не могу найти apply в документах ^_^;

2. @math4tots Щелкните большие буквы «C» и «O» в scaladoc, чтобы переключаться между классом и сопутствующим объектом: T):scala.util.Try[T]» rel=»nofollow noreferrer»> scala-lang.org/api/current /…

Ответ №2:

Не уверен, что это идиоматично, но я бы сделал что-то вроде этого:

 import util.{Try, Success, Failure}
import collection.mutable.ListBuffer

def toInt(s: String) =
  // Correct usage would be Try(s.toInt)
  try {
    Success(s.toInt)
  } 
  catch { 
    case e: Throwable  => Failure(e)
  }

def convert[A](ss: Seq[String], f: String => Try[A]) = {
  ss.foldLeft(Try(ListBuffer[A]())) { 
    case (a, s) =>
      for {
        xs <- a
        x  <- f(s)
      }
      yield xs :  x
  }.map(_.toSeq)
}

scala> convert(List("1", "2"), toInt)
scala.util.Try[Seq[Int]] = Success(List(1, 2))

scala> convert(List("1", "c"), toInt)
scala.util.Try[Seq[Int]] = Failure(java.lang.NumberFormatException: For input string: "c")
  

Если вы действительно хотите завершить работу рано, вместо пропуска элементов вы можете использовать старую добрую рекурсию:

 def convert[A](ss: Seq[String], f: String => Try[A]) = {

  @annotation.tailrec
  def loop(ss: Seq[String], acc: ListBuffer[A]): Try[Seq[A]] = {
    ss match {
      case h::t =>
        f(h) match {
          case Success(x) => loop(t, acc :  x)
          case Failure(e) => Failure(e)
        }
      case Nil =>
        Success(acc.toSeq)

    }
  }

  loop(ss, ListBuffer[A]())
}
  

Комментарии:

1. Спасибо, вы единственный, кто на самом деле пытается ответить на суть моего вопроса. Я также думал об использовании foldLeft , но я надеялся, что преобразование iterable of tries в try of iterables является распространенным шаблоном в функциональном программировании.

2. Это хорошо известный шаблон в FP, только sequence с Scalaz он не работает Try , поскольку Option это было бы List(1.some, 2.some).sequence //> Option[List[Int]] = Some(List(1, 2)) так.