Objective Caml, ein Vertreter aus der ML-Sprachfamilie, scheint wider erwarten eine nutzbare Programmiersprache (und Implementierung) zu sein.
Unter Wie man eine Programmiersprache auswählt beschrieb ich einige Kriterien für die Auswahl einer Programmiersprache. Als ich diesen Text schrieb, zog ich Objective Caml nicht in die nähere Wahl. Ich begann damals erst, mich mit den ML-Sprachen auseinanderzusetzen – zu jenem Zeitpunkt kannte ich nur ein bißchen Haskell, und “The Definition of Standard ML” bestellte ich erst Mitte Juni 2005. Von Objective Caml wußte ich bislang nur, daß
mldonkey
darin geschrieben ist (weswegen es als unwartbar gilt) und daß der Compiler unter einer leidlich entschärften Fassung der “Q Public License” steht (das Laufzeitsystem unterliegt einer nochmals verharmlosten LGPL-Variante).
Nach einigen Spielereien mit Standard ML (hauptsächlich mit MLton, einer Implementierung, die Programme als ganzes optimiert) fand ich gefallen an den Konzepten: Typprüfung zur Kompilierzeit, Closures, Ausnahmen, Finalisierung und effiziente Bit-Manipulation (bei MLton zumindest). MLton bietet außerdem eine ganz passable Schnittstelle zu in C geschriebenen Bibliotheken an. Die Ressourcenverwaltung (für die man in Sprachen wie C++ zweckmäßigerweise Destruktoren einsetzt) kann man, ähnlich zu Lisp, mit kleinen Wrapper-Funktionen nachbilden, die das gewünschte Objekt erzeugen, eine vom Benutzer der Bibliothek bereitgestellte (i. d. R. namenlose) Funktion aufrufen und das Objekt wieder zerstören (egal wie die Funktion verlassen wird). Einiges von meiner ursprünglichen Liste ist also erfüllt.
Was jedoch fehlt, sind real existierende Anwendungen, die in Standard ML (SML) entwickelt wurden. Sicherlich gibt es einige automatische Beweiser und natürlich die SML-Implementierungen selbst, aber das sind alles Nischenprodukte, die wenig über die Eignung für meine Belange aussagen. Keine der genannten Anwendungen greift zum Beispiel auf Datenbanken zu oder interagiert in nennenswerter Weise mit dem Netz.
Was jedoch ärgerlicher bedeutsamer ist: Die SML-Implementierungen (Standard ML of New Jersey, besagtes MLton, Alice
) werden entweder nicht so richtig weiterentwickelt, z. B. zur direkten Erzeugung von nativen Code für IA64 (vielleicht nicht ganz so wichtig) oder AMD64 (schon eher) – oder arbeiten von Vorneherein nur mit Bytecode (wie Alice). Das liegt womöglich daran, daß es für diese Sprachen keinen Markt im herkömmlichen Sinne gibt und daß sich die Entwicklung ganz wesentlich an den eigenen Forschungsbedürfnissen orientiert. Das muß nichts schlechtes sein für die Sprache, kann aber bedeuten, daß der Fokus eher bei innovativen Typsystemen liegt als in der Bereitstellung eines Debuggers.
Damit wären wir auch schon beim nächsten Punkt: Debugging bei funktionalen Sprachen sieht grundsätzlich übel aus. Das gilt für Haskell gleichermaßen wie für Standard ML. Bei Haskell ist das wegen der „faulen“ Auswertung wirklich ein Forschungsprojekt, und nicht nur eine Frage des Programmierens. Typischerweise bieten Implementierungen zwar eine interaktive Konsole an, mit der sich einzelne Funktionen manuell zwecks Fehlersuche testen lassen. Ausgerechnet MLton tut das aber nicht. Da im SML-Lager die C-Schnittstelle nicht normiert ist, kann man sich nicht einfach mit einer anderen Implementierung auf Fehlersuche begeben.
Schweren Herzens kam ich also zu dem Schluß, daß Programmieren in SML zwar Spaß macht und auch praxistaugliche Ergebnisse liefern kann, aber daß es möglicherweise zu denselben Problemen wie bei Ada führt, die mich veranlaßten, den eingangs erwähnten Artikel zu verfassen. Aber immerhin hatte ich mich nach und nach an die Syntax von SML gewöhnt. Insofern war ich offenbar reif für Objective Caml.
Objective Caml ist eine Sprache, die sich einst an Prä-Standard-ML orientierte, heute zwar konzeptionell noch immer recht nahe an SML liegt, aber zahlreiche syntaktische Abweichungen aufweist. Die Standardbibliothek (sie heißt bei SML die „Basis“) ist natürlich auch komplett verschieden.
Wenn man die SML-Syntax gewohnt ist, fühlt sich die Objective-Caml-Syntax ein wenig wie Unterschichten-Programmieren an. Objektiv betrachtet ist das natürlich Blödsinn, und im Nachhinein ist auch nur schwer nachvollziehbar, woher das Gefühl kam. Vielleicht ist SML zwar eine Spur eleganter; und wie so üblich, nehmen derartige Gefühle zu, je kleiner die Unterschiede sind. Die Caml-Syntax ist jedenfalls typisch für die ML-Sprachen und somit außerordentlich gewöhnungsbedürftig, falls man zuvor nur C, Lisp oder Python kannte.
Ähnlich wie bei Perl oder PHP steht der Name “Objective Caml” sowohl für die Programmiersprache, als auch die Implementierung. Je nach Blickwinkel kann das ein Vor- oder Nachteil sein (aber das ist ein anderes Thema, das seinen eigenen Artikel wert ist). Anders als bei den gängigen Skriptsprachen gibt es sowohl einen Compiler für die eigene virtuelle Maschine und einen Compiler für nativen Code.
Wie schlägt sich Objective Caml nach den in Wie man eine Programmiersprache auswählt aufgestellten Kriterien? Statische Typprüfung, Closures, Ausnahmen, Byte-Arrays mit effizientem Zugriff gibt es. Die Implementierung wird gewartet (wenn auch nicht vielleicht in dem Umfang, wie z. B. GCC) und halbwegs offen entwickelt (wobei viel Kommunikation natürlich innerhalb von INRIA abläuft). Unison, MLDonkey
und Hevea
können wohl als echte, in Objective Caml geschriebene Anwendungen gelten. Die C-Schnittstelle ist umfassend und nicht unnötig kompliziert (wobei man C-Module sowohl für die Bytecode-Variante von Objective Caml verwenden kann, als auch für den Native-Code-Compiler). Die Ladezeit für kleinere Anwendungen ist sehr kurz und bei größeren auch unproblematisch. Bei den damals als Hauptkriterien festlegten Merkmalen schaut es also schon ganz gut aus.
Die bei Objective Caml mitgelieferten Bibliotheken machen den Eindruck, als ob sie zumindest im Kernbereich für die UNIX-Systemprogrammierung taugen. Es gibt Thread-Unterstützung, aber innerhalb eines Prozesses kann immer nur ein Thread Objective-Caml-Code ausführen (vergleichbar zum Interpreter-Lock bei Python). Das Linken in C-Anwendungen ist möglicherweise schwierig; ich habe es noch nicht ausprobiert. Objective Caml hat einen sicheren Kern, innerhalb dessen sich die allermeisten Anwendungen implementieren lassen (wenn man einmal die C-Schnittstelle außerachtläßt, die naturgemäß zu “trusted code” führt). Ein Debugger auf Quelltextebene ist vorhanden, sofern nach Bytecode kompiliert wird (er ist unkonventionell, was aber kein Fehler ist – dazu mehr in einem anderen Artikel). Garbage Collection ist verpflichtend, kann also nicht abgeschaltet werden. Bis auf den letzten Punkt und die etwas eingeschränkte Threading-Unterstützung sieht es also auch bei den sekundären Kriterien ganz gut aus.
Trotzdem, ein Zweifel bleibt: Kann eine funktionale Programmiersprache überhaupt geschwindigkeitsmäßig mithalten? Oder müssen die geschwindigkeitskritischen Teile doch wieder in C oder C++ implementiert werden? In diesem Fall wäre es wohl sinnvoller, die C/C++-Komponenten mit einer gängigen Skriptsprache wie Python zusammenzukleben, die mehr Entwickler beherrschen.
Um es vorwegzunehmen: Objective Caml erzeugt erstaunlich flinken Code, ohne daß große Verrenkungen seitens des Programmierers notwendig wären. Das ist umso überraschender, wenn man bedenkt, daß dem nach gängiger Meinung erhebliche Hindernisse entgegen stehen:
Die Laufzeitumgebung verwendet einen Garbage Collector, d. h. die Speicherverwaltung ist weitestgehend automatisiert.
Objekte werden zur Laufzeit mit “Tags” versehen (genauso wie das Lisp-Implementierungen machen). Die statische Typinformation, die beim Kompilieren gesammelt wurde, wird somit teilweise verworfen. So entsteht eigentlich unnötiger Aufwand für die Verwaltung der Tags. Auch muß der Garbage Collector mehr Daten untersuchen, als bei strikter Ausnutzung aller Typinformation nötig wäre.
Der Compiler insgesamt ist deutlich kleiner als die Tree-SSA-Optimierer in GCC für sich genommen (!). Viele Optimierungsschritte können eigentlich gar nicht enthalten sein. Außerdem ist der Compiler selbst sehr schnell mit seiner Arbeit fertig (gerade im Vergleich zu MLton), so daß schlicht keine Zeit für aufwendige Optimierungsalgorithmen bleibt.
Im Gegensatz zu MLton führt der Objective-Caml-Compiler keine Strukturoptimierungen durch und auch keine Monomorphisierung oder Defunktorisierung. Das Ergebnis ist ungefähr so, als ob ein C++-Compiler für ein Template erst einmal eine Klasse synthetisiert, die eine ganze Reihe virtueller Methoden enthält, und dann automatisch für jede Template-Instanz Klassen ableitet, die das unterschiedliche Verhalten mit verschiedenen Repräsentationen bereitstellen. (Java-Generics kommen diesem Verfahren auch recht nahe, wenn ich das richtig sehe.)
Objective Caml trägt seinen Namen zurecht, es handelt sich zu einem gewissen Grad um ein Mitglied der “Objective”-Sprachfamilie, die aus Objective C hervorgegangen ist. Dies führt auch dazu, daß die Methoden einer Klasse nicht einfach durchnumeriert sind (wie z. B. in C++) und auch nicht leicht über Zeigerarithmetik bestimmt werden können. Der Methodenaufruf ist demnach eine eher komplexe Angelegenheit.
Diese Hindernisse sind offenbar nicht so gravierend für tatsächlichen Objective-Caml-Code, wie man annehmen könnte. Die Tagging-Problematik ist gegenüber Sprachen wie Lisp entschärft, weil z. B. eine „+“-Operation nicht die Tag-Bits zur Laufzeit untersuchen muß, sondern die benötigte Implementierung bereits zur Kompilierzeit ermittelt und deren Adresse direkt ins Kompilat geschrieben werden kann.
Methodenaufrufe spielen eine geringere Rolle als beispielsweise bei Java, weil ja noch Closures existieren. Auch muß man wegen des reichhaltigeren Typsystems in Objective Caml nicht für jede Kleinigkeit eine eigene Klasse einführen.
Monomorphisierung und Defunktorisierung vergrößern die Codegröße teilweise drastisch; möglicherweise frißt die Zunahme an Instruction Cache Misses, die sich daraus ergibt, die Vorteile wieder auf, so daß der Ansatz von Objective Caml, möglichst viel Code über indirekte Funktionsaufrufe zu teilen, in der Praxis zu keinen großen Nachteilen führt.
Der letzte offene Punkt ist die Garbage Collection. Bei Objective Caml kommt ein recht ausgeklügelter Collector zum Einsatz, der zwei Generationen verwendet. Die erste wird mit einem Copying Collector beackert, die zweite verwendet ein inkrementelles Mark-&-Sweep-Verfahren mit gelegentlichem Kompaktifizieren. Das bedeutet, daß die Allokation von kurzlebigen, kleinen Objekten sehr billig ist (vgl. auch Brian Goetz, Urban performance legends, revisited – Allocation is faster than you think, and getting faster, September 2005 für eine ähnliche Betrachtung). Ersten Tests zufolge verbringen speicherintensive Objective-Caml-Programme auch tatsächlich erfreulich wenig Zeit im Garbage Collector.
Alles in allem macht Objective Caml einen vielversprechenden Eindruck. Spaßeshalber versuchte ich auch, die aktuelle Distribution auf einem doch etwas betagteren FreeBSD-4.9-System zu kompilieren, und es traten keine Probleme auf. Da die Liste der Plattformen, die von Objective Caml unterstützt wird, ziemlich lang ist (und ich davon ausgehe, daß sich die Entwicklungsumgebung dort ebenfalls einfach übersetzen läßt), bietet sich mir vielleicht doch noch die Möglichkeit, mich vor C++-Programmierung in größerem Umfang zu drücken.
Mag sein, daß ich das anders, wenn ich ein paar tausend Zeilen Objective-Caml-Code geschrieben habe. Wir werden sehen.
Die Verwandtschaft mit Objective C (Brad Cox, “Object-oriented Programming; An Evolutionary Approach”, Addison-Wesley) scheint, wie in der ersten Fassung dieses Artikels angedeutet, tatsächlich nur Zufall zu sein. Der maßgebliche Aufsatz zum Objektsystem von Objective Caml scheint “Objective ML: An effective object-oriented extension to ML” von Didier Rémy and Jérôme Vouillon zu sein (online erhältlich über die Bibliographie von Didier Rémy). In ihm wird Objective C und Brad Cox' Arbeit aber nicht erwähnt.
Kurios ist auch die Weise, wie ich den Aufsatz über “Objective ML” fand: nicht über eine Suchmaschine, nicht durch Nachfragen auf einer Mailingliste, sondern ganz klassisch im Quellenverzeichnis von Types and Programming Languages von Benjamin C. Pierce.
2005-11-08 19:40: veröffentlicht
2005-11-28: Nachtrag hinzugefügt.