Follow my new blog

Posts mit dem Label Einfacher programmieren werden angezeigt. Alle Posts anzeigen
Posts mit dem Label Einfacher programmieren werden angezeigt. Alle Posts anzeigen

Sonntag, 6. Februar 2011

Spielend programmieren

imageBesser wird es nicht durch Klagen. Besser wird es nur, wenn man sich überhaupt vorstellen kann, wie es besser sein könnte. Dafür muss man sich manchmal frei machen von dem, was ist. Einfach alle Begrenzungen hinter sich lassen. Mal frei fabulieren, wie die Welt aussehen sollte, und beherzt eine Antwort finden auf die Frage: “Ja, wie hätte ich es denn gern, wenn ich mir etwas wünschen dürfte von einer Fee?”

Heute habe ich mir gegönnt, diese Frage für die Programmierung mal für mich zu beantworten. Geplant hatte ich das nicht. Eher bin ich ohne zu fragen über meine Antwort gestolpert.

Wie wünsche ich mir also die Programmierung?

Ich wünsche mir die Programmierung spielerisch(er). Ich wünsche mir, dass Programmieren so funktioniert wie TinkerBox von Autodesk.

TinkerBox ist ein Spiel, in dem man “Maschinen” baut bzw. vervollständigt, um eine Aufgabe zu lösen. Zugegeben, das sind sehr, sehr, sehr einfache Aufgaben im Vergleich zu einer Warenwirtschaft oder einem Compiler oder einer Stellwerkssteuerung.

Aber auch Druckpressen, Autos, Fahrstühle sind sehr viel komplizierter als die TinkerBox-Maschinen und doch funktionieren sie letztlich nach denselben Gesetzen.

Hier ein paar Impressionen von TinkerBox:

Als ich mit TinkerBox angefangen habe auf meinem iPad zu spielen, habe ich einfach das Gefühl gehabt: “Wow, so sollte auch die Programmierung laufen!” Ich möchte Programme visuell zusammensetzen. Ich möchte sie sofort probeweise laufen lassen. Dabei möchte ich zusehen, wie die Teile zusammenspielen.

Natürlich kann das nicht ganz so simpel sein wie bei TinkerBox. Aber warum muss es denn sooooo anders aussehen? Warum muss es aussehen wie heute, wo ich eigentlich nur wie vor 30 Jahren Text in einer imperativen Sprache in einen Editor klopfe? Das kann doch nicht das Ende der Fahnenstange sein. Wir können doch nicht ernsthaft der Meinung sein, mit einer textuellen IDE (und ein bisschen Visualisierung drumherum) die Spitze des Möglichen in der Softwareentwicklung erklommen zu haben.

Nein, ich möchte, dass das anders aussieht. Ich will Software aus Bausteinen aufbauen. Ich will sie zusammenstecken. Ich will sehen, wie sie funktioniert – zum einen, indem ich am Code die Funktionalität ablese, zum anderen, indem ich den Code dabei beobachte.

Manche der Bausteine sind dabei Standardbausteine, andere sind Bausteine, die ich noch “zuhauen” muss, wieder andere setze ich aus anderen zusammen.

image

Bei TinkerBox gibt es nur Standardbausteine. Die Kunst besteht darin, sie in zielführender Weise zu kombinieren. Für die Softwareentwicklung reicht das natürlich nicht. Wir brauchen Bausteine, die wir “parametrisieren” können. In die gießen wir mehr oder weniger normalen Quellcode. Das finde ich ok. Denn die Menge des Quellcodes ist dann überschaubar.

TinkerBox hat in mir den Gedanken verstärkt, dass wir in der Softwareentwicklung Konstruktion und “Kreation” strikt trennen müssen. Wir brauchen beides, aber es sind grundsätzlich verschiedene Tätigkeiten.

Bei der Konstruktion nehme ich Bausteine und setze aus ihnen etwas Größeres zusammen.

Bei der Kreation denke ich mir neue Bausteine aus (oder “parametrisiere” Standardbausteine). (Ob “Kreation” der beste Begriff dafür ist, lasse ich mal dahingestellt. An dieser Stelle wollte ich aber nicht Implementation schreiben.)

Bausteine zusammenstecken, sie zu einem funktionierenden Ganzen fügen, das ist etwas ganz anderes, als Bausteine zu entwickeln, zu kreieren.

In der Softwareentwicklung trennen wir aber bisher nicht sauber, sondern sind im Grunde ständig mit der Kreation beschäftigt. Die jedoch ist viel schwieriger als die Konstruktion. Der einfache “Beweis”: Selbst Kinder können mit Legobausteinen tollste Dinge konstruieren – kreiert haben aber nicht Kinder die Legobausteine, sondern Erwachsene.

Genauso ist es mit Excel. Millionen von Poweruser können aus den Standardbausteinen in Excel tollste “Rechengebilde” konstruieren. Kreiert haben diese Standardbausteine jedoch Programmierer.

So ganz neu ist die Vorstellung, die ich hier äußere, natürlich nicht. Von Komponenten, die per glue code nur noch verbunden werden müssen, träumte man schon in der 1990ern oder gar davor. Realisiert ist diese Vision aber nur sehr begrenzt.

Mir geht es auch nicht darum, Laien zu Softwareentwicklern zu machen. Ich möchte den Softwareentwicklern nur das Leben erleichtern. Sie sollen sich mehr auf das Wesentliche konzentrieren. Das – so stelle ich mir in einem kühnen Traum vor – können sie aber besser, wenn Programme visueller machen. Die Anhaftung an Text als primärem Ausdrucksmittel für Software, ist anachronistisch und kontraproduktiv. Ich kenne keine andere Branche, in der Designdokumente primär textueller Art sind; nur die Softwareentwicklung beharrt darauf. Denn Programmierung ist Design.

Also: Befreien wir uns von der Last der Texte! Machen wir die Softwareentwicklung haptischer. Entlasten wir uns durch Trennung von Konstruktion und Kreation. Ich glaube, dann wird vieles besser in der Programmierung. Denn dann können wir besser über Software reden und wir können dann besser gemeinsam an Software arbeiten.

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 ;-)