Follow my new blog

Montag, 3. Mai 2010

Nullonade – Monade statt Null

In der Diskussion um Null als Rückgabewert gab es ein Codeschnippsel, mit dem Thomas Bandt zeigen wollte, dass der TryGetUserById(…)-Vorschlag nicht zwangsläufig zu intuitivem Code führt. Ich formuliere das Beispiel hier mal etwas knapper:

User tmpUser;
bool isAuth = repo.TryGetUserById("987", out tmpUser) &&
              sec.CheckUser("somepassword", tmpUser);

Als Problem sieht Thomas – durchaus zurecht – die Abhängigkeit von CheckUser() von einem Seiteneffekt. Die globale Variable tmpUser muss vor Aufruf gesetzt sein, was innerhalb des logischen Ausdrucks nicht ganz so offensichtlich ist.

Etwas deutlicher wäre die Abhängigkeit der Reihenfolge beider Methodenaufrufe, wenn der Ausdruck traditioneller als Schachtelung formuliert wäre:

bool isAuth;
User tmpUser;
if (repo.TryGetUserById("987", out tmpUser))
    isAuth = sec.CheckUser("somepassword", tmpUser);
else
    isAuth = false;

Doch eigentlich sind Schachtelungen ja “böse” ;-) Sie erhöhen die (zyklomatische) Komplexität des Codes. Also vielleicht lieber so:

User tmpUser;
bool isAuth = repo.TryGetUserById("987", out tmpUser);
isAuth = isAuth && sec.CheckUser("somepassword", tmpUser);

Das mag besser lesbar sein – doch das Grundproblem ist eigentlich nicht gelöst. Das besteht in der Abhängigkeit selbst, also letztlich in der zu beiden Methoden globalen Variablen tmpUser. Die ist nötig, weil Thomas keine Exception werfen möchte, falls es den angeforderten User nicht gibt. Wäre das anders, könnte er nämlich auch ohne diese Variable formulieren:

bool isAuth = sec.CheckUser(“somepassword”,
                            repo.GetUserById(“987”));

Das ist zwar wieder geschachtelter Code, doch irgendwie sind wir sowas ja gewohnt ;-)

Get into the flow

So richtig cool finde ich das aber alles noch nicht. Deshalb schlage ich mal eine weitere Alternative vor. Wäre es nicht am allerschönsten, wenn wir es so schreiben könnten:

var isAuth = repo.GetUserById(“987”) |> sec.CheckUser(“somepassword”);

Das ist natürlich Pseudocode. Aber schön wärs, oder? GetUserById() würde immer noch keine Exception werfen und trotzdem (!) würde das Ganze immer funktionieren.

In F# geht das. Da gibt es so einen Pipe-Operator |> und noch etwas anderes, das nötig ist, einen Option-Typ. Aber in C# sitzen wir erstmal auf dem Trockenen :-(

Zum Glück können wir daran etwas tun. Wir können mit etwas Vorarbeit auch in C# so einen “Flow” von Anweisungen schreiben. Hier meine C#-Version:

bool isAuth = repo.GetUserById("987")
                  .Continue(user => sec.CheckUser("somepassword", user))
                  .WithDefault(false);

Liest sich doch gar nicht schlecht, oder? Es soll ein User geladen werden und danach soll geprüft werden, ob der User ein bestimmtes Passwort hat. Ist das so, dann ist das Ergebnis true, ansonsten false.

Die Sequenz der Verarbeitungsschritte in unserem Kopf findet sich im Code wieder – ohne dass wir eine temporäre Variable bräuchten. Naja, nicht ganz: denn der user-Parameter der Lambda-Funktion ist eine solche temporäre Variable. Doch die ist nicht global, sondern lokal. Es gibt also keine schwer zu überblickenden Abhängigkeiten.

Optionen als Zwischenwerte

Möglich ist so eine Formulierung, wenn als Resultat von GetUserById() kein User (oder gar Null), sondern ein sog. Option-Wert zurückgeliefert wird. Den gibt es in F# gratis; für C# können wir ihn so bauen:

public class Option<T>
{
    public Option()
    {
        this.IsSome = false;
    }

    public Option(T some)
    {
        this.Some = some;
        this.IsSome = true;
    }

    public T Some { get; private set; }

    public bool IsSome { get; private set; }
    public bool IsNone { get { return !IsSome; } }

    …
}

Der Trick an Optionswerten ist, dass es immer eine Instanz gibt. Es geht also nicht um Objekt oder Null, sondern um Options-Objekt gefüllt oder nicht gefüllt. Wenn es gefüllt ist, dann liefert IsSome true, wenn nicht, dann liefert IsSome false bzw. IsNone true. Zugriff auf den Wert gibt die Some-Property.

var optInt = new Option<int>(42);
Console.WriteLine(“{0}, {1}”, optInt.IsSome, optInt.Some);

gibt aus:

true, 42

Mit so einem Typen kann GetUserById() nun umformuliert werden zu:

Option<User> GetUserById(string id) {…}

Die Methode liefert immer ein Option-Objekt zurück und die Umgebung muss prüfen, ob etwas drin steckt oder nicht:

var optUser = repo.GetUserById(“987”);
if (optUser.IsSome)
    …

Das ist fast so wie eine Prüfung auf Null – aber besser. Denn mit einem Option-Objekt kann man immer weiterarbeiten.

Logik verstecken in einer Monade

Das Ziel ist immer noch der “Flow”, d.h. globale Variablen und Prüflogik auf unerwünschte Ergebnisse zu verstecken. Weil nun immer ein Option-Objekt vorliegt, können wir darauf immer eine Methode aufrufen

public class Option<T>
{
    …

    public IEnumerable<Option<TOutput>> Continue<TOutput>(Func<T, TOutput> processor)
    {
        return new[] {this}.Continue(processor);
    }
}

und auch noch Erweiterungsmethoden in Anschlag bringen

public static class OptionExtensions
{
    public static IEnumerable<Option<TOutput>> Continue<TInput, TOutput>(
                                     this IEnumerable<Option<TInput>> values,
                                     Func<TInput, TOutput> processor)
    {
        return values.Where(v => v.IsSome)
                     .Select(v => new Option<TOutput>(processor(v.Some)));
    }

    public static TInput WithDefault<TInput>(
                                     this IEnumerable<Option<TInput>> values,
                                     TInput defaultInCaseOfNone)
    {
        return (values.FirstOrDefault() ??
                     new Option<TInput>(defaultInCaseOfNone)).Some;
    }
}

Ich denke, damit habe ich eine Monade definiert, d.h. ein “Dings”, mit dem sich quasi unsichtbar ein Kontext um mehrere Anweisungen wickeln lassen kann.

Dass ich dafür auf IEnumerable<> zurückgegriffen habe, ist lediglich meiner Faulheit geschuldet ;-) (Mit IEnumerable<> kriegen ist nämlich die Verkettung geschenkt, weil der Typ selbst schon eine Monade ist.) Ebenso die Methode Continue() der Option-Klasse. Hätte ich mir mehr Mühe gegeben, hätte ich statt IEnumerable<> eine spezielle Monadenklasse gebastelt. Das wäre sauberer gewesen.

Für den Punkt, den ich hier rüberbringen wollte, geht es aber auch so, würd ich sagen. Ich möchte einfach einen weiteren Weg aufzeigen, ohne Null leben zu können – und dadurch lesbaren Code zu bekommen. Die Verkettung könnte noch weiter gehen, falls mehr “Transformationsschritte” nötig wären, die immer wieder Output erzeugen, der Input für den nächsten sind. Die Leistung der Monade besteht darin, die Schritte abzubrechen, falls kein Ergebnis geliefert wird (der frühere Null-Fall). Die verbirgt die Logik der Prüfung, ob ein Zwischenergebnis vorliegt und es noch weitergehen soll.

Die C#-Mittel bleiben dabei immer etwas hinter denen von F# zurück. Doch mir scheint “das Denken in Option-Werten” durchaus den einen oder anderen Versuch wert. Wie gesagt, mit etwas mehr einmalig aufgewandter Zeit, ist das auch noch eleganter ;-)

9 Kommentare:

Mike Bild hat gesagt…

Wie hier (http://www.gmbsg.com/null-toleranz/) kommentiert, finde ich deinen beschriebenen Ansatz deiner "Nullonade" :-) als hervorragende Überleitung zur Problemlösung der NULL-Diskussion. Sehr cool! Danke! Das gesamte Posting und die Kommentare über alle Blogs hat Referenzcharakter für die Probleme der Softwareentwicklung. Zu NULL nur noch soviel - 2 Heuristiken zu NULL (Keine Null zurückgeben, Keine Null übergeben) wurden im übrigen auch schon im Buch das jeder kennen sollte - Clean Code - Robert C. Martin, wie ich finde, genügend beleuchtet. Die Diskussion finde ich deshalb ja auch so prägend. Na dann, vielen Dank nochmal dafür.

Björn Rochel hat gesagt…

Option.bind lässt grüßen ;-)

Ist schon interessant, wie funktionale Programmierung die Perspektive verändert, oder?. Noch vor ein paar Wochen hätte ich das nie gedacht, aber seitdem ich mich intensiv mit F# befasse, stoße ich immer wieder auf Dinge, die ich als "gegeben" angenommen habe und es tatsächlich nicht sind. Null vs. Discriminated Unions ist ein gutes Beispiel hierfür. Immer wieder interessant zu sehen, wie andere Sprachen solche Probleme idiomatisch lösen. Das Buch "Real World Functional Programming" zeigt übrigens etwas Ähnliches in C# und F# side-by-side.

Danke noch mal für den Anstoß F# genauer anzusehen ;-)

Mike Bild hat gesagt…

@Björn: Ja das stimmt, sehr prägend. Ich versuche jedoch, meiner zu wackligen Beziehung zu F# geschuldet, weiterhin funktionale Pattern und F# Sprachfeatures nach C# zu transportieren. Hier bin ich mit F# codierten Funktionseinheiten in meinem Team zu einsam. :-( Sei es drum... mit funktionalen Pattern finden wir für einige, sagen wir mal unschöne, Problemchen einfach sehr elegante allgemeine Lösungen. Zum Buch passt auch noch sehr gut http://blogs.msdn.com/wesdyer/archive/2008/01/11/the-marvels-of-monads.aspx

-Mike

Rainer Hilmer hat gesagt…

Hallo Ralf,
ich verstehe deine Option Klasse nicht. Je nachdem ob die interne Funktionalität von GetUserById einen User erhält oder nicht (null), eine Prüfung auf null machen. Ist das interne Ergebnis ein legitimes User-Objekt, wird eine Option-Instanz über den parametrisierten Konstruktor gebildet. Ist das Ergebnis null, muß der parameterlose Konstruktor benutzt werden. Wenn ich das so richtig verstanden habe, wäre das nur eine Verlagerung der null-Prüfung. Wo ist dann der Nutzen?

Ralf Westphal - One Man Think Tank hat gesagt…

@Rainer: Klar, irgendeiner muss eine Entscheidung darüber treffen, was passieren soll. Immer.

Bei Rückgabe von Null sind es aber unabsehbar viele Stellen im Code, die zwangsläufig entscheiden müssen. Müssen!

Bei Rückgabe einer Option muss eine Entscheidung aber nur einmal getroffen werden. Alles andere kann - wie die Monade zeigt - außerhalb des Anwendungscodes in Infrastruktur laufen.

-Ralf

Andreas Graf hat gesagt…

Der Argumentation zu zyklomatischer Komplexität kann ich nicht folgen. Die ersten drei Beispiele sind IMHO von gleicher zyklomatischer Komplexität.

Ralf Westphal - One Man Think Tank hat gesagt…

@Andreas: Mit Monaden werden oft Fallunterscheidungen im Anwendungscode überflüssig. Dadurch sinkt die zykl. Komplexität.

-Ralf

Sebastian Jancke hat gesagt…

Hallo Ralf,

ich musste doch sehr schmunzeln, über die Diskussion und nun deinen Nullonade ;-) Scala hat genauso ein Teil: "Option". Dies kam vor ca. einem Monat in einer Diskussion "hoch" und hat mich und Kollegen zum kritischen Nachdenken über die Verwendung von "null" geführt. Fazit: Meist drücken wir mir "null" ein optionales Ergebnis, einen optionalen Wert aus. Tun wir dies mit "null" ist das un-explizit. Wir vergeben eine Chance für ein explizites, klar lesbares, Konzept.

Darum habe ich vor 2 Wochen den Typ "Option" mit dem konkreten Ausprägungen "Vorhanden" und "Nichts" ins Projekt eingefügt. Darum gibts dann noch eine schöne kleine DSL zur sehr lesbaren Konstruktion. Eine Vorhandene Option lässt sich natürlich nicht mit dem Wert "null" erzeugen ;-)

Als Effekt sieht man bei Konstruktion, Verwendung und in Signaturen was wirklich optional ist.

Eigentlich fehlt könnte dieses Konzept "Standard" in .NET Framework und JDK werden...

Grüße,
Sebastian

Andreas hat gesagt…

Hallo,

hat jemand ein volles Beispiel zum ansehen?
Vielleicht könnte er es bitte hier posten.

Danke Andreas