Nomenklatur: Exceptional<>
-
Spaßeshalber habe ich gerade mal die
Exceptional
-Monade in C# implementiert, also einen generischen TypExceptional<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 TypExceptional
für Funktionen ohne Rückgabewert ergänzen sollte (wir sind ja nicht in Haskell), denn die können auch Exceptions verursachen, wasExceptional<T>
ja abbilden soll. Also habe ich analog dazu einen TypExceptional
definiert (in Analogie zuTask
undTask<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"), aberExceptional
finde ich nichtssagend. Sprechender wäre vielleichtMaybeException
oderMaybeError
oderDiagnostic
, 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
undExceptional<T>
, aber dennoch dieselbe Funktion erfüllen.Habt ihr besser Vorschläge für die Benennung von
Exceptional
undExceptional<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
undResult<T>
zu nennen. Aus einer Funktion, die den Rückgabetypvoid
hat, wird dann eine mit RückgabetypResult
(analog zuHRESULT
), und eine Funktion, die einen Wert vom Typ T zurückgibt, wird zu einer mitResult<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 wieNullable<T>
, und die ist anders alsExceptional<T>
: ersteres liefert einen Wert oder keinen Wert, letzteres einen Wert oder eine Fehlermeldung. Und außerdem, was heißt dannMaybe
undMaybeVoid
? 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 meinetwegenEither<T, Exception>
. Einem Tupel könntest du einT
und eineException
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
undIErrorInfo
, um Exceptions abzubilden, und das Win32-API benutztGetLastError()
und (meistens) einenBOOL
-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ügungDer 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, weilnull
nichtssagend ist: hat die Funktion jetzt einen Bug, weil sienull
zurückgibt, oder bedeutetnull
, daß es keinen Fehler gab? Selbst wenn ich das für die Funktion klarstellen kann, z.B. indem ich sie inGetNameValidationErrorOrNull()
umbenenne, bleibt das Problem bestehen, wenn ich das Funktionsergebnis herumreiche, weil die Bedeutung dernull
nicht im Typsystem abgebildet wird. (Aus solchen Gründen sollte man aufnull
-Objektreferenzen möglichst verzichten und jedesnull
als einen Fehler behandeln.) Also verwendete ich stattdessen einMaybe<Exception>
als Rückgabewert, das zurzeitValueDiagnostic
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 dieExceptional
-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 insbesondereValueDiagnostic<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 einfachResult
nenne. Die Semantik ist jedem vertraut, der mal einHRESULT
gesehen hat, woran der Name eben erinnert; die Erweiterung aufResult<T>
würde einleuchten; und nicht zuletzt gibt es schon einen Präzedenzfall mitTask
undTask<T>
. Ich habe nur Bedenken, weil "Result" so allgemein ist und sich oft genug nur auf den Wert bezieht, etwa inTaskCompletionSource<T>.SetResult()
. AberResultOrException
/ResultOrException<T>
ist lang und häßlichOb man dann wirklich die kanonischen LINQ-Extension-Methoden definieren will, damit man
from ... in ... select ...
schreiben kann, ist eine andere Frage. FürTask
/Task<T>
könnte man das ja auch machen, aber in der Praxis ist es mitasync
/await
viel natürlicher. So wie das Werfen von Exceptions viel natürlicher wäre als der Umgang mitResult
/Result<T>