- Dieser Text erzählt in satirischer Form von einer Bewerbungssituation, in der ein Kandidat versucht, das einfache FizzBuzz-Problem mit reinen Funktionskombinatoren auf Basis des Lambda-Kalküls zu implementieren
- Die Hauptfigur beginnt mit JavaScript, definiert dann aber nach und nach die S-, K- und I-Kombinatoren und rekonstruiert so die grundlegende Struktur der Sprache selbst
- Anschließend werden Boolesche Werte, Zahlen, Listen und Strings ausschließlich mit Kombinatoren dargestellt, und über den Y-Kombinator wird Rekursion implementiert
- Am Ende wird FizzBuzz in einem vollständig point-free Stil fertiggestellt, doch der Code wächst zu einem Ausmaß an, das für Menschen nicht mehr verständlich ist
- Der Text erkundet auf humorvolle Weise das Wesen von Programmiersprachen und die Extreme der Abstraktion und zeigt dabei die selbstironische Seite der Entwicklerkultur
Der Beginn des Interviews: FizzBuzz und Kombinatoren
- Die Geschichte beginnt mit der Szene, in der die Interviewerin Dana den Kandidaten bittet, das FizzBuzz-Problem zu lösen
- Statt einer gewöhnlichen Schleife beginnt der Kandidat damit, die S- und K-Kombinatoren zu definieren
- Dana ist irritiert, aber der Kandidat sagt nur, dass „es gleich geschafft ist“, und macht weiter
- Der Kandidat definiert weitere Kombinatoren wie I, B, C, W, T und verwendet sie als grundlegende Bausteine einer neuen Sprache
- Jeder Kombinator implementiert zentrale Konzepte des Lambda-Kalküls wie Funktionskomposition, Argumentvertauschung oder Selbstanwendung
- In den Code-Kommentaren tauchen auch Spitznamen wie „Bluebird, Cardinal, Warbler, Thrush“ für diese Kombinatoren auf
Die Definition von Booleschen Werten und Zahlen
- Der Kandidat definiert TRUE, FALSE, NOT ausschließlich mit Kombinatoren und baut damit eine boolesche Logik auf
- Dana fragt, ob das Lambda-Kalkül sei, worauf der Kandidat antwortet, das Lambda-Kalkül sei „zu aufgebläht“
- Danach definiert er ZERO, SUCC, PRED, IS_ZERO und konstruiert so ein Zahlensystem (Church numerals)
SUCC implementiert den Nachfolger, PRED den Vorgänger, und DECREMENT eine Verringerung, die nicht unter 0 fällt
- Die Zahlen von
ONE bis TEN werden der Reihe nach definiert
Rekursion und der Y-Kombinator
- Der Kandidat definiert die Funktion ADD und wandelt sie schrittweise in eine point-free-Form um
- Er definiert
ADD_MAKER und umhüllt es mit dem Y-Kombinator, um Rekursion zu ermöglichen
- Dana merkt an, dass Rekursion auch in JavaScript möglich sei, doch der Kandidat erwidert, dass es „bald kein JavaScript mehr sein wird“
- Als wegen der strikten Auswertung (eager evaluation) von JavaScript ein Stack Overflow auftritt, führt der Kandidat den Code in einem „lazy“ JavaScript-Interpreter namens Skoobert aus
- Das Ergebnis gibt erfolgreich
3 aus
Arithmetik, Listen und Strings
- Der Kandidat implementiert arithmetische Operationen wie SUBTRACT, MULTIPLY, MOD, DIVIDE vollständig auf Basis von Kombinatoren
- Jede Operation wird rekursiv aus
IS_ZERO, PRED, SUCC und ähnlichen Bausteinen zusammengesetzt
- Danach definiert er CONS, FIRST, REST, EMPTY und baut damit eine Listenstruktur auf
- Auch funktionale Higher-Order-Operationen wie
MAP, FOLD, RANGE, CONCAT werden vollständig in Kombinatorform umgesetzt
- Um Listen als Strings auszugeben, schreibt er die Funktionen
renderList und showLines
- Dana scheint inzwischen bereits aufgegeben zu haben und reagiert kaum noch
Aufbau von Zeichen und Strings
- Der Kandidat definiert Zeichencodes von 1 bis 9 sowie in 10er-Schritten mithilfe von Kombinatoren
- Von
CHAR_A bis CHAR_Z und von CHAR_0 bis CHAR_9 wird alles als Zahlenkombination ausgedrückt
- Über die Funktion
ARRAY werden Strings in Listen umgewandelt und daraus FIZZ, BUZZ, FIZZBUZZ erzeugt
- Mit
extractString wird die Liste wieder in einen echten String umgewandelt
- Die Konsolenausgabe ist
"fizzbuzz"
Umwandlung von Zahlen in Strings
- Es werden
NUMBER_TO_DIGIT_LIST zur Umwandlung einer Zahl in eine Liste ihrer Ziffern sowie NUMBER_TO_STRING zur Umwandlung in einen String definiert
- Mit
DIVIDE, MOD, TEN und weiteren Bausteinen werden die einzelnen Stellen getrennt
- Über die Liste
DIGITS_NUMERAL erfolgt das Mapping auf Zahlenzeichen
- Dadurch wird die Umwandlung Zahl → Zeichen → String vollständig innerhalb des Kombinator-Systems möglich
Die Vollendung von FizzBuzz
FIFTEEN und ONE_HUNDRED werden definiert, und mit MAP und RANGE wird eine Liste von 1 bis 100 erzeugt
- Für jede Zahl wird geprüft, ob sie ein Vielfaches von 3, 5 oder 15 ist, und entsprechend
"Fizz", "Buzz", "FizzBuzz" oder der Zahlen-String ausgegeben
- Mit
showLines(extractString)(FIZZBUZZ_RESULT) wird das Endergebnis ausgegeben
- Dana fragt: „Bist du jetzt zufrieden?“, doch der Kandidat entfernt alle Variablendefinitionen und schreibt den Code nur noch mit reinen Kombinatoren neu
- Das Ergebnis ist ein mehrere tausend Zeilen langer riesiger Ausdruck, der nur aus S und K besteht
Schluss und Bedeutung
- Der Text erforscht die grundlegenden Bausteine von Programmiersprachen und persifliert zugleich die Tendenz von Entwicklern, einfache Probleme unnötig zu verkomplizieren
- Der Titel „Mit weniger als nichts programmieren“ symbolisiert den Versuch, die Welt aus den kleinsten Einheiten einer Sprache neu zu konstruieren
- Das Werk ist technische Literatur, die ein tiefes Verständnis von funktionaler Programmierung, Lambda-Kalkül und Kombinatorlogik mit Humor und Erzählung verbindet
- Zugleich zeigt es satirisch den „Entwicklergeist, der sogar FizzBuzz in ein philosophisches Experiment verwandelt“
1 Kommentare
Hacker-News-Kommentare
Da viele sich offenbar fragen „Was soll das eigentlich?“, hier der Kernpunkt von combinatorn:
Ein combinator ist eine Funktion, die keinen globalen Zustand verändert und keine externen Variablen einfängt. Gibt man ihr dieselben Argumente, liefert sie immer dasselbe Ergebnis und beeinflusst den Rest des Systems überhaupt nicht.
Kombiniert man solche reinen Funktionen, entsteht wieder ein weiterer combinator. Das heißt: Die Komposition reiner Funktionen bleibt weiterhin eine reine Funktion.
Solche Funktionen lassen sich auch problemlos in Assembler oder C schreiben. Wenn man zum Beispiel S und K direkt auf x64 implementiert und darauf Common Lisp combinator-basiert kompiliert, läuft Lisp letztlich auf zwei Assemblerfunktionen.
Die Performance wäre wohl nicht besonders gut, aber wenn man ein paar Hundert häufig verwendete Muster als Inline-Combinator definiert und benennt, wird daraus eine durchaus praktische „Maschine“.
Diese Struktur ähnelt auch Forth, das mutation scheut, einer Bytecode-VM oder einer CPU, die combinatoren als „Instruktionen“ bezeichnet.
Letztlich kann man combinatoren als einen Ausdruck der angewandten Informatik verstehen, bei dem Implementierungsdetails so weit wie möglich ignoriert werden. Deshalb kann man sagen, dass sie einfacher als Lambda-Kalkül sind.
Wenn man Lambda-Kalkül mit einigen combinatoren implementiert und darauf Lisp aufsetzt, reduziert sich die Menge an maschinenabhängiger Arbeit ganz unten im Stack extrem.
Das muss man nur ein paarmal zusammensetzen.
„Ich benutze niemals Lambda-Kalkül. Das ist eine viel zu aufgeblähte Sprache.“
Tatsächlich ist combinatory logic aber noch aufgeblähter. Um dasselbe Programm auszudrücken, braucht man mehr Bits.
Lambda-Kalkül ist vielmehr eine der kompaktesten Sprachen, die es gibt.
Selbst in einer eager Sprache wie JavaScript kann man etwas lazy machen, indem man Argumente in Dummy-Lambdas verpackt. Ein Beispiel gibt es in diesem JS-BLC-Interpreter.
Siehe dazu auch dieses PDF und das IOCCC-Beispiel.
uni.c) Lambda-Ausdrücke letztlich in combinatory logic umwandeln und so auswerten. Sie verwenden nur eine größere Grundmenge als S und K.w?“„Kennst du FizzBuzz?“
„Natürlich. Die 10-Zeichen-Codegolf-Version, die 12-Zeilen-Version, die selbst ein Junior versteht, oder die völlig absurde Version zum Angeben mit obskurem Wissen?“
Ergebnislink
Die Erklärung von combinator logic war wirklich großartig. Der Schreibstil erinnerte mich an diese Serie.
Das erinnerte mich an den Artikel „FizzBuzz in TensorFlow“, den ich vor Jahren gelesen habe.
Link
Dieses Framing von Programmierinterviews wirkt etwas verfehlt.
Der Zweck eines Interviews ist, (1) das Programmierwissen des Bewerbers zu prüfen und (2) zu sehen, ob er zur Programmierkultur des Unternehmens passt.
Wenn man zum Beispiel für eine JavaScript-Position rekrutiert und jemand tief in combinatory logic steckt, passt das kulturell womöglich nicht. Dann ist eine Absage auch nicht seltsam.
Es ist Zeit, sich an Moses Schönfinkel zu erinnern.
Er erfand 1920 als Schüler Hilberts die combinatory logic. Nach seiner Rückkehr nach Russland litt er an psychischen Erkrankungen und starb verarmt in Moskau. Laut Wikipedia wurden seine Aufsätze von Nachbarn zum Heizen verbrannt.
Der letzte Codeblock ist so groß, dass ein Scrollbalken erscheint (166 KB).
S und K sind curried Funktionen, die immer nur ein Argument gleichzeitig annehmen.
Mehr dazu in dieser StackOverflow-Antwort.
Ich liebe diese Vogelnamen wie „Bluebird, cardinal, warbler, thrush“. Ich möchte das als neue Benennungskonvention übernehmen.
„Barendregt. Church ist doch viel zu Mainstream.“
„Bald ist es nicht mal mehr JavaScript.“
Es wirkt, als würde Douglas Adams SICP unterrichten.
„Ist das also … der Y combinator?“
„Genau. Für Rekursion braucht man ihn.“
Fun Fact: Es gibt unendlich viele Fixpunktkombinatoren. Man kann Rekursion also auf unendlich viele Arten auch ohne den Y combinator implementieren.