Verunreinigt das Fangen / Werfen von Ausnahmen eine ansonsten reine Methode?

27

Die folgenden Codebeispiele bieten Kontext zu meiner Frage.

Die Raumklasse wird mit einem Delegaten initialisiert. In der ersten Implementierung der Room-Klasse gibt es keine Schutzmaßnahmen gegen Delegierte, die Ausnahmen auslösen. Solche Ausnahmen sprudeln in die Eigenschaft North, in der der Delegat ausgewertet wird (Hinweis: Die Methode Main () zeigt, wie eine Room-Instanz im Clientcode verwendet wird):

public sealed class Room
{
    private readonly Func<Room> north;

    public Room(Func<Room> north)
    {
        this.north = north;
    }

    public Room North
    {
        get
        {
            return this.north();
        }
    }

    public static void Main(string[] args)
    {
        Func<Room> evilDelegate = () => { throw new Exception(); };

        var kitchen = new Room(north: evilDelegate);

        var room = kitchen.North; //<----this will throw

    }
}

Da ich lieber bei der Objekterstellung als beim Lesen der Eigenschaft North versagen möchte, ändere ich den Konstruktor in private und führe eine statische Factory-Methode mit dem Namen Create () ein. Diese Methode fängt die vom Delegaten ausgelöste Ausnahme ab und löst eine Wrapper-Ausnahme mit einer aussagekräftigen Ausnahmemeldung aus:

public sealed class Room
{
    private readonly Func<Room> north;

    private Room(Func<Room> north)
    {
        this.north = north;
    }

    public Room North
    {
        get
        {
            return this.north();
        }
    }

    public static Room Create(Func<Room> north)
    {
        try
        {
            north?.Invoke();
        }
        catch (Exception e)
        {
            throw new Exception(
              message: "Initialized with an evil delegate!", innerException: e);
        }

        return new Room(north);
    }

    public static void Main(string[] args)
    {
        Func<Room> evilDelegate = () => { throw new Exception(); };

        var kitchen = Room.Create(north: evilDelegate); //<----this will throw

        var room = kitchen.North;
    }
}

Verunreinigt der Try-Catch-Block die Create () -Methode?

Rock Anthony Johnson
quelle
1
Was ist der Vorteil der Raumerstellungsfunktion? Wenn Sie einen Stellvertreter verwenden, warum rufen Sie ihn an, bevor der Client "North" anruft?
SH
1
@ SH Wenn der Delegat eine Ausnahme auslöst, möchte ich dies beim Erstellen des Room-Objekts herausfinden. Ich möchte nicht, dass der Client die Ausnahme bei der Verwendung findet, sondern beim Erstellen. Die Create-Methode ist der perfekte Ort, um böse Delegierte bloßzustellen.
Rock Anthony Johnson
3
@ RockAnthonyJohnson, ja. Was tun Sie, wenn Sie den Stellvertreter ausführen, möglicherweise nur beim ersten Mal oder wenn Sie beim zweiten Anruf einen anderen Raum zurückgeben? Das Anrufen des Delegierten kann in Betracht gezogen werden / selbst eine Nebenwirkung verursachen?
SH
4
Eine Funktion, die eine unreine Funktion aufruft, ist eine unreine Funktion. Wenn der Delegat also unrein Createist, ist er auch unrein, weil er sie aufruft.
Idan Arye
2
Ihre CreateFunktion schützt Sie nicht vor einer Ausnahme beim Abrufen der Eigenschaft. Wenn Ihr Delegierter wirft, ist es im wirklichen Leben sehr wahrscheinlich, dass er nur unter bestimmten Bedingungen wirft. Es besteht die Möglichkeit, dass die Bedingungen für das Werfen während des Baus nicht gegeben sind, aber sie sind vorhanden, wenn das Eigentum erhalten wird.
Bart van Ingen Schenau

Antworten:

26

Ja. Das ist praktisch eine unreine Funktion. Es entsteht ein Nebeneffekt: Die Programmausführung wird an einer anderen Stelle fortgesetzt als an der Stelle, an die die Funktion voraussichtlich zurückkehren wird.

Um es zu einer reinen Funktion zu machen, returnein tatsächliches Objekt, das den erwarteten Wert der Funktion und einen Wert, der einen möglichen Fehlerzustand anzeigt, wie ein MaybeObjekt oder ein Objekt der Arbeitseinheit , kapselt .

Robert Harvey
quelle
16
Denken Sie daran, "rein" ist nur eine Definition. Wenn Sie eine Funktion benötigen, die auslöst, aber auf jede andere Weise referenziell transparent ist, schreiben Sie diese.
Robert Harvey
1
In haskell kann mindestens jede Funktion werfen, aber Sie müssen sich in IO befinden, um zu fangen. Eine reine Funktion, die manchmal ausgelöst wird, wird normalerweise als partial
jk bezeichnet.
4
Ich habe eine Offenbarung wegen Ihres Kommentars gehabt. Ich bin zwar intellektuell fasziniert von Reinheit, aber was ich in der Praxis wirklich brauche, ist Determinismus und ehrfurchtsvolle Transparenz. Wenn ich Reinheit erreiche, ist das nur ein zusätzlicher Bonus. Vielen Dank. Was mich auf diesem Weg antreibt, ist die Tatsache, dass Microsoft IntelliTest nur gegen deterministischen Code wirksam ist.
Rock Anthony Johnson
4
@RockAnthonyJohnson: Nun, jeder Code sollte deterministisch oder zumindest so deterministisch wie möglich sein. Referentielle Transparenz ist nur ein Weg, um diesen Determinismus zu erreichen. Einige Techniken, wie z. B. Threads, arbeiten tendenziell gegen diesen Determinismus. Daher gibt es andere Techniken, wie z. B. Tasks, die die nicht deterministischen Tendenzen des Threading-Modells verbessern. Dinge wie Zufallszahlengeneratoren und Monte-Carlo-Simulatoren sind ebenfalls deterministisch, basieren jedoch auf unterschiedlichen Wahrscheinlichkeiten.
Robert Harvey
3
@zzzzBov: Ich halte die tatsächliche, strenge Definition von "pur" nicht für so wichtig. Siehe den zweiten Kommentar oben.
Robert Harvey
12

Na ja ... und nein.

Eine reine Funktion muss über referenzielle Transparenz verfügen. Das heißt, Sie sollten in der Lage sein, jeden möglichen Aufruf einer reinen Funktion durch den zurückgegebenen Wert zu ersetzen, ohne das Verhalten des Programms zu ändern. * Es ist garantiert, dass Ihre Funktion immer nach bestimmten Argumenten wirft. Daher gibt es keinen Rückgabewert, durch den der Funktionsaufruf ersetzt werden kann. Lassen Sie uns ihn stattdessen ignorieren . Betrachten Sie diesen Code:

{
    var ignoreThis = func(arg);
}

Wenn funces rein ist, könnte ein Optimierer entscheiden, dass func(arg)es durch sein Ergebnis ersetzt werden könnte. Es weiß noch nicht, was das Ergebnis ist, aber es kann feststellen, dass es nicht verwendet wird - also kann es einfach ableiten, dass diese Aussage keine Wirkung hat und sie entfernen.

Aber wenn func(arg)zufällig geworfen wird, bewirkt diese Anweisung etwas - sie löst eine Ausnahme aus! Der Optimierer kann es also nicht entfernen - es spielt keine Rolle, ob die Funktion aufgerufen wird oder nicht.

Aber...

In der Praxis spielt dies nur eine sehr geringe Rolle. Eine Ausnahme - zumindest in C # - ist etwas Außergewöhnliches. Sie sollten es nicht als Teil Ihres regulären Kontrollflusses verwenden - Sie sollten versuchen, es abzufangen, und wenn Sie etwas abfangen, können Sie den Fehler beheben, um das, was Sie getan haben, entweder rückgängig zu machen oder es irgendwie noch zu erreichen. Wenn Ihr Programm nicht richtig funktioniert, weil ein Code, der fehlgeschlagen wäre, wegoptimiert wurde, verwenden Sie Ausnahmen falsch (es sei denn, es handelt sich um Testcode, und wenn Sie für Tests Ausnahmen erstellen, sollten Sie nicht optimieren).

Davon abgesehen ...

Wirf keine Ausnahmen von reinen Funktionen mit der Absicht, dass sie abgefangen werden - es gibt einen guten Grund, warum funktionale Sprachen lieber Monaden als Stack-Unwinding-Ausnahmen verwenden.

Wenn C # eine ErrorKlasse wie Java (und viele andere Sprachen) hätte, hätte ich vorgeschlagen, eine Erroranstelle einer zu werfen Exception. Es zeigt an, dass der Benutzer der Funktion etwas falsch gemacht hat (eine Funktion übergeben hat, die ausgelöst wird), und solche Dinge sind in reinen Funktionen zulässig. C # hat jedoch keine ErrorKlasse, und die Ausnahmebedingungen für Verwendungsfehler scheinen sich daraus abzuleiten Exception. Mein Vorschlag ist, ein zu werfen ArgumentException, um zu verdeutlichen, dass die Funktion mit einem falschen Argument aufgerufen wurde.


* Rechnerisch gesehen. Eine Fibonacci-Funktion, die mit einer naiven Rekursion implementiert wird, wird für große Zahlen viel Zeit in Anspruch nehmen und möglicherweise die Ressourcen der Maschine erschöpfen, da diese bei unbegrenzter Zeit und unbegrenztem Speicher immer den gleichen Wert zurückgeben und keine Nebenwirkungen haben (außer der Zuweisung) Erinnerung und Veränderung dieser Erinnerung) - es wird immer noch als rein betrachtet.

Idan Arye
quelle
1
+1 Für Ihre vorgeschlagene Verwendung von ArgumentException und für Ihre Empfehlung, keine "Ausnahmen aus reinen Funktionen mit der Absicht auszulösen, dass sie abgefangen werden".
Rock Anthony Johnson
Dein erstes Argument (bis zum "Aber ...") scheint mir nicht stichhaltig zu sein. Die Ausnahme ist Vertragsbestandteil der Funktion. Sie können konzeptionell jede Funktion umschreiben (value, exception) = func(arg)und die Möglichkeit des Auslösens einer Ausnahme entfernen (in einigen Sprachen und für einige Arten von Ausnahmen müssen Sie sie tatsächlich mit der Definition auflisten, genauso wie Argumente oder Rückgabewerte). Jetzt wäre es genauso rein wie zuvor (vorausgesetzt, es gibt immer eine Ausnahme aus, wenn das gleiche Argument vorliegt). Das Auslösen einer Ausnahme ist keine Nebenwirkung, wenn Sie diese Interpretation verwenden.
AnoE
@AnoE Wenn Sie so geschrieben hätten, hätte funcder Block, den ich gegeben habe, optimiert werden können, da anstelle des Abwickelns des Stapels die geworfene Ausnahme codiert worden wäre ignoreThis, die wir ignorieren. Im Gegensatz zu Ausnahmen, die den Stapel auflösen, verstößt eine im Rückgabetyp codierte Ausnahme nicht gegen die Reinheit - und aus diesem Grund bevorzugen es viele funktionale Sprachen, sie zu verwenden.
Idan Arye
Genau ... Sie hätten die Ausnahme für diese imaginäre Transformation nicht ignoriert. Ich denke, es ist alles ein bisschen eine Frage der Semantik / Definition, ich wollte nur darauf hinweisen, dass die Existenz oder das Auftreten von Ausnahmen nicht notwendigerweise alle Vorstellungen von Reinheit ungültig macht.
AnoE
1
@AnoE Aber der Code, den ich geschrieben habe, würde die Ausnahme für diese imaginäre Transformation ignorieren. func(arg)würde das Tupel zurückgeben (<junk>, TheException())und ignoreThiswird das Tupel auch enthalten (<junk>, TheException()), aber da wir ignoreThisdie Ausnahme nie verwenden , hat dies keine Auswirkung auf irgendetwas.
Idan Arye
1

Eine Überlegung ist, dass der Try-Catch-Block nicht das Problem ist. (Gestützt auf meine Kommentare zu der Frage oben).

Das Hauptproblem besteht darin, dass die North-Eigenschaft ein E / A-Aufruf ist .

Zu diesem Zeitpunkt in der Codeausführung muss das Programm die vom Clientcode bereitgestellten E / A überprüfen. (Es ist nicht relevant, dass der Input in Form eines Delegierten vorliegt oder dass der Input nominell bereits übergeben wurde.)

Sobald Sie die Kontrolle über die Eingabe verlieren, können Sie nicht mehr sicherstellen, dass die Funktion rein ist. (Besonders wenn die Funktion werfen kann).


Mir ist nicht klar, warum Sie den Anruf für Move [Check] Room nicht überprüfen möchten? Nach meinem Kommentar zur Frage:

Ja. Was tun Sie, wenn Sie den Stellvertreter ausführen, möglicherweise nur beim ersten Mal oder wenn Sie beim zweiten Anruf einen anderen Raum zurückgeben? Das Anrufen des Delegierten kann in Betracht gezogen werden / selbst eine Nebenwirkung verursachen?

Wie Bart van Ingen Schenau oben sagte,

Ihre Create-Funktion schützt Sie nicht vor einer Ausnahme beim Abrufen der Eigenschaft. Wenn Ihr Delegierter wirft, ist es im wirklichen Leben sehr wahrscheinlich, dass er nur unter bestimmten Bedingungen wirft. Es besteht die Möglichkeit, dass die Bedingungen für das Werfen während des Baus nicht gegeben sind, aber sie sind vorhanden, wenn das Eigentum erhalten wird.

Im Allgemeinen verzögert jede Art von verzögertem Laden implizit die Fehler bis zu diesem Zeitpunkt.


Ich würde vorschlagen, eine Move [Check] Room-Methode zu verwenden. Auf diese Weise können Sie die unreinen E / A-Aspekte an einer Stelle trennen.

Ähnlich wie bei Robert Harvey :

Um es zu einer reinen Funktion zu machen, geben Sie ein tatsächliches Objekt zurück, das den erwarteten Wert der Funktion und einen Wert enthält, der auf einen möglichen Fehlerzustand hinweist, z. B. ein Vielleicht-Objekt oder ein Arbeitseinheit-Objekt.

Es wäre Sache des Codeschreibers, zu bestimmen, wie mit der (möglichen) Ausnahme von der Eingabe umgegangen werden soll. Dann kann die Methode ein Room-Objekt oder ein Null-Room-Objekt zurückgeben oder möglicherweise die Ausnahme ausblasen.

Auf diesen Punkt kommt es an:

  • Behandelt die Raumdomäne Raumausnahmen als Null oder etwas Schlimmeres?
  • So benachrichtigen Sie den Client-Code, der North in einem Null- / Ausnahmeraum anruft. (Kaution / Statusvariable / Globaler Zustand / Monade zurückgeben / Was auch immer; Einige sind reiner als andere :)).
SCH-
quelle
-1

Hmm ... ich habe mich nicht richtig gefühlt. Ich denke, Sie wollen sicherstellen, dass andere Menschen ihre Arbeit richtig machen, ohne zu wissen, was sie tun.

Da die Funktion vom Verbraucher übergeben wird, könnte dies von Ihnen oder von einer anderen Person geschrieben werden.

Was passiert, wenn die Funktionsübergabe zum Zeitpunkt der Erstellung des Raumobjekts nicht gültig ist?

Aus dem Code konnte ich nicht sicher sein, was der Norden tut.

Sagen wir, wenn die Funktion darin besteht, das Zimmer zu buchen, und es die Zeit und den Zeitraum der Buchung benötigt, dann würden Sie eine Ausnahme bekommen, wenn Sie die Informationen nicht bereit haben.

Was ist, wenn es sich um eine Langzeitfunktion handelt? Sie möchten Ihr Programm nicht blockieren, während Sie ein Raumobjekt erstellen. Was ist, wenn Sie 1000 Raumobjekte erstellen müssen?

Wenn Sie die Person wären, die den Consumer schreibt, der diese Klasse konsumiert, würden Sie sicherstellen, dass die übergebene Funktion richtig geschrieben ist, nicht wahr?

Wenn es eine andere Person gibt, die den Konsumenten schreibt, kennt er / sie möglicherweise nicht die "spezielle" Bedingung zum Erstellen eines Raumobjekts, die zur Ausführung führen kann, und kratzt sich am Kopf, um herauszufinden, warum.

Eine andere Sache ist, dass es nicht immer gut ist, eine neue Ausnahme zu machen. Grund dafür ist, dass es nicht die Informationen der ursprünglichen Ausnahme enthält. Sie wissen, dass Sie einen Fehler erhalten (eine freundliche Meldung für eine allgemeine Ausnahme), aber Sie würden nicht wissen, was der Fehler ist (Nullreferenz, Index außerhalb der Grenzen, Teilung durch Null usw.). Diese Informationen sind sehr wichtig, wenn wir Nachforschungen anstellen müssen.

Sie sollten die Ausnahme nur dann behandeln, wenn Sie wissen, was und wie Sie damit umgehen sollen.

Ich denke, Ihr ursprünglicher Code ist gut genug. Das Einzige, was Sie tun können, ist, dass Sie die Ausnahme auf der "Hauptseite" behandeln möchten (oder auf einer beliebigen Ebene, die weiß, wie man damit umgeht).

user251630
quelle
1
Sehen Sie sich noch einmal das zweite Codebeispiel an. Ich initialisiere die neue Ausnahme mit der unteren Ausnahme. Der gesamte Stack-Trace bleibt erhalten.
Rock Anthony Johnson
Hoppla. Mein Fehler.
user251630
-1

Ich werde mit den anderen Antworten nicht einverstanden sein und Nein sagen . Ausnahmen führen nicht dazu, dass eine ansonsten reine Funktion unrein wird.

Jede Verwendung von Ausnahmen könnte umgeschrieben werden, um explizite Überprüfungen auf Fehlerergebnisse durchzuführen. Darüber hinaus könnten Ausnahmen als syntaktischer Zucker betrachtet werden. Praktischer syntaktischer Zucker macht reinen Code nicht unrein.

JacquesB
quelle
1
Die Tatsache, dass Sie die Verunreinigung wegschreiben können, macht die Funktion nicht rein. Haskell ist ein Beweis dafür, dass die Verwendung unreiner Funktionen mit reinen Funktionen umgeschrieben werden kann. Es verwendet Monaden, um unreines Verhalten in den Rückgabetypen reine reine Funktionen zu codieren. Die Existenz von IO-Monaden impliziert nicht, dass reguläre IO rein ist. Warum sollte die Existenz von Exceptions-Monaden implizieren, dass reguläre Exceptions rein sind?
Idan Arye
@IdanArye: Ich behaupte, es gibt überhaupt keine Verunreinigung. Das Umschreibebeispiel diente nur dazu, dies zu demonstrieren. Regelmäßige E / A-Vorgänge sind unrein, da sie beobachtbare Nebenwirkungen verursachen oder aufgrund von externen Eingaben unterschiedliche Werte mit demselben Eingang zurückgeben können. Das Auslösen einer Ausnahme führt zu keiner dieser Aktionen.
JacquesB
Freier Code für Nebenwirkungen reicht nicht aus. Die referenzielle Transparenz ist auch eine Voraussetzung für die Funktionsreinheit.
Idan Arye
1
Determinismus ist nicht genug für referentielle Transparenz - Nebenwirkungen können auch deterministisch sein. Referentielle Transparenz bedeutet, dass der Ausdruck durch seine Ergebnisse ersetzt werden kann. Unabhängig davon, wie oft Sie referenzielle transparente Ausdrücke in welcher Reihenfolge auswerten und ob Sie sie überhaupt auswerten oder nicht, sollte das Ergebnis dasselbe sein. Wenn eine Ausnahme deterministisch geworfen wird, sind wir gut mit den Kriterien "egal wie oft", aber nicht mit den anderen. Ich habe in meiner eigenen Antwort über das "ob oder nicht" -Kriterium
Idan Arye
1
(... Fortsetzung) und was die "in welcher Reihenfolge" angeht - wenn fund gsind beide reine Funktionen, dann f(x) + g(x)sollte das gleiche Ergebnis unabhängig von der Reihenfolge, die Sie auswerten f(x)und g(x). Aber wenn f(x)wirft Fund g(x)wirft G, dann wäre die Ausnahme, die von diesem Ausdruck geworfen wird, Fwenn f(x)zuerst ausgewertet wird und Gwenn g(x)zuerst ausgewertet wird - so sollten sich reine Funktionen nicht verhalten! Wenn die Ausnahme im Rückgabetyp codiert ist, würde das Ergebnis ungefähr so ​​aussehenError(F) + Error(G) unabhängig von der Auswertungsreihenfolge angezeigt.
Idan Arye