Kleine Knobelei: Die Kette

»Die Kette« ist eine kleine syntaktische Knobelei, die es in sich hat – mit Lösungen in Python, JavaScript und C++.

Aufgabenstellung

Gegeben ist dieser Ausdruck:

doIt("Hallo")("Welt,")("was")("ist")("das")("denn?")();

Als Ausgabe wird erwartet:

Hallo Welt, was ist das denn?

Dabei gilt: Die Ausgabe soll erst dann erscheinen, wenn kein weiterer String-Parameter mehr »angekettet« wurde. Beispiel:

doIt("Hallo")("Welt,");
// Keine Ausgabe
doIt("was");
// Keine Ausgabe
doIt("geht?")();
// Ausgabe: Hallo Welt, was geht?

Außerdem soll nach der Ausgabe eine neue Kette beginnen und begonnene Ketten beliebig neu aufgenommen werden können. Beispiel:

doIt("Hallo")("Welt")();
// Ausgabe: Hallo Welt
doIt("Bla");
// Keine Ausgabe
doIt("Blubb")();
// Ausgabe: Bla Blubb

Eine letzte Regel: Wenn man nach einem leeren Kettenglied ein weiteres Kettenglied anhängt, soll es zum Fehler kommen. Beispiel:

doIt("Hallo")("Welt")()("bla");
// Ausgabe: Hallo Welt
// Fehlermeldung

Was ist eigentlich zu tun?

Für manchen mag die Lösung zu diesem Rätsel trivial erscheinen, und andere denken einfach zu kompliziert. Daher bietet sich zunächst eine kleine Analyse an.

Ein Funktionsaufruf besteht »normalerweise« aus dem Namen einer Funktion und notwendiger Parameter in der nachfolgenden Klammer, oder auch einer leeren Klammer, wenn es keine Parameter gibt:

funktionsname("Parameter 1", "Parameter 2", 3);
funktionsname();

Die Klammern schließen also den Funktionsaufruf ab – mehr noch: Sie machen den Funktionsaufruf erst zu einem Aufruf. Doch was sehen wir genau in dem Kettenaufruf?

doIt("Hallo")("Welt,")("was")("geht")();
  ↑    ↑    ↑   ↑
  |    |    |   WTF?
  |    |    Funktionsaufruf eigentlich abgeschlossen  
  |    Parameter
  Funktionsname

Nach dem eigentlichen Abschluss des Funktionsaufrufs kommen weitere Klammern – nach der obigen Annahme also weitere Funktionsaufrufe. Das heißt, dass mit dem Ergebnis aus des vorherigen Aufrufs ein neuer Aufruf erfolgt. Und mit dessen Ergebnis wieder ein weiterer Aufruf. Und so weiter und so fort. Irgendwann kommt dann ein Aufruf ohne Parameter.

Das Ergebnis eines Funktionsaufrufs wird mit dem return-Statement einer Funktion geliefert. Also muss etwas »returned« werden, was selbst wieder aufrufbar ist.

Okay, was ist der Trick?

Der »Trick« liegt darin, dass verschiedene Programmiersprachen Referenzen auf Funktionen so als Funktionsergebnis zurück liefern können, dass dieses Ergebnis wieder aufrufbar ist. Andere Programmiersprachen implementieren sogenannte Lambdas (oder anonyme Funktionen), die als Ergebnis zurück geliefert werden können.

Mit diesen Ergebnissen kann dann direkt weiter »arbeiten«.

Die Implementierung der Lösung

Die folgenden Beispiele zeigen mehr oder weniger gute Lösungen für Python und JavaScript, jeweils mit direkten Funktionsreferenzen und mit Lambdas bzw. anonymen Funktionen – als Anregung für Versuche in anderen Sprachen.

Dazu gibt es noch eine krude Lösung in C++.

Python 3

Praktischerweise erlaubt Python die Vorbelegung von Parametern mit Standardwerten, so dass man sich »aussuchen« kann, ob man sie angeben möchte oder nicht. Das erlaubt den Aufruf ein und der selben Funktion mit und ohne Parameter in der Klammer.

Mit Funktionsreferenz

values = []

def doIt(s: str=None):
    global values
    if s is not None:
        values.append(s)
        return doIt   # Funktionsreferenz
    else:
        print(' '.join(values))
        values = []
        return None

doIt("Hallo")("Welt,")("was")("ist")("das")("denn?")()

Mit Lambdas

values = []

def doIt(s: str=None):
    global values
    if s is not None:
        values.append(s)
        return lambda x=None: doIt(x)   # Lambda mit Parameter-Vorbelegung
    else:
        print(' '.join(values))
        values = []
        return None

doIt("Hallo")("Welt,")("was")("ist")("das")("denn?")()

JavaScript

In JavaScript müssen wir keine Vorbelegung von Funktionsparametern durchführen. Nicht angegebene Parameter sind einfach undefined. Ansonsten gleichen die Lösungen sehr stark den Python-Lösungen.

Mit Funktionsreferenz

var values = [];

function doIt(s) {
    if (s) {
        values.push(s);
        return doIt;   // Funktionsreferenz
    }
    else {
        console.log(values.join(' '));
        values = [];
        return null;
    }
}

doIt("Hallo")("Welt,")("was")("ist")("das")("denn?")();

Mit anonymer Funktion

var values = [];

function doIt(s) {
    if (s) {
        values.push(s);
        return function(x) { return doIt(x); };  // Anonyme Funktion
    }
    else {
        console.log(values.join(' '));
        values = [];
        return null;
    }
}

doIt("Hallo")("Welt,")("was")("ist")("das")("denn?")();

C++

doIt wird hier als struct definiert. Hier ist der Trick, dass zunächst der erste String mit dem Konstruktur in einen neuen String-Vektor geladen wird. Dazu gibt es zwei Operatoren: Einen ohne Parameter, der die Ausgabe durchführt; und einen mit String-Parameter, der einen neuen String in den Vektor legt und einen Zeiger auf das aktuelle Struct zurückliefert.

Auch hier ist values global, damit die Anforderung der Wiederaufnahme einer Kette erfüllt werden kann.

#include <iostream>
#include <vector>

using namespace std;

vector<string> values = vector<string>();

struct doIt {

    doIt(string str) {
        values.push_back(str);
    };

    void operator()() {
        for (int i = 0; i < values.size(); i++) {
            if (i > 0) {
                cout << " ";
            }
            cout << values[i];
        }
        cout << endl;
        values.clear();
    };

    doIt operator()(string str) {
        values.push_back(str);
        return * this;    // Referenz auf das aktuelle Struct
    };

};

int main() {
    doIt("Hallo")("Welt,")("was")("ist")("das")("denn?")();
}

Wozu brauche ich das?

Kurz gesagt: Für nichts. Würde so etwas in produktivem Code auftauchen, würde man das sofort verändern: Es ist schlecht lesbar und dazu noch schlecht debugbar. Einfach miserabler Stil. Aber es ist eine nette Knobelei, um sich ein wenig mit den Eigenheiten diverser Programmiersprachen zu beschäftigen.

Viel Spaß also beim Ausprobieren!


Kommentare

  • Andreas Krey schrieb am 13. März 2017 08:30

    Ein Wort: Currying.