Vielleicht Monade gegen Ausnahmen

Antworten:

57

Die Verwendung von Maybe(oder seines Cousins, Eitherder im Wesentlichen auf die gleiche Weise funktioniert, Sie jedoch anstelle von einen beliebigen Wert zurückgeben lässt Nothing) dient einem etwas anderen Zweck als Ausnahmen. In Java ist das so, als hätte man eher eine aktivierte Ausnahme als eine Laufzeitausnahme. Es steht für etwas Erwartetes, mit dem Sie sich befassen müssen, und nicht für einen Fehler, den Sie nicht erwartet haben.

Eine Funktion wie indexOfwürde also einen MaybeWert zurückgeben, da Sie die Möglichkeit erwarten, dass das Element nicht in der Liste enthalten ist. Dies ähnelt der Rückkehr nullvon einer Funktion, außer auf typsichere Weise, die Sie dazu zwingt, sich mit dem nullFall zu befassen . EitherDies funktioniert auf die gleiche Weise, mit der Ausnahme, dass Sie Informationen zurückgeben können, die mit dem Fehlerfall verknüpft sind, sodass sie einer Ausnahme ähnlicher sind als Maybe.

Was sind die Vorteile des Maybe/ EitherAnsatzes? Zum einen ist es ein erstklassiger Bürger der Sprache. Vergleichen wir eine Funktion mit einer Funktion, Eitherdie eine Ausnahme auslöst. Für den Ausnahmefall ist Ihre einzige wirkliche Möglichkeit eine try...catchAussage. Für die EitherFunktion können Sie vorhandene Kombinatoren verwenden, um die Flusssteuerung übersichtlicher zu gestalten. Hier einige Beispiele:

Nehmen wir zunächst an, Sie möchten mehrere Funktionen ausprobieren, die nacheinander fehlschlagen können, bis Sie eine erhalten, die dies nicht tut. Wenn Sie keine ohne Fehler erhalten, möchten Sie eine spezielle Fehlermeldung zurückgeben. Dies ist eigentlich ein sehr nützliches Muster, aber es wäre ein schrecklicher Schmerz try...catch. Glücklicherweise können Sie, da Eitheres sich nur um einen normalen Wert handelt, vorhandene Funktionen verwenden, um den Code viel klarer zu gestalten:

firstThing <|> secondThing <|> throwError (SomeError "error message")

Ein weiteres Beispiel ist eine optionale Funktion. Angenommen, Sie müssen mehrere Funktionen ausführen, darunter eine, die versucht, eine Abfrage zu optimieren. Wenn dies fehlschlägt, möchten Sie, dass alles andere ausgeführt wird. Sie könnten Code schreiben wie:

do a <- getA
   b <- getB
   optional (optimize query)
   execute query a b

Beide Fälle sind klarer und kürzer als die Verwendung try..catchund vor allem semantischer. Wenn Sie eine Funktion wie <|>oder verwenden, werden optionalIhre Absichten viel klarer, als wenn Sie try...catchimmer Ausnahmen behandeln.

Beachten Sie auch , dass Sie nicht tun zu Wurf haben Sie den Code mit Zeilen wie if a == Nothing then Nothing else ...! Der springende Punkt bei der Behandlung Maybeund Eitherals Monade ist, dies zu vermeiden. Sie können die Ausbreitungssemantik in die Bindefunktion kodieren, sodass Sie die Null- / Fehlerprüfungen kostenlos erhalten. Das einzige Mal, dass Sie explizit prüfen müssen, ist, ob Sie etwas anderes als ein Nothinggegebenes zurückgeben möchten Nothing, und selbst dann ist es einfach: Es gibt eine Reihe von Standard-Bibliotheksfunktionen, die diesen Code schöner machen.

Ein weiterer Vorteil ist, dass ein Maybe/ Either-Typ nur einfacher ist. Es ist nicht erforderlich, die Sprache um zusätzliche Schlüsselwörter oder Kontrollstrukturen zu erweitern - alles ist nur eine Bibliothek. Da es sich nur um normale Werte handelt, wird das Typensystem einfacher - in Java müssen Sie zwischen Typen (z. B. dem Rückgabetyp) und Effekten (z. B. throwsAnweisungen) unterscheiden, die Sie nicht verwenden würden Maybe. Sie verhalten sich ebenso wie alle anderen benutzerdefinierten Typen - es ist kein spezieller Fehlerbehandlungscode erforderlich, der in die Sprache eingebettet ist.

Ein weiterer Sieg ist , dass Maybe/ Eithersind functors und Monaden, welche Mittel sie die Vorteile der bestehenden Monade Ablaufsteuerungsfunktionen übernehmen kann (von denen es eine ganze Reihe) und im Allgemeinen spielen schön zusammen mit anderen Monaden.

Das heißt, es gibt einige Einschränkungen. Zum einen werden ungeprüfte Ausnahmen weder ersetzt Maybenoch Eitherersetzt . Sie möchten eine andere Möglichkeit, um mit Dingen wie dem Teilen durch 0 umzugehen, einfach, weil es ein Problem wäre, wenn jede einzelne Division einen Wert zurückgibt.Maybe

Ein weiteres Problem besteht darin, dass mehrere Arten von Fehlern zurückgegeben werden (dies gilt nur für Either). Mit Ausnahmen können Sie verschiedene Arten von Ausnahmen in derselben Funktion auslösen. mit Eitherbekommst du nur einen typ. Dies kann durch Subtypisierung oder ein ADT überwunden werden, das alle Arten von Fehlern als Konstruktoren enthält (dieser zweite Ansatz wird normalerweise in Haskell verwendet).

Trotzdem bevorzuge ich den Maybe/ Either-Ansatz, weil ich ihn einfacher und flexibler finde.

Tikhon Jelvis
quelle
Meistens einverstanden, aber ich denke nicht, dass das "optionale" Beispiel Sinn macht - es scheint, als ob Sie dies genauso gut mit Ausnahmen tun können: void Optional (Action act) {try {act (); } catch (Exception) {}}
Errorsatz
11
  1. Eine Ausnahme kann weitere Informationen zur Ursache eines Problems enthalten. OpenFile()kann werfen FileNotFoundoder NoPermissionoder TooManyDescriptorsusw. A Keine trägt diese Information nicht.
  2. Ausnahmen können in Kontexten verwendet werden, in denen Rückgabewerte fehlen (z. B. bei Konstruktoren in Sprachen, in denen sie vorhanden sind).
  3. Mit einer Ausnahme können Sie die Informationen sehr einfach über den Stapel senden, ohne viele if None return None-Stil-Anweisungen.
  4. Die Ausnahmebehandlung wirkt sich fast immer auf die Leistung aus, als nur einen Wert zurückzugeben.
  5. Vor allem haben eine Ausnahme und eine Vielleicht-Monade unterschiedliche Zwecke - eine Ausnahme wird verwendet, um ein Problem zu kennzeichnen, eine Vielleicht-Monade nicht.

    "Schwester, wenn ein Patient in Raum 5 ist, können Sie ihn bitten zu warten?"

    • Vielleicht Monade: "Doktor, in Raum 5 ist kein Patient."
    • Ausnahme: "Herr Doktor, es gibt kein Zimmer 5!"

    (Beachten Sie das "Wenn" - dies bedeutet, dass der Arzt eine Vielleicht-Monade erwartet. )

Eiche
quelle
7
Ich stimme den Punkten 2 und 3 nicht zu. Insbesondere stellt 2 in Sprachen, die Monaden verwenden, kein Problem dar, da diese Sprachen Konventionen haben, die nicht das Rätsel aufwerfen, Fehler während der Erstellung behandeln zu müssen. In Bezug auf 3 haben Sprachen mit Monaden eine geeignete Syntax, um Monaden auf transparente Weise zu behandeln, ohne dass eine explizite Überprüfung erforderlich ist (zum Beispiel können NoneWerte einfach weitergegeben werden). Ihr Punkt 5 ist nur irgendwie richtig… die Frage ist: Welche Situationen sind eindeutig außergewöhnlich? Wie sich herausstellt ... nicht viele .
Konrad Rudolph
3
In Bezug auf (2) können Sie so schreiben bind, dass das Testen auf Nonekeinen syntaktischen Aufwand entsteht. Ein sehr einfaches Beispiel: C # überlastet die NullableOperatoren nur angemessen. NoneAuch bei Verwendung des Typs ist keine Überprüfung erforderlich. Natürlich wird die Prüfung noch durchgeführt (es ist typsicher), aber hinter den Kulissen und nicht Ihren Code überladen. Das Gleiche gilt in gewissem Sinne für Ihren Einwand gegen meinen Einwand gegen (5), aber ich bin damit einverstanden, dass er möglicherweise nicht immer zutrifft.
Konrad Rudolph
4
@Oak: In Bezug auf (3) besteht der springende Punkt bei der Behandlung Maybeals Monade darin, die Ausbreitung Noneimplizit zu machen . Dies bedeutet, dass Sie, wenn Sie einen Nonebestimmten NoneCode zurückgeben möchten , überhaupt keinen speziellen Code schreiben müssen. Sie müssen nur übereinstimmen, wenn Sie etwas Besonderes tun möchten None. Sie brauchen nie eine if None then NoneArt von Aussagen.
Tikhon Jelvis
5
@Oak: Das stimmt, aber das war nicht wirklich mein Punkt. Was ich sage ist, dass man nullgenau so (zB if Nothing then Nothing) kostenlosMaybe checkt, weil es eine Monade ist. Es ist in der Definition von bind ( >>=) für codiert Maybe.
Tikhon Jelvis
2
Auch in Bezug auf (1): Sie können leicht eine Monade schreiben, die Fehlerinformationen enthält (z. B. Either), die sich genau so verhalten Maybe. Das Umschalten zwischen den beiden ist eigentlich ziemlich einfach, da Maybees sich eigentlich nur um einen Sonderfall handelt Either. (In Haskell, könnte man sich vorstellen , Maybewie Either ().)
Tichon Jelvis
6

"Vielleicht" ist kein Ersatz für Ausnahmen. Ausnahmen sollen in Ausnahmefällen verwendet werden (zum Beispiel: Öffnen einer DB-Verbindung und der DB-Server ist nicht da, obwohl es sein sollte). "Vielleicht" dient zum Modellieren einer Situation, in der Sie möglicherweise einen gültigen Wert haben oder nicht. Angenommen, Sie erhalten einen Wert aus einem Wörterbuch für einen Schlüssel: Er kann vorhanden sein oder nicht - an keinem dieser Ergebnisse ist etwas "Außergewöhnliches".

Nemanja Trifunovic
quelle
2
Eigentlich habe ich ziemlich viel Code mit Wörterbüchern, bei denen ein fehlender Schlüssel bedeutet, dass irgendwann ein furchtbarer Fehler aufgetreten ist, höchstwahrscheinlich ein Fehler in meinem Code oder in dem Code, der die Eingabe in das konvertiert hat, was zu diesem Zeitpunkt verwendet wird.
3
@delnan: Ich sage nicht, dass ein fehlender Wert niemals ein Zeichen für einen Ausnahmezustand sein kann - es muss einfach nicht sein. Modelliert möglicherweise wirklich eine Situation, in der eine Variable einen gültigen Wert haben kann oder nicht - denken Sie an nullfähige Typen in C #.
Nemanja Trifunovic
6

Ich stimme Tikhons Antwort zu, aber ich denke, es gibt einen sehr wichtigen praktischen Punkt, den jeder vermisst:

  1. Ausnahmebehandlungsmechanismen sind, zumindest in den Hauptsprachen, eng mit einzelnen Threads verbunden.
  2. Der EitherMechanismus ist überhaupt nicht an Threads gekoppelt.

Was wir heutzutage im wirklichen Leben sehen, ist, dass viele asynchrone Programmierlösungen eine Variante des EitherStils der Fehlerbehandlung verwenden. Betrachten Sie Javascript- Versprechen , wie in einem dieser Links beschrieben:

Das Konzept der Versprechungen ermöglicht es Ihnen, wie folgt asynchronen Code zu schreiben (entnommen aus dem letzten Link):

var greetingPromise = sayHello();
greetingPromise
    .then(addExclamation)
    .then(function (greeting) {
        console.log(greeting);    // 'hello world!!!!’
    }, function(error) {
        console.error('uh oh: ', error);   // 'uh oh: something bad happened’
    });

Grundsätzlich ist ein Versprechen ein Objekt, das:

  1. Stellt das Ergebnis einer asynchronen Berechnung dar, die möglicherweise noch nicht abgeschlossen wurde.
  2. Ermöglicht es Ihnen, weitere Operationen zu verketten, um das Ergebnis auszuführen, das ausgelöst wird, wenn dieses Ergebnis verfügbar ist und dessen Ergebnisse wiederum als Versprechen verfügbar sind.
  3. Hier können Sie einen Fehlerbehandler anschließen, der aufgerufen wird, wenn die Berechnung des Versprechens fehlschlägt. Wenn es keinen Handler gibt, wird der Fehler an spätere Handler in der Kette weitergegeben.

Da die systemeigene Ausnahmebedingungsunterstützung der Sprache nicht funktioniert, wenn Ihre Berechnung über mehrere Threads hinweg erfolgt, muss eine versprochene Implementierung einen Fehlerbehandlungsmechanismus bereitstellen, der sich als Monaden herausstellt, die denen von Haskell Maybe/ Eithertypes ähneln .

Sacundim
quelle
Es hat nichts mit Threads zu tun. JavaScript im Browser wird immer in einem Thread ausgeführt, nicht in mehreren Threads. Sie können jedoch keine Ausnahme verwenden, da Sie nicht wissen, wann Ihre Funktion in Zukunft aufgerufen wird. Asynchron bedeutet nicht automatisch eine Beteiligung von Threads. Das ist auch der Grund, warum Sie nicht mit Ausnahmen arbeiten können. Sie können eine Ausnahme nur dann abrufen, wenn Sie eine Funktion aufrufen und diese sofort ausgeführt wird. Aber der ganze Zweck von Asynchronous ist, dass es in der Zukunft ausgeführt wird, oft wenn etwas anderes fertig ist und nicht sofort. Deshalb können Sie dort keine Ausnahmen verwenden.
David Raab
1

Das System vom Typ Haskell fordert den Benutzer auf, die Möglichkeit von a zu bestätigen Nothing, wohingegen Programmiersprachen häufig nicht erfordern, dass eine Ausnahme abgefangen wird. Das bedeutet, dass wir zur Kompilierungszeit wissen, dass der Benutzer nach einem Fehler gesucht hat.

Chrisaycock
quelle
1
In Java gibt es geprüfte Ausnahmen. Und die Leute haben sogar sie oft schlecht behandelt.
Vladimir
1
@ DairT'arg ButJava erfordert eine Überprüfung auf E / A-Fehler (als Beispiel), nicht jedoch auf NPEs. In Haskell ist es umgekehrt: Das Null-Äquivalent (Vielleicht) ist markiert, aber E / A-Fehler werfen einfach (nicht markierte) Ausnahmen. Ich bin mir nicht sicher, wie groß der Unterschied ist, aber ich höre Haskellers nicht, der sich über Vielleicht beschwert, und ich denke, viele Leute möchten, dass NPEs überprüft werden.
1
@delnan Leute wollen, dass NPE überprüft wird? Sie müssen verrückt sein. Und das meine ich auch so. Die Überprüfung von NPE ist nur dann sinnvoll, wenn Sie eine Möglichkeit haben, dies zu vermeiden (indem Sie nicht nullfähige Referenzen haben, die Java nicht hat).
Konrad Rudolph
5
@KonradRudolph Ja, die Leute wollen wahrscheinlich nicht, dass es in dem Sinne überprüft wird, dass sie throws NPEjeder einzelnen Signatur und catch(...) {throw ...} jedem einzelnen Methodenkörper ein hinzufügen müssen . Aber ich glaube, es gibt einen Markt für Überprüfungen in demselben Sinne wie für Vielleicht: Die Nullwertfähigkeit ist optional und wird im Typsystem nachverfolgt.
1
@delnan Ah, dann stimme ich zu.
Konrad Rudolph
0

Die Vielleicht-Monade ist im Grunde die gleiche wie die Prüfung mit "Null bedeutet Fehler" in den meisten gängigen Sprachen (mit der Ausnahme, dass die Null geprüft werden muss) und weist im Wesentlichen dieselben Vor- und Nachteile auf.

Telastyn
quelle
8
Nun, es hat nicht die gleichen Nachteile, da es bei korrekter Verwendung statisch typgeprüft werden kann. Es gibt kein Äquivalent zu einer Nullzeiger-Ausnahme, wenn Sie möglicherweise Monaden verwenden (vorausgesetzt, sie werden korrekt verwendet).
Konrad Rudolph
Tatsächlich. Wie denkst du sollte das geklärt werden?
Telastyn
@Konrad Das liegt daran, dass Sie, um den Wert (möglicherweise) in "Vielleicht" zu verwenden, nach "Keine" suchen müssen (obwohl es natürlich die Möglichkeit gibt, viele dieser Überprüfungen zu automatisieren). Andere Sprachen halten so etwas manuell (hauptsächlich aus philosophischen Gründen, glaube ich).
Donal Fellows
2
@Donal Ja, aber beachten Sie, dass die meisten Sprachen, die Monaden implementieren, geeigneten syntaktischen Zucker enthalten, sodass diese Überprüfung häufig vollständig ausgeblendet werden kann. Sie können beispielsweise einfach zwei MaybeZahlen durch Schreiben hinzufügen , a + b ohne nachsehen zu müssen None, und das Ergebnis ist wieder ein optionaler Wert.
Konrad Rudolph
2
Der Vergleich mit nullfähigen Typen gilt für den Maybe Typ , aber die Verwendung Maybeals Monade fügt Syntaxzucker hinzu, mit dem sich nullartige Logik viel eleganter ausdrücken lässt.
tdammers
0

Die Ausnahmebehandlung kann ein echtes Problem beim Factoring und Testen sein. Ich weiß, dass Python eine nette "with" -Syntax bietet, mit der Sie Ausnahmen ohne den starren "try ... catch" -Block abfangen können. Aber in Java zum Beispiel sind catch-Blöcke groß, ausführlich oder extrem ausführlich und schwer zu trennen. Darüber hinaus fügt Java das gesamte Rauschen um aktivierte und nicht aktivierte Ausnahmen hinzu.

Wenn Ihre Monade stattdessen Ausnahmen abfängt und sie als eine Eigenschaft des monadischen Raums behandelt (anstelle von Verarbeitungsfehlern), können Sie Funktionen, die Sie in diesen Raum einbinden, unabhängig davon, was sie werfen oder abfangen, mischen und zuordnen.

Wenn, noch besser, Ihre Monade Bedingungen verhindert, in denen Ausnahmen auftreten könnten (z. B. ein Null-Check in Vielleicht), dann noch besser. wenn ... dann ist es viel einfacher zu faktorisieren und zu testen als zu versuchen ... zu fangen.

Wie ich gesehen habe, verfolgt Go einen ähnlichen Ansatz, indem angegeben wird, dass jede Funktion zurückgibt (Antwort, Fehler). Das ist in etwa so, als würde man die Funktion in einen Monadenraum "heben", in dem der Kernantworttyp mit einer Fehleranzeige versehen ist, und Ausnahmen effektiv ausweichen und abfangen.

rauben
quelle