Warum entweder over (checked) Exception verwenden?

17

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, Eithers anstelle von (angekreuzten) Exceptions 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 Eithergegenüber Exceptionist die bessere Leistung; ein Exceptionmuss einen Stack-Trace erstellen und wird geworfen. Nach meinem Verständnis ist das Werfen von Exceptionzwar nicht der anspruchsvolle Teil, aber das Erstellen des Stack-Trace.

Aber dann kann man Exceptions immer konstruieren / erben scala.util.control.NoStackTrace, und noch mehr sehe ich viele Fälle, in denen die linke Seite eines Eithertatsächlich ein Exception(Verzicht auf den Leistungsschub) ist.

Ein weiterer Vorteil Eitherist die Compilersicherheit. Der Scala-Compiler beschwert sich nicht über nicht behandelte Exceptions (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 Exceptiondenke 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 Eitheroder 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 Exceptionmit Try, oder die Verwendung catchmit der Ausnahme , umwickeln Left.

Mein Hauptproblem mit Either/ Tryentsteht, 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 Exceptions zu knacken return(was in der Tat ein weiteres "Tabu" in Scala ist). Der Code wäre immer noch klarer als der EitherAnsatz, und obwohl er etwas weniger klar als der ExceptionStil ist, würde es keine Angst vor nicht gefangenen Exceptions 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 LeftErsetzungen throw new Exceptionund die implizite Methode für beide rightOrReturneine Ergänzung für die automatische Weitergabe von Ausnahmen im Stapel.

Eyal Roth
quelle
Lesen Sie dies: mauricio.github.io/2014/02/17/…
Robert Harvey
@RobertHarvey Scheint, wie die meisten Artikel diskutiert Try. Der Teil über Eithervs gibt Exceptionlediglich an, dass Eithers 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, Eithers zu verwenden, wenn es nicht den Syntax-Overhead gäbe, den sie darstellen.
Eyal Roth
Eithersieht 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 .
Robert Harvey
2
@RobertHarvey: Technisch gesehen ist es Eitheran sich keine Monade. Die Projektion auf die linke oder die rechte Seite ist eine Monade, Eitheran 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 Eitherwar 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.
Jörg W Mittag
@ JörgWMittag: Na gut. Das bedeutet, dass Sie es für all seine monadische Güte verwenden können und sich nicht sonderlich darum kümmern müssen, wie "sauber" der resultierende Code ist (obwohl ich vermute, dass Fortsetzungscode im funktionalen Stil tatsächlich sauberer wäre als die Exception-Version).
Robert Harvey

Antworten:

13

Wenn Sie nur einen Eithergenau wie ein Imperativ- try-catchBlock verwenden, sieht es natürlich nach einer anderen Syntax aus, um genau dasselbe zu tun. Eitherssind ein Wert . Sie haben nicht die gleichen Einschränkungen. Du kannst:

  • Hören Sie auf, Ihre Versuchsblöcke klein und zusammenhängend zu halten. Der Teil "catch" von Eithersmuss 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.
  • Speicher Eithersin Datenstrukturen. Sie können sie durchlaufen und Fehlermeldungen konsolidieren, die erste finden, die nicht fehlgeschlagen ist, sie faul auswerten oder Ähnliches.
  • Erweitern und anpassen. Mögen Sie keine Heizplatte, mit der Sie schreiben müssen 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 Tryeine einfachere Benutzeroberfläche zur Verfügung steht, die meisten der oben genannten Vorteile bietet und in der Regel genau so gut Ihren Anforderungen entspricht. Eine Optionist sogar einfacher , wenn alles , was Sie über Pflege Erfolg oder Misserfolg ist und braucht keine Zustandsinformationen wie Fehlermeldungen über den Fehlerfall.

Nach meiner Erfahrung Eitherswerden sie nicht wirklich bevorzugt, es Tryssei denn, es Lefthandelt sich um eine andere als Exceptioneine Validierungsfehlermeldung, und Sie möchten die LeftWerte 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 Eithersviel 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:

def compute(): Either[String, Int] = {
  Right(someSeq)
    .filterOrElse(someACondition,            "someACondition failed")
    .filterOrElse(_ forall someSeqCondition, "someSeqCondition failed")
    .map(_ map {_.toInt})
    .filterOrElse(_.sum <= 22, "Sum is greater than 22")
    .map(compute("good",_))
}

Hier können Sie sehen, dass die Semantik filterOrElseperfekt 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, filterOrElsebefindet 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 EithersGegensatz 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 kennenlernen Eithersund experimentieren möchten.

Karl Bielefeldt
quelle
1. In der Lage zu sein, das tryund zu trennen, catchist ein faires Argument, aber könnte dies nicht einfach erreicht werden Try? 2. Das Speichern von Eithers in Datenstrukturen könnte neben dem throwsMechanismus erfolgen. Ist es nicht einfach eine zusätzliche Schicht über die Behandlung von Fehlern? 3. Sie können Exceptions definitiv erweitern . Sicher, Sie können die try-catchStruktur 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.
Eyal Roth
Ich habe gerade festgestellt, dass Sie gesagt haben, dass Eithers erweitert werden kann, wo dies eindeutig nicht möglich ist (da sealed traitnur zwei Implementierungen vorhanden sind, beide final). Können Sie das näher erläutern? Ich gehe davon aus, dass Sie nicht die wörtliche Bedeutung der Erweiterung gemeint haben.
Eyal Roth
1
Ich meinte erweitert wie im englischen Wort, nicht das Programmier-Schlüsselwort. Hinzufügen von Funktionen durch Erstellen von Funktionen zum Verarbeiten gängiger Muster.
Karl Bielefeldt
8

Die beiden sind eigentlich sehr unterschiedlich. EitherDie Semantik von ist viel allgemeiner als nur die Darstellung potenziell fehlgeschlagener Berechnungen.

EitherDamit 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. Eitherist 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 darf Either[String, Date].

Oder denken Sie an Lambda-Literale in C♯: Sie werden entweder zu einer Funcoder einer ausgewertet Expression, 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 Fall Either<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 eigentlich Funcin zwei weitere Fälle aufgeteilt werden, da C♯ keinen UnitTyp 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 Bias Either 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 Semantik Either, 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 als ErrorMonade bezeichnet und ist die bessere Wahl, um eine möglicherweise fehlgeschlagene Berechnung darzustellen. In Scala wird diese Art von Typ genannt Try.

Es ist viel sinnvoller, die Verwendung von Ausnahmen mit Tryals mit zu vergleichen Either.

Dies ist also der Grund Nr. 1, warum Eitherüber geprüften Ausnahmen verwendet werden könnte: Eitherist 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 wahrscheinlicher Try) 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.)

Jörg W. Mittag
quelle
Ich liebe die Antwort, obwohl ich immer noch nicht verstehe, warum ich auf Exceptions verzichten soll . Eitherscheint 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 einfach Tryauf eine Methode anwenden, die eine auslöst, Exceptionwenn er im Moment nicht damit umgehen möchte. Nicht Stack-Trace Abwickeln Affekt Either/ Tryals 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.
Eyal Roth
Und ich kann nicht wirklich mit "rein funktionalem" Denken streiten, oder?
Eyal Roth
0

Das Schöne an Ausnahmen ist, dass der Ursprungscode den Ausnahmefall für Sie bereits klassifiziert hat. Der Unterschied zwischen an IllegalStateExceptionund a BadParameterExceptionist 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 Erweiterungen Throwablekönnen neben der Textnachrichtenzeichenfolge so viele Informationen enthalten, wie Sie benötigen.

Dies ist der wahre Nutzen Tryin der Scala-Umgebung, da Throwableer als Umhüllung für zurückgelassene Gegenstände dient, um das Leben zu erleichtern.

BobDalgleish
quelle
2
Ich verstehe deinen Standpunkt nicht.
Eyal Roth
Eitherhat Stilprobleme, weil es keine echte Monade ist (?); Die Beliebigkeit des leftWertes 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 von EitherZeichenfolgen , die in einem Fall auf der linken Seite zurückgegeben werden, try/catchmit der Verwendung von richtig getippten Zeichenfolgen in einem anderen Fall Throwables ist daher nicht richtig. Aufgrund des Werts Throwablesfür die Bereitstellung von Informationen und der prägnanten Art und Weise, die Trybehandelt wird Throwables, Tryist eine bessere Eithertry/catch
Wahl
Ich mache mir eigentlich keine Sorgen um die 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.
Eyal Roth