Bye, bye, function. Hello, transformer!

Erstellt am 24. September 2013 von Ralfwestphal @ralfw

Funktionen sollten nicht länger die kleinsten Bausteine unseres Codes sein. Sie vermengen nämlich zwei Aspekte: Kontrollfluss und Datenfluss.

Wenn eine Funktion die Kontrolle aufgibt, dann fließen Daten aus ihr heraus. Sie kann danach die Kontrolle nicht zurückbekommen. Erst ein erneuter Aufruf überträgt Kontrolle wieder an sie.

Wenn Daten aus einer Funktion fließen sollen, dann muss sie die Kontrolle aufgeben. Sie kann keine Zwischenergebnisse zur Weiterverarbeitung liefern.

Kontrolle und Daten fließen mit Funktionen also immer gleichzeitig. Das ist oft ok – aber eben nicht immer. Indem wir diese Aspektkopplung jedoch so tief in unseren Sprachen verankert haben, fällt es uns schwer, anders zu denken. Das halte ich aber für nötig, wenn wir evolvierbarere Software herstellen wollen.

Funktionen sind ein Relikt aus der Anfangszeit der Programmierung. Sie sind syntactic sugar für Maschinencodebefehle, die nicht nur ein Unterprogramm aufrufen (CALL), sondern auch noch anschließen ein Resultat für den Aufrufer bereitstellen. Dabei gibt es immer nur einen Punkt, an dem die Kontrolle ist: den Befehl, auf den der eine Befehlszeiger weist. Daten sind in diesem Paradigma wie Hunde, die ihrem Herrchen an der Leine folgen müssen. Sie können immer nur am selben Ort in Verarbeitung gedacht werden, wo auch gerade die eine Kontrolle ist.

Das ist alles wunderbar und verständlich. Das hatte seine lange Zeit. Doch ich glaube, wir sollten jetzt darüber hinaus gehen. Dadurch wird dieses Paradigma nicht überflüssig. Die Newtonsche Physik gilt ja auch weiterhin. Aber wir denken dann nicht mehr, dass alles nur mit diesen Mitteln erklärt und beschrieben werden muss. Die relativistische Physik umfasst die Newtonsche. So sollte auch ein neues Paradigma das bisherige umschließen.

Ist da schon alles mit der Funktionalen Programmierung gesagt? Hm… Nein, ich glaube, nicht. In der steckt ja schon im Namen die Funktion, also das, was wir überwinden sollten.

Aber wie können Kontrollfluss und Datenfluss entkoppelt werden? Mit Continuations bzw. Observern.

Aus einer Funktion

R f(P p) {
  …
  return r;
}

würde z.B. eine Prozedur wie

void f(P p, Action<R> continueWith) {
  …
  continueWith(r);
}

Diese Prozedur hat alle Freiheiten, Kontroll- und Datenfluss zu entkoppeln:

  • Sie kann ein Resultat via continueWith() liefern und dann die Kontrolle aufgeben – oder auch nicht.
  • Sie kann entscheiden, überhaupt ein Resultat zu liefern.
  • Sie kann sogar entscheiden, mehrfach ein Resultat zu liefern.
  • Und schließlich ist eine solche Prozedur auch nicht darauf festgelegt, nur über einen “Kanal” Daten zu liefern.

void f(P p, Action<R> onR, Action<T> onT) {
  …
  onR(r);
  …
  onT(t);
  …
}

Solange der Name der Continuation auf die Prozedur bezogen ist, erhält sie keine Information über den Kontext der Weiterverarbeitung ihres Output. Wie eine Funktion erfüllt sie damit das Principle of Mutual Oblivion (PoMO).

Ich nenne so ein Unterprogramm Transformator. Funktion impliziert gleichzeitigen Kontroll- und Datenfluss. Transformator ist als Begriff hingegen noch nicht verbrannt. Und irgendetwas gibt es ja immer zu transformieren, oder? Zahlen in andere Zahlen, Zeichenketten in andere Zeichenketten oder Zahlen in Zeichenketten oder Zeichenketten in Wahrheitswerte oder in-memory Daten in persistente Daten usw. usf.

Unsere Programmiersprachen sind natürlich für den Umgang mit Funktionen optimiert:

var y = f(x);
var z = g(y);
h(z);

Das lässt sich leicht hinschreiben und lesen. Aber leider ist es begrenzt in seiner Ausdrucksfähigkeit, weil eben Kontrolle und Daten immer gleichzeitig fließen müssen.

Die Alternative mit Transformatoren ist nicht so schön, ich weiß:

f(x, y =>
g(y,
h));

Mit ein wenig Übung kann man das allerdings auch flüssig lesen und hinschreiben. Aber es ist zumindest ungewohnt. Schöner wäre es, wenn man z.B. schreiben könnte:

f –> g –> h

In F# geht das ähnlich – allerdings nur für Funktionen. Bei aller Fortschrittlichkeit ist man auch dort dem alten Paradigma verhaftet. Es sitzt tief in uns.

Wie eine Lösung aussehen kann, weiß ich auch nicht. In meiner Entwicklung von Funktionen hin zu Transformatoren möchte ich mich dadurch aber nicht beschränken lassen. Denken kann ich Transformatoren, visuell darstellen kann ich Transformatoren, codieren kann ich Transformatoren. Da wird sich doch auch eine textuelle Notation finden lassen, oder?

Der Pfeil scheint mir ein passender Operator, wenn mit Transformatoren nun Datenflüsse in den Blick kommen. Vielleicht könnten dann mehrere Flüsse so notiert werden:

f.onR –> g.onY –> h,
f.onT –> m,
g.onZ –> n;

Das Diagramm dazu wäre:

Das ist wunderbar übersichtlich. Der heutige C#-Code hinkt leider hinterher:

f(x, r =>
  g(r,
   h,
   n),
  m);

Aber ich bin unverdrossen. Den Übergang von Funktionen zu Transformatoren halte ich für wichtig. Wir lösen uns damit aus der Umklammerung der von-Neumann-Maschinen. Nicht Kontrolle ist wichtig, sondern Resultate, d.h. der Fluss von Daten.

Kontrolle kann in mehreren Transformatoren gleichzeitig sein. Oder Kontrolle kann vor uns zurück springen, wenn sie denn nur an einem Ort zur Zeit sein kann.

Die Funktion als Werkzeug wird damit nicht überflüssig, sondern nur als Sonderfall neu positioniert. Transformatoren können sich wie Funktionen verhalten, wenn es sein muss. Können, müssen aber nicht. Und das find ich so wichtig. Mit Transformatoren ist entkoppelt, was nicht zwangsläufig zusammengehört. Freiheit für die Daten!