Vor nicht allzu langer Zeit habe ich angefangen, Scala anstelle von Java zu verwenden. Ein Teil des "Konvertierungs" -Prozesses zwischen den Sprachen bestand für mich darin, zu lernen, Either
s anstelle von (angekreuzten) Exception
s zu verwenden. Ich habe eine Weile auf diese Weise programmiert, aber vor kurzem habe ich mich gefragt, ob das wirklich ein besserer Weg ist.
Ein großer Vorteil Either
gegenüber Exception
ist die bessere Leistung; ein Exception
muss einen Stack-Trace erstellen und wird geworfen. Nach meinem Verständnis ist das Werfen von Exception
zwar nicht der anspruchsvolle Teil, aber das Erstellen des Stack-Trace.
Aber dann kann man Exception
s immer konstruieren / erben scala.util.control.NoStackTrace
, und noch mehr sehe ich viele Fälle, in denen die linke Seite eines Either
tatsächlich ein Exception
(Verzicht auf den Leistungsschub) ist.
Ein weiterer Vorteil Either
ist die Compilersicherheit. Der Scala-Compiler beschwert sich nicht über nicht behandelte Exception
s (im Gegensatz zum Java-Compiler). Aber wenn ich mich nicht irre, ist diese Entscheidung mit der gleichen Begründung begründet, die in diesem Thema diskutiert wird, also ...
In Bezug auf die Syntax Exception
denke ich, dass -style viel klarer ist. Untersuchen Sie die folgenden Codeblöcke (beide erzielen dieselbe Funktionalität):
Either
Stil:
def compute(): Either[String, Int] = {
val aEither: Either[String, String] = if (someCondition) Right("good") else Left("bad")
val bEithers: Iterable[Either[String, Int]] = someSeq.map {
item => if (someCondition(item)) Right(item.toInt) else Left("bad")
}
for {
a <- aEither.right
bs <- reduce(bEithers).right
ignore <- validate(bs).right
} yield compute(a, bs)
}
def reduce[A,B](eithers: Iterable[Either[A,B]]): Either[A, Iterable[B]] = ??? // utility code
def validate(bs: Iterable[Int]): Either[String, Unit] = if (bs.sum > 22) Left("bad") else Right()
def compute(a: String, bs: Iterable[Int]): Int = ???
Exception
Stil:
@throws(classOf[ComputationException])
def compute(): Int = {
val a = if (someCondition) "good" else throw new ComputationException("bad")
val bs = someSeq.map {
item => if (someCondition(item)) item.toInt else throw new ComputationException("bad")
}
if (bs.sum > 22) throw new ComputationException("bad")
compute(a, bs)
}
def compute(a: String, bs: Iterable[Int]): Int = ???
Letzteres sieht für mich viel sauberer aus, und der Code, der den Fehler behandelt (entweder Pattern-Matching an Either
oder try-catch
), ist in beiden Fällen ziemlich klar.
Also meine Frage ist - warum Either
über (überprüft) Exception
?
Aktualisieren
Nachdem ich die Antworten gelesen hatte, stellte ich fest, dass ich den Kern meines Dilemmas möglicherweise nicht dargestellt hatte. Mein Anliegen ist nicht das Fehlen der try-catch
; kann man entweder „fängt“ ein Exception
mit Try
, oder die Verwendung catch
mit der Ausnahme , umwickeln Left
.
Mein Hauptproblem mit Either
/ Try
entsteht, wenn ich Code schreibe, der an vielen Stellen auf dem Weg fehlschlagen kann. Wenn in diesen Szenarien ein Fehler auftritt, muss ich diesen Fehler auf den gesamten Code übertragen, wodurch der Code umständlicher wird (wie in den oben genannten Beispielen gezeigt).
Es gibt tatsächlich eine andere Möglichkeit, den Code ohne Exception
s zu knacken return
(was in der Tat ein weiteres "Tabu" in Scala ist). Der Code wäre immer noch klarer als der Either
Ansatz, und obwohl er etwas weniger klar als der Exception
Stil ist, würde es keine Angst vor nicht gefangenen Exception
s geben.
def compute(): Either[String, Int] = {
val a = if (someCondition) "good" else return Left("bad")
val bs: Iterable[Int] = someSeq.map {
item => if (someCondition(item)) item.toInt else return Left("bad")
}
if (bs.sum > 22) return Left("bad")
val c = computeC(bs).rightOrReturn(return _)
Right(computeAll(a, bs, c))
}
def computeC(bs: Iterable[Int]): Either[String, Int] = ???
def computeAll(a: String, bs: Iterable[Int], c: Int): Int = ???
implicit class ConvertEither[L, R](either: Either[L, R]) {
def rightOrReturn(f: (Left[L, R]) => R): R = either match {
case Right(r) => r
case Left(l) => f(Left(l))
}
}
Grundsätzlich sind die return Left
Ersetzungen throw new Exception
und die implizite Methode für beide rightOrReturn
eine Ergänzung für die automatische Weitergabe von Ausnahmen im Stapel.
Try
. Der Teil über Either
vs gibt Exception
lediglich an, dass Either
s verwendet werden sollte, wenn der andere Fall der Methode "nicht außergewöhnlich" ist. Erstens ist dies eine sehr, sehr vage Definition imho. Zweitens, ist es die Syntaxstrafe wirklich wert? Ich meine, es würde mir wirklich nichts ausmachen, Either
s zu verwenden, wenn es nicht den Syntax-Overhead gäbe, den sie darstellen.
Either
sieht für mich wie eine Monade aus. Verwenden Sie es, wenn Sie die Vorteile einer funktionellen Zusammensetzung benötigen, die Monaden bieten. Oder vielleicht auch nicht .
Either
an sich keine Monade. Die Projektion auf die linke oder die rechte Seite ist eine Monade, Either
an sich jedoch nicht. Sie können machen es eine Monade, durch „Vorspannen“ , um es entweder der linken oder der rechten Seite, though. Dann vermitteln Sie jedoch eine bestimmte Semantik auf beiden Seiten eines Either
. Scala's Either
war ursprünglich unvoreingenommen, war aber vor kurzem voreingenommen, so dass es heutzutage tatsächlich eine Monade ist, aber die "Monadität" ist keine inhärente Eigenschaft von Either
, sondern eine Folge davon, dass sie voreingenommen ist.