Nomenklatur: Exceptional<>



  • Spaßeshalber habe ich gerade mal die Exceptional -Monade in C# implementiert, also einen generischen Typ Exceptional<T> , der entweder einen Wert vom Typ T oder eine Exception beinhaltet, inklusive der üblichen Supportfunktionen für die comprehension syntax ( Select() , SelectMany() ):

    static Exceptional<int> Divide(int lhs, int rhs)
            {
                if (rhs == 0) return new ArgumentException("Cannot divide by zero");
                return lhs / rhs;
            }
            static Exceptional<int> Remainder(int lhs, int rhs)
            {
                return from quotient in Divide(lhs, rhs)
                       select lhs - quotient * rhs;
            }
    
            static void Main(string[] args)
            {
                var r1 = from quotient in Divide(4, 3)
                         from remainder in Remainder(2, 1)
                         select new { quotient, remainder };
                // r1 ist vom Typ Exceptional<int>
            }
    

    So weit, so sinnlos. Aber ist es nicht hübsch? 🙂 Jedenfalls fiel mir auf, daß man Exceptional<T> auch noch um einen nicht-generischen Typ Exceptional für Funktionen ohne Rückgabewert ergänzen sollte (wir sind ja nicht in Haskell), denn die können auch Exceptions verursachen, was Exceptional<T> ja abbilden soll. Also habe ich analog dazu einen Typ Exceptional definiert (in Analogie zu Task und Task<T> ), mit dem ich denselben Effekt für Funktionen ohne Rückgabewert bekomme:

    static Exceptional<Ingredients> BuyIngredients(Recipe recipe) { ... }
            static Exceptional ShredIngredients(Ingredients ingredients) { ... }
            static Exceptional Cook(Recipe recipe, Ingredients ingredients) { ... }
            static Exceptional Decorate(Plate plate, Ingredients ingredients) { ... }
    
            static Exceptional<Plate> MakeDinner(Recipe recipe)
            {
                return from ingredients in BuyIngredients(recipe)
                       where ShredIngredients(ingredients)
                          && Cook(recipe, ingredients)
                       let plate = new Plate()
                       where Decorate(plate, ingredients)
                       select plate;
            }
    

    Das einzige, was mich nun stört, ist der Name Exceptional . Für einen generischen Typ kann ich damit leben, auch wenn es nicht intuitiv ist ("an exceptional integer"), aber Exceptional finde ich nichtssagend. Sprechender wäre vielleicht MaybeException oder MaybeError oder Diagnostic , aber so richtig überzeugt es mich nicht, auch weil es für die generische Variante dann nicht mehr paßt (daß Diagnostic<T> entweder eine Exception oder ein T ist, leuchtet nicht ein). Und mir gefällt auch nicht, wenn die beiden Typen nicht mehr denselben Namen tragen, also z.B. Diagnostic und Exceptional<T> , aber dennoch dieselbe Funktion erfüllen.

    Habt ihr besser Vorschläge für die Benennung von Exceptional und Exceptional<T> ?

    Vielleicht sage ich sicherheitshalber noch ausdrücklich, daß das kein Produktivcode ist, sondern ich nur am Experimentieren bin.



  • Bitte nicht so viel posten, ich komme ja kaum noch mit 🙂

    Momentan neige ich dazu, die Typen einfach Result und Result<T> zu nennen. Aus einer Funktion, die den Rückgabetyp void hat, wird dann eine mit Rückgabetyp Result (analog zu HRESULT ), und eine Funktion, die einen Wert vom Typ T zurückgibt, wird zu einer mit Result<T> als Rückgabewert.



  • Evtl. MaybeResult und MaybeVoid? Maybe und MaybeVoid?

    Vielleicht sage ich sicherheitshalber noch ausdrücklich, daß das kein Produktivcode ist, sondern ich nur am Experimentieren bin.

    Vielleicht sage ich sicherheitshalber noch ausdrücklich, dass ich dieses Vorgehen überhaupt nicht gutheißen kann. Ist ja quasi nur Schnickschnack für ein Tuple<T, Exception> und das wiederum nur ein Schnickschnack für HRESULT und das wiederum nur ein Schnickschnack für GetLastError(). Nene.

    MfG SideWinder



  • SideWinder schrieb:

    Evtl. MaybeResult und MaybeVoid? Maybe und MaybeVoid?

    Maybe<T> hat ja schon anerkanntermaßen eine ähnliche Bedeutung wie Nullable<T> , und die ist anders als Exceptional<T> : ersteres liefert einen Wert oder keinen Wert, letzteres einen Wert oder eine Fehlermeldung. Und außerdem, was heißt dann Maybe und MaybeVoid ? Vielleicht keinen Wert? Und was ist dann andernfalls?

    SideWinder schrieb:

    Vielleicht sage ich sicherheitshalber noch ausdrücklich, dass ich dieses Vorgehen überhaupt nicht gutheißen kann. Ist ja quasi nur Schnickschnack für ein Tuple<T, Exception>

    Nein, ist es nicht. Eine korrekte Entsprechung wäre Union<T, NonNullable<Exception>> , oder meinetwegen Either<T, Exception> . Einem Tupel könntest du ein T und eine Exception mitgeben, und das macht keinen Sinn.

    SideWinder schrieb:

    und das wiederum nur ein Schnickschnack für HRESULT und das wiederum nur ein Schnickschnack für GetLastError()

    Auch nicht. COM benutzt HRESULT und IErrorInfo , um Exceptions abzubilden, und das Win32-API benutzt GetLastError() und (meistens) einen BOOL -Rückgabewert für denselben Zweck. Beides sind technische Details von Low-level-APIs. Und wir sind ja in C#, wo wir den Umgang mit derartigen Schnittstellen möglichst vermeiden, geschweige denn welche davon nachbauen, haben wir doch die Abstraktionsmittel einer ausgewachsenen modernen Sprache zur Verfügung 🙂

    Der Anwendungsfall, der mich auf diese Geschichte gebracht hat, ist der folgende:

    In einer GUI-Anwendung möchte ich die Benutzereingabe auf Gültigkeit prüfen. Über ein Databinding ist z.B. eine Textbox mit einem String-Property verbunden. Der Setter führt natürlich eine Prüfung durch und wirft ggf. eine Exception; aber natürlich will ich nicht, daß eine Exception fliegt, nur weil der Benutzer eine Fehleingabe tätigt. Aber die Fehlermeldung, die der Exception beiliegen würde, möchte ich dem Benutzer z.B. via ErrorProvider anzeigen.

    Der erste Versuch war eine Validatorfunktion, die die Exception nicht warf, sondern einfach zurückgab:

    Exception ValidateName(string input)
    {
        if (input.Length > 200) return new ArgumentException("Value may not exceed 200 characters");
        ...
        return null;
    }
    

    Daran gefiel mir allerdings der null -Rückgabewert nicht, weil null nichtssagend ist: hat die Funktion jetzt einen Bug, weil sie null zurückgibt, oder bedeutet null , daß es keinen Fehler gab? Selbst wenn ich das für die Funktion klarstellen kann, z.B. indem ich sie in GetNameValidationErrorOrNull() umbenenne, bleibt das Problem bestehen, wenn ich das Funktionsergebnis herumreiche, weil die Bedeutung der null nicht im Typsystem abgebildet wird. (Aus solchen Gründen sollte man auf null -Objektreferenzen möglichst verzichten und jedes null als einen Fehler behandeln.) Also verwendete ich stattdessen ein Maybe<Exception> als Rückgabewert, das zurzeit ValueDiagnostic heißt:

    ValueDiagnostic ValidateName(string input)
    {
        if (input.Length > 200) return new ArgumentException("Value may not exceed 200 characters");
        ...
        return ValueDiagnostic.None;
    }
    

    Weil die Funktion nie null zurückgibt, kann ich jetzt denselben Validator bequem im Property-Setter verwenden:

    string Name
    {
        get { ... }
        set
        {
            ValidateName(value).Check(); // wirft die Validierungs-Exception, falls es eine gab
            ...
        }
    }
    

    Jedenfalls fiel mir da auf, daß es eigentlich auch ein ValueDiagnostic -Pendant geben müßte für Funktionen, die einen Rückgabewert haben. Und das ist eben die Exceptional -Monade, wie sie z.B. in Haskell verwendet wird, und die ich zur Förderung der Erkenntnis mal in C# nachgebaut hatte.

    Und jetzt bin ich eben unsicher, ob ValueDiagnostic wirklich so passend ist. Eigentlich ist diese Bezeichnung zu spezifisch für meinen Einsatzzweck (Eingabewerte prüfen), und insbesondere ValueDiagnostic<T> macht keinen Sinn (wieso soll ein Diagnoseergebnis einen Typparameter haben?). Ebenso finde ich, wie oben dargelegt, Exceptional ohne Typparameter nicht intuitiv. Deshalb überlege ich nun, ob ich das Ding einfach Result nenne. Die Semantik ist jedem vertraut, der mal ein HRESULT gesehen hat, woran der Name eben erinnert; die Erweiterung auf Result<T> würde einleuchten; und nicht zuletzt gibt es schon einen Präzedenzfall mit Task und Task<T> . Ich habe nur Bedenken, weil "Result" so allgemein ist und sich oft genug nur auf den Wert bezieht, etwa in TaskCompletionSource<T>.SetResult() . Aber ResultOrException / ResultOrException<T> ist lang und häßlich 😞

    Ob man dann wirklich die kanonischen LINQ-Extension-Methoden definieren will, damit man from ... in ... select ... schreiben kann, ist eine andere Frage. Für Task / Task<T> könnte man das ja auch machen, aber in der Praxis ist es mit async / await viel natürlicher. So wie das Werfen von Exceptions viel natürlicher wäre als der Umgang mit Result / Result<T> 🙂


Anmelden zum Antworten