Sunday, September 9, 2007

IO und funktionale Programmierung in C#

Zusammenfassung
Das seiteneffektfreie Programmieren ist in der funktionalen Programmierung ein wichtiger Grundsatz, welcher in der imperativen Programmierung (z.B C#) oft vernachlässigt wird obwohl dadurch höhere Nachvollziehbarkeit, Wiederverwendbarkeit und Testbarkeit erreicht werden kann. Im folgenden wird Anhand eines Beispiels gezeigt wie eine Konsolenausgabe seiteneffektfrei realisiert werden kann.

Einleitung
In der funktionalen Programmierung werden Seiteneffekte und Zustände wie der Teufel das Weihwasser gescheut. Leicht passiert es dass durch Seiteneffekte Methoden und Klassen, bei entsprechend ungeeignetem Design, ein nicht deterministisches Verhalten zeigen, was Zuverlässigkeit, Stabilität und Fehleranalyse von Programmen umgemein behindert. IO-Operationen können als Beispiel für Funktionalitäten dienen welche gezwungenermassen Seiteneffekte aufweisen.
string line = System.Console.ReadLine();


Wartet das Programm im Methodenaufruf ReadLine() auf eine Benutzereingabe, so ist nicht sicher ob der Aufruf jemals einen Wert zurückliefert (z.B falls der Benutzer am Bildschirm eingeschlafen ist und somit keine Eingabe macht). Weiter liefert der Aufruf ReadLine() bei jedem Aufruf unter Umständen einen anderen Wert zurück, obwohl aus Sicht des Programmes nichts geändert hat. Dieses Verhalten ist in der imperativen Programmierung (C#) normal, in der funktionalen Programmierung aber verpönnt da in dem darunterliegenden mathematischen Verständnis der Programmierung kein Platz für Nichtdeterminismus und Zufall ist. Auch die Methode System.Console.WriteLine() macht aus mathematischer Sicht wenig Sinn da sie keinen Rückgabewert hat und den Zustand des Programmes nicht verändert.

Beispiel
Wie könnte man nun die Seiteneffekte für folgendes Beispiel beheben?
static void Main(string[] args)
{
int index = 0;
var numbers = new int[] { 1, 3, 7, 10 };
foreach (var number in numbers)
{
System.Console.WriteLine("Index {0}: {1}", index++, number);
}
System.Console.ReadLine();
}

Lösung

Die Seiteneffekte können zwar nicht ganz eliminiert werden da eine Ausgabe auf die Console nun mal erwünscht ist. Die Idee ist es den Seiteneffekt an eine unkritische Stelle zu verschieben und den Zustand der gewünschten Ausgabe explizit abzubilden (wie dies ein Monad in der funktionalen Programmierung macht).

static void Main(string[] args)
{
System.Console.Write(
new int[]{1,3,7,10}
.Select( (number,index)=> string.Format("Index {0}: {1}{2}", index,number,Environment.NewLine))
.Aggregate( (lines,line) => lines + line));
System.Console.ReadLine();
}

Unter Verwendung von LINQ wird mit Select und string.Format() jedes Listenelement in eine Ausgabenzeile projiziert. Danach werden alle Zeilen mit Aggregate und 'lines + line' in einen einzigen String aggregiert welcher am Schluss auf die Konsole ausgegeben werden kann.

Da unsere Funktion nun die Ausgabe explizit abbildet, können wir Tests schreiben und prüfen ob die Ausgabe korrekt ist:


[TestMethod]
public void TestOutput()
{
string output = new int[]{1,3,7,10}
.Select( (number,index)=> string.Format("Index {0}: {1}{2}", index,number,Environment.NewLine))
.Aggregate( (lines,line) => lines + line);
Assert.AreEqual( "Index 0: 1" + Environment.NewLine +
"Index 1: 3" + Environment.NewLine +
"Index 2: 7" + Environment.NewLine +
"Index 3: 10" + Environment.NewLine , output);
}



No comments: