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.
quelle
Try
. Der Teil überEither
vs gibtException
lediglich an, dassEither
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 einesEither
. Scala'sEither
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 vonEither
, sondern eine Folge davon, dass sie voreingenommen ist.Antworten:
Wenn Sie nur einen
Either
genau wie ein Imperativ-try-catch
Block verwenden, sieht es natürlich nach einer anderen Syntax aus, um genau dasselbe zu tun.Eithers
sind ein Wert . Sie haben nicht die gleichen Einschränkungen. Du kannst:Eithers
muss nicht direkt neben dem Teil "try" stehen. Es kann sogar in einer gemeinsamen Funktion geteilt werden. Dies erleichtert die ordnungsgemäße Trennung von Anliegen erheblich.Eithers
in Datenstrukturen. Sie können sie durchlaufen und Fehlermeldungen konsolidieren, die erste finden, die nicht fehlgeschlagen ist, sie faul auswerten oder Ähnliches.Eithers
? Sie können Hilfsfunktionen schreiben, um die Arbeit mit ihnen für Ihre speziellen Anwendungsfälle zu vereinfachen. Im Gegensatz zu Try-Catch bleibt man nicht permanent bei der Standardoberfläche, die die Sprachentwickler erstellt haben.Beachten Sie, dass in den meisten Anwendungsfällen
Try
eine einfachere Benutzeroberfläche zur Verfügung steht, die meisten der oben genannten Vorteile bietet und in der Regel genau so gut Ihren Anforderungen entspricht. EineOption
ist sogar einfacher , wenn alles , was Sie über Pflege Erfolg oder Misserfolg ist und braucht keine Zustandsinformationen wie Fehlermeldungen über den Fehlerfall.Nach meiner Erfahrung
Eithers
werden sie nicht wirklich bevorzugt, esTrys
sei denn, esLeft
handelt sich um eine andere alsException
eine Validierungsfehlermeldung, und Sie möchten dieLeft
Werte aggregieren . Beispielsweise verarbeiten Sie einige Eingaben und möchten alle Fehlermeldungen erfassen , nicht nur die ersten. Diese Situationen tauchen, zumindest für mich, in der Praxis nicht sehr oft auf.Schauen Sie über das Try-Catch-Modell hinaus und Sie werden feststellen, dass es
Eithers
viel angenehmer ist, mit ihm zu arbeiten.Update: Diese Frage erhält immer noch Aufmerksamkeit und ich hatte nicht gesehen, dass das Update des OP die Syntaxfrage klarstellte, also dachte ich, ich würde mehr hinzufügen.
Als Beispiel für den Ausbruch aus der Ausnahmebedingung schreibe ich normalerweise Ihren Beispielcode:
Hier können Sie sehen, dass die Semantik
filterOrElse
perfekt erfasst, was wir hauptsächlich in dieser Funktion tun: Überprüfen von Bedingungen und Verknüpfen von Fehlermeldungen mit den angegebenen Fehlern. Da dies ein sehr häufiger Anwendungsfall ist,filterOrElse
befindet es sich in der Standardbibliothek, aber wenn dies nicht der Fall wäre, könnten Sie es erstellen.Dies ist der Punkt, an dem jemand ein Gegenbeispiel findet, das nicht genau so gelöst werden kann wie ich, aber der Punkt, den ich anstrebe, ist, dass es im
Eithers
Gegensatz zu Ausnahmen nicht nur einen Weg gibt, es zu verwenden . Es gibt Möglichkeiten, den Code für die meisten Fehlerbehandlungssituationen zu vereinfachen, wenn Sie alle zur Verfügung stehenden Funktionen kennenlernenEithers
und experimentieren möchten.quelle
try
und zu trennen,catch
ist ein faires Argument, aber könnte dies nicht einfach erreicht werdenTry
? 2. Das Speichern vonEither
s in Datenstrukturen könnte neben demthrows
Mechanismus erfolgen. Ist es nicht einfach eine zusätzliche Schicht über die Behandlung von Fehlern? 3. Sie könnenException
s definitiv erweitern . Sicher, Sie können dietry-catch
Struktur nicht ändern , aber das Behandeln von Fehlern ist so häufig, dass ich definitiv möchte, dass die Sprache mir ein Boilerplate dafür gibt (das ich nicht verwenden muss). Das ist so, als würde man sagen, dass Vorverständnis schlecht ist, weil es eine Kesselplatte für monadische Operationen ist.Either
s erweitert werden kann, wo dies eindeutig nicht möglich ist (dasealed trait
nur zwei Implementierungen vorhanden sind, beidefinal
). Können Sie das näher erläutern? Ich gehe davon aus, dass Sie nicht die wörtliche Bedeutung der Erweiterung gemeint haben.Die beiden sind eigentlich sehr unterschiedlich.
Either
Die Semantik von ist viel allgemeiner als nur die Darstellung potenziell fehlgeschlagener Berechnungen.Either
Damit können Sie einen von zwei verschiedenen Typen zurückgeben. Das ist es.Nun, eine dieser beiden Arten kann oder kann keinen Fehler dar und das andere Mai oder nicht Erfolg darstellen, aber das ist nur eine mögliche Verwendung bei vielen.
Either
ist viel, viel allgemeiner. Sie können auch eine Methode verwenden, die Eingaben des Benutzers liest, der entweder einen Namen eingeben oder ein Datum zurückgeben darfEither[String, Date]
.Oder denken Sie an Lambda-Literale in C♯: Sie werden entweder zu einer
Func
oder einer ausgewertetExpression
, dh sie werden entweder zu einer anonymen Funktion ausgewertet oder sie werden zu einer AST ausgewertet. In C♯ geschieht dies "magisch", aber wenn Sie ihm einen richtigen Typ geben möchten, ist dies der FallEither<Func<T1, T2, …, R>, Expression<T1, T2, …, R>>
. Keine der beiden Optionen ist jedoch ein Fehler, und keine der beiden Optionen ist entweder "normal" oder "außergewöhnlich". Es handelt sich lediglich um zwei verschiedene Typen, die von derselben Funktion zurückgegeben werden können (oder in diesem Fall demselben Literal zugeschrieben werden). [Anmerkung: Muss eigentlichFunc
in zwei weitere Fälle aufgeteilt werden, da C♯ keinenUnit
Typ hat und etwas, das nichts zurückgibt, nicht so dargestellt werden kann wie etwas, das etwas zurückgibt.Either<Either<Func<T1, T2, …, R>, Action<T1, T2, …>>, Expression<T1, T2, …, R>>
Nun
Either
kann verwendet werden , um einen Erfolg Typen darstellen und eine Fehlertyp. Und wenn Sie auf eine einheitliche Reihenfolge der beiden Arten einverstanden sind , können Sie sogar BiasEither
eine der beiden Arten zu bevorzugen, was es möglich macht Ausfälle durch monadische Verkettungs zu propagieren. Aber in diesem Fall vermitteln Sie eine bestimmte SemantikEither
, die sie nicht unbedingt für sich besitzt.Ein
Either
, bei dem einer seiner beiden Typen als Fehlertyp festgelegt und zur Fehlerausbreitung auf eine Seite vorgespannt ist, wird normalerweise alsError
Monade bezeichnet und ist die bessere Wahl, um eine möglicherweise fehlgeschlagene Berechnung darzustellen. In Scala wird diese Art von Typ genanntTry
.Es ist viel sinnvoller, die Verwendung von Ausnahmen mit
Try
als mit zu vergleichenEither
.Dies ist also der Grund Nr. 1, warum
Either
über geprüften Ausnahmen verwendet werden könnte:Either
ist viel allgemeiner als Ausnahmen. Es kann in Fällen verwendet werden, in denen Ausnahmen nicht einmal zutreffen.Sie haben sich jedoch höchstwahrscheinlich nach dem eingeschränkten Fall erkundigt, in dem Sie nur
Either
(oder wahrscheinlicherTry
) Ausnahmen ersetzen. In diesem Fall gibt es immer noch einige Vorteile oder eher unterschiedliche Ansätze.Der Hauptvorteil ist, dass Sie die Behandlung des Fehlers verschieben können. Sie speichern einfach den Rückgabewert, ohne zu prüfen, ob es sich um einen Erfolg oder einen Fehler handelt. (Behandle es später.) Oder gib es woanders hin. (Lassen Sie andere sich darüber Gedanken machen.) Sammeln Sie sie in einer Liste. (Behandeln Sie alle auf einmal.) Sie können monadische Verkettung verwenden, um sie entlang einer Verarbeitungspipeline zu verbreiten.
Die Semantik von Ausnahmen ist fest in die Sprache eingebunden. In Scala lösen beispielsweise Ausnahmen den Stapel auf. Wenn Sie zulassen, dass sie zu einem Ausnahmehandler hochsprudeln, kann der Handler nicht dorthin zurückkehren, wo es passiert ist, und es beheben. OTOH, wenn Sie es tiefer in den Stapel legen, bevor Sie es abwickeln und den notwendigen Kontext zerstören, um es zu reparieren, befinden Sie sich möglicherweise in einer Komponente, die zu niedrig ist, um eine fundierte Entscheidung über das weitere Vorgehen zu treffen.
Dies ist beispielsweise in Smalltalk anders, wo Ausnahmen den Stapel nicht abwickeln und wiederaufnehmbar sind und Sie die Ausnahme hoch im Stapel abfangen können (möglicherweise so hoch wie der Debugger), beheben Sie den Fehler waaaaayyyyy unten im stapeln und die Ausführung von dort fortsetzen. Oder CommonLisp-Bedingungen, bei denen Heben, Fangen und Behandeln drei verschiedene Dinge sind, anstatt wie bei Ausnahmen zwei (Heben und Behandeln mit Fangen).
Durch die Verwendung eines tatsächlichen Fehlerrückgabetyps anstelle einer Ausnahme können Sie ähnliche und weitere Aktionen ausführen, ohne die Sprache ändern zu müssen.
Und dann ist da noch der offensichtliche Elefant im Raum: Ausnahmen sind ein Nebeneffekt. Nebenwirkungen sind böse. Hinweis: Ich sage nicht, dass dies unbedingt wahr ist. Es ist jedoch ein Gesichtspunkt, den einige Leute haben, und in diesem Fall liegt der Vorteil eines Fehlertyps gegenüber Ausnahmen auf der Hand. Wenn Sie also eine funktionale Programmierung durchführen möchten, können Sie Fehlertypen nur deshalb verwenden, weil dies der funktionale Ansatz ist. (Beachten Sie, dass Haskell Ausnahmen hat. Beachten Sie auch, dass niemand sie gerne verwendet.)
quelle
Exception
s verzichten soll .Either
scheint ein großartiges Werkzeug zu sein, aber ich frage speziell nach der Behandlung von Fehlern, und ich würde lieber ein viel spezifischeres als ein allmächtiges Werkzeug für meine Aufgabe verwenden. Das Aufschieben der Fehlerbehandlung ist ein faires Argument, aber man kann es einfachTry
auf eine Methode anwenden, die eine auslöst,Exception
wenn er im Moment nicht damit umgehen möchte. Nicht Stack-Trace Abwickeln AffektEither
/Try
als auch? Sie können dieselben Fehler wiederholen, wenn Sie sie falsch verbreiten. Zu wissen, wann ein Fehler behoben werden muss, ist in beiden Fällen nicht trivial.Das Schöne an Ausnahmen ist, dass der Ursprungscode den Ausnahmefall für Sie bereits klassifiziert hat. Der Unterschied zwischen an
IllegalStateException
und aBadParameterException
ist in stärker typisierten Sprachen viel einfacher zu unterscheiden. Wenn Sie versuchen, "gut" und "schlecht" zu analysieren, nutzen Sie nicht das äußerst leistungsfähige Tool, das Ihnen zur Verfügung steht, nämlich den Scala-Compiler. Ihre eigenen ErweiterungenThrowable
können neben der Textnachrichtenzeichenfolge so viele Informationen enthalten, wie Sie benötigen.Dies ist der wahre Nutzen
Try
in der Scala-Umgebung, daThrowable
er als Umhüllung für zurückgelassene Gegenstände dient, um das Leben zu erleichtern.quelle
Either
hat Stilprobleme, weil es keine echte Monade ist (?); Die Beliebigkeit desleft
Wertes weicht von der höheren Wertschöpfung ab, wenn Sie Informationen über außergewöhnliche Umstände ordnungsgemäß in eine geeignete Struktur einbinden. Ein Vergleich der Verwendung vonEither
Zeichenfolgen , die in einem Fall auf der linken Seite zurückgegeben werden,try/catch
mit der Verwendung von richtig getippten Zeichenfolgen in einem anderen FallThrowables
ist daher nicht richtig. Aufgrund des WertsThrowables
für die Bereitstellung von Informationen und der prägnanten Art und Weise, dieTry
behandelt wirdThrowables
,Try
ist eine bessereEither
try/catch
try-catch
. Wie in der Frage gesagt, sieht die letztere für mich viel sauberer aus, und der Code, der den Fehler behandelt (entweder Pattern-Matching auf Either oder Try-Catch), ist in beiden Fällen ziemlich klar.