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!
Autor:
Jürgen Edelbluth
Themen:
Knobelei C++ JavaScript Software-Entwicklung Python
Veröffentlicht:
21.09.2016 21:04
Zuletzt aktualisiert:
13.03.2020 16:57