2013-03-19 17 views
13

Gibt es eine einfache Möglichkeit, eine Sammlung von Versuchen zu verflachen, um entweder einen Erfolg der Versuchswerte oder nur den Fehler zu geben? Zum Beispiel:Flatten Scala Try

def map(l:List[Int]) = l map { 
    case 4 => Failure(new Exception("failed")) 
    case i => Success(i) 
} 

val l1 = List(1,2,3,4,5,6) 
val result1 = something(map(l1)) 

result1: Failure(Exception("failed")) 

val l2 = List(1,2,3,5,6) 
val result2 = something(map(l2)) 

result2: Try(List(1,2,3,5,6)) 

Und kann wie würden Sie mehrere Fehler in der Sammlung behandeln?

+3

Bitte sagen, wie Ihr Ergebnis aussehen sollte. 'Liste (1, 2, 3, Ausnahme, 5, 7)'? –

+1

Scalaz würde diese Operation 'sequence' nennen, aber es fehlt leider in der Standardbibliothek. – Impredicative

Antwort

5

Vielleicht nicht ganz so einfach, wie man sich erhofft, aber das funktioniert:

def flatten[T](xs: Seq[Try[T]]): Try[Seq[T]] = { 
    val (ss: Seq[Success[T]]@unchecked, fs: Seq[Failure[T]]@unchecked) = 
    xs.partition(_.isSuccess) 

    if (fs.isEmpty) Success(ss map (_.get)) 
    else Failure[Seq[T]](fs(0).exception) // Only keep the first failure 
} 

val xs = List(1,2,3,4,5,6) 
val ys = List(1,2,3,5,6) 

println(flatten(map(xs))) // Failure(java.lang.Exception: failed) 
println(flatten(map(ys))) // Success(List(1, 2, 3, 5, 6)) 

Beachten Sie, dass die Verwendung von partition ist nicht als Typ sicher wie es wird, wie durch die @unchecked Anmerkungen erlebt. In dieser Hinsicht wäre ein foldLeft, der zwei Sequenzen Seq[Success[T]] und Seq[Failure[T]] akkumuliert, besser.

Wenn Sie alle Fehler halten wollen, können Sie diese verwenden:

def flatten2[T](xs: Seq[Try[T]]): Either[Seq[T], Seq[Throwable]] = { 
    val (ss: Seq[Success[T]]@unchecked, fs: Seq[Failure[T]]@unchecked) = 
    xs.partition(_.isSuccess) 

    if (fs.isEmpty) Left(ss map (_.get)) 
    else Right(fs map (_.exception)) 
} 

val zs = List(1,4,2,3,4,5,6) 

println(flatten2(map(xs))) // Right(List(java.lang.Exception: failed)) 
println(flatten2(map(ys))) // Left(List(1, 2, 3, 5, 6)) 
println(flatten2(map(zs))) // Right(List(java.lang.Exception: failed, 
          //   java.lang.Exception: failed)) 
+0

+1 "In dieser Hinsicht wäre ein foldLeft, das zwei Sequenzen Seq [Erfolg [T]] und Seq [Fehler [T]] sammelt, besser." Ja! Es gibt so viele Validierungs- oder Fehlertypen im Scala-Ökosystem (Option, entweder, Try, scalaz.Disjunction, scalaz.Validation, ...), und trotzdem konnte ich keine solchen doppelten seq-Beispiele unter den öffentlichen Methoden finden übliche Muster. – rloth

9

Etwas weniger ausführlich, und Art sicher:

def sequence[T](xs : Seq[Try[T]]) : Try[Seq[T]] = (Try(Seq[T]()) /: xs) { 
    (a, b) => a flatMap (c => b map (d => c :+ d)) 
} 

Ergebnisse:

sequence(l1) 

res8: scala.util.Try [Seq [Int]] = Fehler (java.lang.Exception: failed)

sequence(l2) 

res9: scala.util.Try [Seq [Int]] = Erfolg (List (1, 2, 3, 5, 6))

+0

Es sieht gut aus, obwohl es mit der Append-Operation nicht zu einem Leistungseinbruch kommen wird? –

+1

Messepunkt - Ich hatte vergessen, dass der Standard 'Seq' eine' List' ist. Fügen Sie das Gegenteil hinzu/verwenden Sie einen Vektor nach Ihrem Ermessen! – Impredicative

+0

Schön, ich habe nicht gesehen, dass foldLeft Abkürzung vor '/:' – Stephen

3

Werfen Sie einen Blick auf die Liftweb Box Monade. Mit Hilfe der Konstruktorfunktion tryo erhalten Sie genau die Abstraktion, nach der Sie suchen.

Mit tryo können Sie eine Funktion in eine Box heben. Die Box enthält dann entweder das Ergebnis der Funktion oder es enthält einen Fehler. Sie können dann mit den üblichen monadischen Hilfsfunktionen (flatMap, Filter, etc.) auf die Box zugreifen, ohne zu stören, wenn die Box einen Fehler enthält oder das Ergebnis der Funktion.

Beispiel:

import net.liftweb.util.Helpers.tryo 

List("1", "2", "not_a_number") map (x => tryo(x.toInt)) map (_ map (_ + 1)) 

Ergebnisse zu

List[net.liftweb.common.Box[Int]] = 
    List(
    Full(2), 
    Full(3), 
    Failure(For input string: "not_a_number",Full(java.lang.NumberFormatException: For input string: "not_a_number"),Empty) 
) 

Sie können die fehlerhaften Werte mit flatMap

List("1", "2", "not_a_number") map (x => tryo(x.toInt)) flatMap (_ map (_ + 1)) 

Ergebnisse überspringen

List[Int] = List(2, 3) 

Es gibt mehrere andere Hilfsmethoden, z.B. zum Kombinieren von Boxen (beim Verketten von Fehlermeldungen). Einen guten Überblick finden Sie hier: Box Cheat Sheet for Lift

Sie können es alleine verwenden, ohne das gesamte Aufzugsgerüst verwenden zu müssen.Für die Beispiele oben verwendete ich die follwing sbt Skript:

scalaVersion := "2.9.1" 

libraryDependencies += "net.liftweb" %% "lift-common" % "2.5-RC2" 

libraryDependencies += "net.liftweb" %% "lift-util" % "2.5-RC2" 
6

Als Ergänzung zu imprädikative Antwort und kommentieren, wenn Sie sowohl scalaz-seven und scalaz-contrib/scala210 in Ihrer Abhängigkeiten:

> scala210/console 
[warn] Credentials file /home/folone/.ivy2/.credentials does not exist 
[info] Starting scala interpreter... 
[info] 
Welcome to Scala version 2.10.0 (OpenJDK 64-Bit Server VM, Java 1.7.0_17). 
Type in expressions to have them evaluated. 
Type :help for more information. 

scala> import scala.util._ 
import scala.util._ 

scala> def map(l:List[Int]): List[Try[Int]] = l map { 
    | case 4 => Failure(new Exception("failed")) 
    | case i => Success(i) 
    | } 
map: (l: List[Int])List[scala.util.Try[Int]] 

scala> import scalaz._, Scalaz._ 
import scalaz._ 
import Scalaz._ 

scala> import scalaz.contrib.std.utilTry._ 
import scalaz.contrib.std.utilTry._ 

scala> val l1 = List(1,2,3,4,5,6) 
l1: List[Int] = List(1, 2, 3, 4, 5, 6) 

scala> map(l1).sequence 
res2: scala.util.Try[List[Int]] = Failure(java.lang.Exception: failed) 

scala> val l2 = List(1,2,3,5,6) 
l2: List[Int] = List(1, 2, 3, 5, 6) 

scala> map(l2).sequence 
res3: scala.util.Try[List[Int]] = Success(List(1, 2, 3, 5, 6)) 

Sie benötigen scalaz um eine instance für die List (versteckt in der MonadPlus Instanz), um die sequence Methode zu erhalten. Sie benötigen scalaz-contrib für die TraverseinstanceTry, die von der Typensignatur sequence erforderlich ist. Try lebt außerhalb von Scalaz, da es nur in Scala 2.10 erschien, und scalaz zielt auf Cross-Compilierung zu früheren Versionen ab.

24

Das ist ziemlich nah an minimal für ausfall erste Operation:

def something[A](xs: Seq[Try[A]]) = 
    Try(xs.map(_.get)) 

(bis zu dem Punkt, wo man nicht stören soll, ein Verfahren zu schaffen, nur Try verwenden). Wenn Sie alle Fehler wollen, ist eine Methode sinnvoll; Ich würde verwenden eine Either:

def something[A](xs: Seq[Try[A]]) = 
    Try(Right(xs.map(_.get))). 
    getOrElse(Left(xs.collect{ case Failure(t) => t })) 
+0

Nizza! (Obwohl die Exception erneut ausgelöst wird, falls vorhanden.) Wie würden Sie sie so erweitern, dass alle Exceptions beibehalten werden? –

+0

@mhs - Wenn ein Fehler nicht ungewöhnlich ist oder die Leistung nicht kritisch ist, möchten Sie den Re-Throw, da es der einfachste Weg ist, mit dem Problem umzugehen. Der Stack-Trace der Exception wurde bereits generiert, daher ist der throw/catch selbst nicht zu teuer. Wenn ich alle Ausnahmen behalten möchte, packe ich sie stattdessen in ein "Entweder", z. wie in meiner Bearbeitung. –

0

sind meine 2cents sind:

def sequence[A, M[_] <: TraversableOnce[_]](in: M[Try[A]]) 
    (implicit cbf:CanBuildFrom[M[Try[A]], A, M[A]]): Try[M[A]] = { 
    in.foldLeft(Try(cbf(in))) { 
     (txs, tx) => 
     for { 
      xs <- txs 
      x <- tx.asInstanceOf[Try[A]] 
     } yield { 
      xs += x 
     } 
    }.map(_.result()) 
    }