Language Design Is Not Just Solving Puzzles

Language Design Is Not Just Solving Puzzles ist ein recht interessanter Artikel von Guido van Rossum über die Unmöglichkeit einer eleganten Syntax für mehrzeilige Lambdas in Python. Lesenswert und in weiten Teilen stimme ich ihm zu. Allerdings stolpere ich dann über so einen letzten Absatz:

And there's the rub: there's no way to make a Rube Goldberg language feature appear simple. Features of a programming language, whether syntactic or semantic, are all part of the language's user interface. And a user interface can handle only so much complexity or it becomes unusable. This is also the reason why Python will never have continuations, and even why I'm uninterested in optimizing tail recursion. But that's for another installment.

Ich bin durchaus bereit zu akzeptieren das Continuations komplex sind - aber nicht wegen des Interfaces. Denn im Interface für Continuations braucht man nur den callcc Aufruf zum Binden der Continuation und eine einfache Funktionssyntax zum Auslösen der Continuation. Das Hauptproblem bei Continuations liegt in der Kooperation mit Generatoren und Exceptions - was passiert, wenn eine Continuation innerhalb eines Generators ausgelöst wird? Was passiert, wenn innerhalb einer Continuation eine Exception ausgelöst wird? Das sind die schwierigen Aspekte - die übrigens auch Scheme-Implementatoren zum Schwitzen bringen, weshalb bei denen in der Regel Exceptions nicht so gerne gesehen werden (gleiches Problem, einfach nur aus der anderen Richtung betrachtet).

Also ok, keine Continuations in Python - auch wenn wir schon längst poor-mans-continuations mit pickable generators bekommen (oder mit Greenlets, oder mit cloneable Coroutines, oder einer der anderen vielen Ansätze um Subsets von Continuation-Features zu erhalten).

Aber was bitte schön ist komplex an Tail-Call-Optimization (denn es geht nicht nur um Tail-Recursion)? Die ist so primitiv, das sie transparent für den Programmierer implementiert werden kann - wenn ein Tailcall vorliegt, keine Rücksprungadresse auf dem Stack notieren, sondern die Parameter im Stackframe umladen und einen einfachen Jump notieren. Wenn man nett sein will, kann man noch eine Pseudofunktion "tailcall" einführen, welche eine Exception auslöst wenn sie nicht in einer Tailcall-Position ausgeführt werden soll. Es mag weitere Bedingungen geben, unter denen Tailcalls nicht optimiert werden können - aber diese können genauso in eine entsprechende Prüfung einfließen.

Gerade der Funktions-Overhead ist es ja, der manche Algorithmen in Scriptsprachen nur unschön implementierbar macht. Und Tail-Call-Optimization würde da definitiv helfen. Ganz besonders in Situationen, in denen man eine Kette von kleinen Funktionsaufrufen hat. Wegen meiner kann es auch gerne eine Optimierung sein, die nur bei -O (oder einem -O2 oder sonstwas) aktiviert wird.

tags: Programmierung, Python

bwolf Feb. 13, 2006, 10:58 p.m.

Tja, das sind genau die Gründe für mich, immer wieder zum meinem geliebten Scheme - oder auch gerne CL - zurück zu kehren. Python hat für mich viele Vorteile, weile ich darin sehr schnell Ideen umsetzen kann. Aber "leider" denke ich viel zu rekursiv - das ist für mich, obwohl nur Gewohnheit - einfach intuitiver. Umso trauriger ist es für mich, dass GvR hier einige Dinge durcheinander bringt. Und mit Problemen habe ich hier immer wieder zu kämpfen. So z.B. sind generatoren für mich ein völlig überflüssiger first-class-citizen. Das sind für mein Verständnis nur workarounds.
Was ich nicht verstehe ist, warum continuations und exceptions ein Problem sein sollen? Wenn man exceptions als vereinfachtes Konzept einer continuation versteht, dann sollten doch ein gegenseitiges Vorkommen kein Problem sein? Bei generators sehe ich das auch so. Schließlich ist ein generator nur eine vereinfachte continuation.
Oder verstehe ich Dich letztlich nur falsch?



hugo Feb. 14, 2006, 9:03 a.m.

Ne, war etwas blöd ausgedrückt von mir. Eigentlich sinds nicht die Exceptions selber, sondern die dynamischen Catch-Umgebungen (das gleiche gilt für Finalizations). Also nicht der "raise", sondern das "try:except:" und ganz besonders sein Kumpel "try:finalize" (bzw. natürlich die entsprechenden Teile in Scheme - "unwind-protect" oder "with-open-file", die ja auch mit Finalization arbeiten).

Wenn du Continuations in einem Stack von dynamischen Exception-Handlern oder Finalizations speicherst, kannst du dort jederzeit wieder hineinspringen. Auch wenn die für diese dynamischen Handler notwendige Umgebung nicht mehr korrekt ist.

Also stell dir einfach einen Code vor, der eine Datenbank-Transaktion startet und mittels try:except:else: dann bei Fehlern ein rollback und bei Erfolg ein commit macht. In dem Code innerhalb des try: speicherst du eine Continuation. Jetzt wird irgendwann der Code entweder durch Exception oder normal beendet - die offene Transaktion also geschlossen. Irgendwann später aktivierst du die Continuation - aber deine dynamische Umgebung passt nicht mehr, die Transaktion ist ja schon weg (und kann nicht reaktiviert werden).

Das Hauptproblem ist also eigentlich das Zusammenspiel von dynamischen Umgebungen und äußeren Zuständen - Datenbanken, offene Dateien, was auch immer über die dynamischen Umgebungen gekapselt werden soll (dafür werden diese ja sehr oft eingesetzt - bei Lisp mit "with-open-file" und Friends ja sehr viel expliziter) - wo die äusseren Zustände nicht Bestandteil der gespeicherten Continuation sein können.

Eigentlich ist das Problem - wenn man es theoretisch betrachtet - darin zu suchen, das die Continuation nur die funktionale Umgebung, nicht aber die prozeduralen Abläufe umfasst.

Das gleiche hast du dann bei Exceptionhandling auch: was ist, wenn in einem except: Block eine Continuation gespeichert wird und später aktiviert wird? Die Fehlersituation kann ja in der Regel nicht wieder hergestellt werden (z.B. wenn es ein Systemfehler war, der diesen Exception-Zweig ausgelöst hat).

Also Exception-Handling, Finalization und andere dynamische Umgebungen beissen sich mit den Continuations. Man kann das Problem natürlich ignorieren - höchstwarscheinlich sind es doch eher obskurere Randphänomene, die in Python sich vermutlich schlicht in weiteren Exeptions äussern würden, wenn man auf Dinge zugreift, die nicht so sind wie man sie erwartet. Aber ich kann verstehen, wenn GvR dabei von Continuations Abstand nimmt.

Und klar, das Generatoren First-Class-Citizens sind, ist irgendwie überflüssig - jedenfalls wenn man andere Mechanismen für den gleichen Zweck verfügbar hätte (z.B. First-Class-Coroutines). Unter dem Aspekt, das GvR aber keine Continuations will (die man auch als High-End-Coroutines betrachten könnte), machen Generatoren und - mit 2.5 - Coroutinen durchaus Sinn.

Ich muss ehrlicherweise gestehen, das ich ausserhalb der Request-Response-Thematik noch nie wirklich Continuations gebraucht hätte. Aber da ich mich in den letzten Jahren fast nur noch mit genau dieser Request-Response-Thematik beschäftige, hätte ich schon ganz gerne effiziente Continuations, vor allem Multishot-Continuations. Das macht das Leben in der Webapp-Programmierung wirklich drastisch leichter :-)