Im ersten Teil unseres Beitrages in Linux Enterprise 9/2002 haben wir Ihnen bereits die Syntax und die funktionalen Aspekte von Dylan vorgestellt. Im folgenden setzen wir uns mit dem leistungsfähigen Objektsystem und der flexiblen Syntax auseinander, die durch Neudefinition von Makros die Sprache erweiterbar macht. Danach gehen wir auf das Modulsystem ein und zeigen, wie das Geheimnisprinzip ermöglicht werden kann und wie Bezeichnerkonflikte durch Umbenennung vermeidbar werden. Schließlich zeigen wir, wie präzise Typangaben und die Versiegelung von verschiedenen Objekten die Arbeit des Compilers vereinfachen und so ein performantes Laufzeitverhalten ermöglichen, das sich mit dem von C messen kann, ohne dafür die Vorteile einer sicheren Sprache zu opfern.
Funktionen höherer Ordnung
Wie schon erwähnt, stellt die Dylan-Bibliothek schon einen großen Vorrat an nützlichen Funktionen zur Verfügung. Doch besondere Erwähnung verdienen jene, die selbst Funktionen als Argumente/Resultate erhalten/liefern. Diese werden auch Funktionen höherer Ordnung genannt. Ein paar davon stellen wir nun vor:
- map(coll, fun) wendet fun auf alle Elemente der Kollektion coll an und gibt die Resultate in einer neuen Kollektion zurück.
- curry(fun, arg1, arg2, ...) liefert eine Funktion zurück, bei der die Argumente von fun von links her an arg1 usw. verdrahtet sind. Zum Beispiel ist curry(\+, 1) eine Funktion, die jede übergebene Zahl um Eins erhöht.
- rcurry verdrahtet die Argumente von rechts.
- compose(f, g) ist die wohlbekannte Verkettung von Funktionen: auf x angewandt ist das Resultat gleich f(g(x)).
- reduce(f, start, coll) kontrahiert eine Kollektion coll zu einem Wert, indem fortgesetzt f auf start und das nächste Element angewendet wird. Das Resultat wird als neuer start-Wert genommen, bis alle Elemente erfasst sind. reduce(\*, 1, #[1, 2, 3, 4]) bildet das Produkt von eins bis vier.
- always(a) liefert eine Funktion, die stets a zurückgibt, unabhängig davon, mit welchen Argumenten sie aufgerufen wurde.
- apply(fun, arg1, arg2, more-args) wendet fun auf die danach stehenden Argumente an. more-args muss dabei eine Sequenz (auch leer) sein. Diese Form des Funktionsaufrufs ist allgemeiner als fun(arg1, arg2), da beliebig viele Argumente an fun übergeben werden können.
Warum funktional programmieren?
Welche Vorteile erwachen aus der Anwendung funktionaler Prinzipien? Die Antwort darauf steckt zwischen den Zeilen der obigen Abschnitte: Die Abwesenheit von Zuweisungen garantiert, dass sich Teilsysteme eines größeren Programms nicht gegenseitig beeinflussen können.
Dies ist insbesondere dann vorteilhaft, wenn die Spezifikation von Teilsystemen in sich geschlossen und diese nur über externe Interfaces ansprechbar sind. Statt Datenstukturen zu verändern, werden neue Datenstrukturen aufgebaut, und mehrfache Aufrufe der gleichen Funktion mit gleichen Argumenten liefern auch immer gleiche Resultate.
Das rekursive Durchlaufen von Datenstrukturen durch Aufspalten, Lösung der Teilprobleme und Verknüpfung der Teilergebnisse zu einem Resultat führt fast zwangsläufig zu besser verständlichen Algorithmen und oft auch auf Anhieb korrekten Programmen.
Unter dem Strich freut sich der Entwickler über schnelle und anhaltende Erfolgserlebnisse, Spaß beim Programmieren und extrem kurze Verweildauer im Debugmodus. Das Budget freut sich über erhöhte Produktivität, kurze Entwicklungszeiten, gute Portabilität und geringen Wartungsaufwand.
Der Preis, den ein Compiler für diese Features zahlen muss, ist entsprechend hoch. Neben den schon erwähnten sind noch viele andere Optimierungen nötig, um das hohe Abstraktionsniveau des Dylan-Quellcodes in ein effizient laufendes Binärprogramm zu übersetzen. Dynamische Speicherverwaltung (auch Garbage-Collection genannt) muss außerdem die hohe Anzahl von kurzlebigen Objekten effizient entsorgen, sobald sie nicht mehr erreichbar sind, denn das funktionale Prinzip verbietet die explizite Deallozierung von Datenstrukturen.
Objektorientierung funktional
Wesentlich auffälliger ist die Karriere eines anderen modernen Paradigmas verlaufen: objektorientierte Programmierung ist in aller Munde. Dylan integriert auch dieses Prinzip, und unterstützt mit Objekten das gesamte Spektrum vom rein funktionalen Stil bis zum voll imperativen.
Alle Laufzeitobjekte sind Instanzen der Klasse
<object> und deren Unterklassen. Dieser Abschnitt führt in die etwas andere Art ein, wie Dylan das objektorientierte Paradigma auf seine Art umsetzt und dabei andere Sprachen beengend und willkürlich erscheinen lässt.
Als Einstimmung beschäftigen wir uns mit der Frage, ob man mit dem bisher Erarbeiteten nicht schon ein Objektsystem aufbauen könnte. Überraschenderweise ist das wirklich der Fall, wie Listing 3 zeigt. Wir implementieren dort eine einfache Erzeugerfunktion (das moralische Äquivalent einer Klasse), deren Resultate Punkte der Ebene beschreiben, welche um einen Vektor verschiebbar sind.
Listing 3 define function x(point)
point(x)
end;
define function x-setter(new-x, point)
point(x-setter, set: new-x)
end;
define function y(point)
point(y)
end;
define function y-setter(new-y, point)
point(y-setter, set: new-y)
end;
define function move(point)
method(delta-x, delta-y)
point(move, set: curry(values, delta-x, delta-y, point));
end;
//// dasselbe für Rätselfans:
// compose(curry(point, move, set:), rcurry(values, point))
end;
define function stretch(point, by)
point(stretch, set: curry(values, by, point));
end;
define function create-point(current-x, current-y)
method(selector, #key set)
select (selector)
x => current-x;
x-setter => current-x := set;
y => current-y;
y-setter => current-y := set;
move => let (delta-x, delta-y, point) = set();
point.x := point.x + delta-x;
// identisch zu: x-setter(x(point) + delta-x, point);
point.y := point.y + delta-y;
stretch => let (factor, point) = set();
point.x := point.x * factor;
point.y := point.y * factor;
end select;
end method;
end create-point;
begin // ausprobieren
let point = create-point(42, 25);
format-out("initially -- x: %d, y: %d\n", point.x, point.y);
point.move(-17, 5);
format-out("moved -- x: %d, y: %d\n", point.x, point.y);
stretch(point, 2);
format-out("stretched -- x: %d, y: %d\n", point.x, point.y);
end;
Das Beispiel nutzt einige Tricks, um eine gewöhnliche Syntax vorzugaukeln. Unsere Punktobjekte sind einfach nur anonyme Funktionen, die mit einem Verb (hier
selector) und einem optionalen Schlüsselwort-Argument (hier
set) aufgerufen werden.
Dieser Aufruf geschieht nicht direkt, sondern über die Verbfunktionen (
x,
x-setter,
y,
y-setter,
move und
stretch), die das Objekt und eventuelle weitere Daten durchreichen. Die anonyme Methode sorgt dann für die Decodierung der Nachricht und das Ausführen der entsprechenden Aktion.
Die Anwendungsweise der
stretch Methode ist typisch für Dylan, während
move die eher konventionelle Syntax imitiert. Entsprechend anders fällt auch die Verbfunktion aus: Wir erinnern uns, dass
point.move nur eine andere Scheibweise für
move(point) ist. Dieses resultiert in einer weiteren anonymen Methode, die die beiden Deltas entgegennimmt. Nun wird die Punktfunktion aufgerufen, und zwar mit dem Verb
move und den beiden Deltas, verpackt im
set-Argument. Dieses ist auch wieder eine Funktion, allerdings mit einer Besonderheit.
values kann beliebig viele Werte als unabhängige Resultate zurückgeben. Sofern die Rückgabewertdeklaration von Funktionen es zulässt, werden alle Resultate durchgereicht und können mit einem mehrfach-
let Konstrukt an Namen gebunden werden. Im Listing 3 geschieht das mit der Zeile
let (factor, point) = set();Nun bleibt noch zu klären, in welcher Datenstruktur die aktuellen Koordinaten gespeichert werden. Ein Blick auf
create-point klärt auch diese Frage. Die dort erzeugte anonyme Methode hat Zugriff auf die Argumente
current-x und
current-y. Diese Möglichkeit, stets auf Variablen aus umgebenden syntaktischen Strukturen zuzugreifen, nennt man lexikalischer Umfang (lexical scoping). Und nachdem die Methode diesen Zugriff auch nicht verliert, nachdem sie ihr Eigenleben als Rückgabewert begonnen hat, nimmt sie diese Variablen als Kopie mit, und kann sie fortan als unabhängige Speicherzellen verwenden. Die Auflösung offener Variablenreferenzen wird lexikalischer Abschluss (lexical closure) genannt und spielt in der funktionalen Programmierung eine immens wichtige Rolle.
Objektorientierung à la Dylan
Nach dieser eher spielerischen Einleitung in Objektsysteme beschäftigen wir uns als nächstes mit dem eher ungewöhnlichen, aber sehr konsequenten und leistungsfähigen System, das Dylan bietet. Dylan unterstützt Mehrfachvererbung, wodurch aus einer baumförmigen Klassenhierarchie ein gerichteter azyklischer Graph an Vererbungsrelationen entstehen kann.
Alle Werte in Dylan, die Laufzeitrelevanz haben, sind einer Klasse zugeordnet, die sich durch
value.object-class ermitteln lässt. An der obersten Spitze der Objektheterarchie steht die Klasse
<object>.
Alle anderen Klassen sind von
<object> abgeleitet. Die Tabelle Klassen zeigt die wichtigsten Klassen, von denen bereits einige erwähnt worden sind.
Die Dylan-Bibliothek bietet eine sehr reichhaltige Auswahl an Klassen, sodass man oft ohne weitere Modifikationen auskommt.
In den klassischen Objektsystemen sind die auszeichnenden Eigenschaften Identität, Zustand, Verhalten und Geheimnisprinzip alle in der Klassendefinition verankert. Dylan trennt diese auf und ordnet sie verschiedenen Bereichen zu.
Objekte sind Datenbehälter mit Identität, letztere lässt sich mit der Funktion
== (oder in Negation mit
~==) testen. Die Daten können mit den
getter Funktionen gelesen und mit den üblicherweise vorhandenen
setter Funktionen verändert werden. Das Verhalten von Objekten wird von generischen Funktionen festgelegt. Dies ist eines der interessantesten Features und wurde (mit Verbesserungen) vom Common Lisp Object System CLOS übernommen. Das Geheimnisprinzip (information hiding) schließlich wird vom Modulsystem überwacht und durch gezieltes (Nicht-)Exportieren von Funktionen realisiert.
Generische Funktionen
Generische Funktionen (GF) sind das Vehikel, um in Dylan polymorphes Objektverhalten zu erreichen. Konzeptionell sind GF Objekte der Klasse
, mit beitragenden Methoden der Klasse
. Beim Aufruf einer GF wird von den vorhandenen Methoden diejenige selektiert, die den Argumenttypen am besten entspricht. Die generische Funktion vermittelt also die Argumente zu der bestgeeigneten Methode weiter.
Listing 4 define class <point>(<object>)
slot x, required-init-keyword: x:;
slot y, required-init-keyword: y:;
end;
define method move(point :: <point>, delta-x, delta-y)
point.x := point.x + delta-x;
point.y := point.y + delta-y;
end;
define method stretch(point :: <point>, by)
point.x := point.x * by;
point.y := point.y * by;
end;
begin // ausprobieren
let point = make(<point>, x: 42, y: 25);
format-out("initially -- x: %d, y: %d\n", point.x, point.y);
move(point, -17, 5);
format-out("moved -- x: %d, y: %d\n", point.x, point.y);
stretch(point, 2);
format-out("stretched -- x: %d, y: %d\n", point.x, point.y);
end;
Listing 4 illustriert die Definition von Klassen und Methoden. Die Klasse
ist von
<object> abgeleitet und fügt zwei Slots (Datenspeicher) hinzu. Außerdem wird gefordert, dass diese gleich bei Objekterzeugung als Schlüsselwortargumente
x: und
y: mitgeliefert werden müssen.
Die Instanzen dieser Klasse können nun mit der GF
make erzeugt werden. Die
define method-Definitionen (nicht zu verwechseln mit
method() ... end Ausdrücken) können nur auf oberster Ebene vorkommen (wie
define class auch) und fügen den GF
move und
stretch Objekte hinzu, die auf
Instanzen spezialisiert sind. Ähnliches tut auch die Klassendefinition: die GF
x,
y,
x-setter,
y-setter und
initialize werden von ihr für Instanzen der Klasse
sowie
make für den Rückgabewert
spezialisiert.
Doch Listing 4 allein beleuchtet noch nicht hinreichend, warum Generics herkömmlichen, auf Methodentabellen basierender Methodenimplementierungen weit überlegen sind.
Erstens: Durch die Gleichbehandlung aller positionalen Argumente (also nicht der Schlüsselwort- sowie
#rest und
#next Argumente) wird das Konzept des Empfängerobjekts zur Empfängergruppe generalisiert und somit ein mehrdimensionaler Implementierungsraum zugänglich gemacht.
Zweitens: In jeder Bibliothek können neue GF eingeführt und über beliebige Klassen spezialisiert werden, ohne die ursprüngliche Klassendefinition antasten zu müssen. Dies ist insbesondere dann von Vorteil, wenn die Entwicklung in unabhängigen Teams durchgeführt wird, die nur für ihre veröffentlichten Schnittstellen zuständig sind und sich nicht um Sonderwünsche anderer Teams kümmern wollen. In solchen Fällen sind neue GF eine ideale Lösung.
Listing 5 // interface
define abstract class <shape>(<object>) end;
define abstract class <output-medium>(<object>)
slot page-dimensions;
end;
define generic draw(shape :: <shape>, medium :: <output-medium>);
define generic ink-consumed(shape :: <shape>, medium :: <output-medium>);
// implementation for media
define abstract class <printer>(<output-medium>)
slot physical-connection;
end;
define abstract class <description-stream>(<output-medium>)
slot output-stream, required-init-keyword: stream:;
end;
define concrete class <portable-document>(<description-stream>) end;
define concrete class <ps-printer>(<printer>, <description-stream>) end;
define concrete class <line-printer>(<printer>)
slot canvas-bitmap;
end;
// implementation for shapes
define concrete class <circle>(<shape>)
slot radius :: <number>, required-init-keyword: radius:;
slot centre :: <point>, required-init-keyword: at:;
end;
define concrete class <rectangle>(<shape>)
slot top;
slot left;
slot bottom;
slot right;
end;
// implementation for drawing
define generic draw-to-bitmap(shape :: <shape>, bitmap);
define method draw(shape :: <shape>, medium :: <line-printer>)
draw-to-bitmap(shape, medium.canvas-bitmap);
end;
define method draw(shape :: <circle>, medium :: <portable-document>)
format(medium.output-stream, "circle at: (%d, %d) radius: %d;\n",
shape.centre.x, shape.centre.y, shape.radius);
end;
// etc.
begin // ausprobieren
let point = make(<point>, x: 42, y: 25);
let circle = make(<circle>, at: point, radius: 10);
let document = make(<portable-document>, stream: *standard-output*);
draw(circle, document);
end;
Generische Funktionen im Einsatz
Wir zeigen in Listing 5 skizzenhaft das Grundgerüst eines Vektorgrafik-Programms. Bereits hier zeigen sich die natürlichen Trennlinien der Modularisierung.
Da die Klassendefinitionen außer dem minimalen Initialisierungsprotokoll, das eher dem Verhalten des Klassenobjekts zugeordnet werden muss, kein Objektverhalten vorwegnehmen, ist es ein leichtes, anwendungsfallspezifische GF (zum Beispiel
draw-to-bitmap) zu definieren, um die gewünschte Funktionalität nicht nur schnell mit einem Prototypen untermauern zu können, sondern auch eine tragfähige Basis für das fertige Produkt zu legen.
Wir zeigen ebenfalls die Anwendung von Adjektiven, die die beabsichtigte Verwendung dokumentieren. So unterdrückt
abstract in
define class die automatische Spezialisierung der GF
make, die nachträglich vom Programmierer selbst geschrieben werden kann. Gewöhnlich wird diese ein Exemplar einer nicht abstrakten Unterklasse erzeugen.
Bietet der Programmierer keine spezialisierte
make Methode für eine abstrakte Klasse an, so mündet der Versuch eine Instanz zu erzeugen in einen Fehler.
concrete ist das Gegenteil von
abstract und gleichzeitig der Standardwert.
In machen Fällen macht es aber durchaus Sinn, für eine abstrakte Klasse eine Spezialisierung der GF
make anzubieten: wenn zum Beispiel einer ihrer abgeleiteten Klassen eine besondere Bedeutung zukommt. So gibt es in der Dylan-Bibliothek eine auf
spezialisierte
make Methode, die einen
, (die gebräuchlichste und allgemeinste Vektorklasse), zurückliefert.
Klassen, Typen und Singletons
Typen sind etwas allgemeiner als Klassen und damit besser geeignet, um als Grundlage der Methodenspezialisierung und -selektion zu dienen. Singuläre Typen enthalten genau ein Exemplar und werden von der Funktion
singleton erzeugt.
instance?(x, singleton(y)) gilt genau dann, wenn
x == y. Somit ist
singleton die Umsetzung der Identitätseigenschaft auf Typebene.
Listing 6 define generic fak (n :: <integer>);
define method fak (n :: singleton(0))
1
end;
define method fak (n == 1)
1
end;
define method fak (n :: <integer>)
n * fak(n - 1)
end;
In Listing 6 wird unser schon bekanntes Beispiel mit singulärer Spezialisierung implementiert. Die zweite Methode ist eigentlich überflüssig, aber gut geeignet, um Dylans syntaktische Unterstützung für singuläre Spezialisierung aufzuzeigen. In einer positionalen Argumentliste sind die Argumentdeklarationen
n == 1 und
n :: singleton(1) identisch.
Sehr nützlich sind auch Vereinigungstypen, die durch
type-union erzeugt werden. Die Funktion
type-union nimmt beliebig viele Typen unter einen Hut zusammen und ermöglicht die gemeinsame Spezialisierung mit einer einzigen Methode, wo ohne Unionstypen zwei Einzelmethoden mit identischer Definition nötig gewesen wären. Beliebt ist auch das Idiom
type-union(singleton(#f), <point>)
mit dem betont wird, dass an Stelle eines Punktes als Argument auch
#f akzeptiert wird. Mit diesem Trick werden auch optionale Positionsargumente machbar. Vereinigungstypen können auch für Aufzählungstypen verwendet werden:
type-union(singleton(#"monday"), singleton(#"tuesday"), ...)
wäre eine Typdeklaration für Wochentage.
Makros
Es gibt Aspekte einer Programmiersprache, die sich durch Definition von Funktionen oder neuen Klassen alleine nicht erweitern lassen: So ein Aspekt ist beispielsweise die Syntax. Die meisten Sprachen bieten einen Grundvorrat an Konstrukten an, und überlassen es dem Programmierer, damit auszukommen.
Dylan beschreitet hier einen anderen Weg. Mit Hilfe einer Makrodefinition können anwendungsspezifische Muster (mit Varianten) zusammengefasst werden und damit die Routineaufgaben auf ihre Essenz zusammengeschrumpft werden. Dies ist sowohl für die Produktivität als auch für die Wartbarkeit ein Gewinn. Erkennt der Parser zum Beispiel eines der Konstrukte
define bezeichner ... end
bezeichner ... end
bezeichner(...)
und ist der
bezeichner über eine Makrodefinition eingeführt worden, so wird diese Form bei Bedarf makroexpandiert. Listing 7 zeigt eine einfache Makrodefinition.
Listing 7 define macro increment!
{ increment!(?:expression) }
=> { ?expression := ?expression + 1 }
{ increment!(?:expression, ?by:expression) }
=> { ?expression := ?expression + ?by }
end;
begin // ausprobieren
let n = 1;
increment!(n);
format-out("n ist %d", n);
increment!(my-window.height, delta);
// expandiert zu
// height-setter(height(my-window) + delta, my-window);
end;
Eine Makrodefinition besteht aus einer oder mehreren Transformationsregeln. Auf der linken Seite steht ein Muster, mit dem der Quelltext verglichen wird und das Makrovariablen enthalten kann, auf der rechten Seite befindet sich eine Schablone, mit der die Expansion gesteuert wird und die die Makrovariablen verwenden darf.
Die Transformationsregeln werden von oben nach unten mit dem Quellcode verglichen und bei einem Treffer wird die rechte Seite expandiert. Realistische Makros vermögen komplizierte Ausdrücke in ihre Bestandteile zu zerlegen und dann daraus andere Ausdrücke zusammenzusetzen. Alle Kontrollstrukturen (
for,
while,
until,
select,
case, usw.) der Dylan-Bibliothek sind als Makros definiert.
Die meisten Probleme, die andere Makrosysteme verursachen, sind in Dylan erfolgreich gelöst worden. So gehören die in C berüchtigten Verschmelzungen von arithmetischen Ausdrücken mit ihrer Umgebung wegen unterschiedlicher Operatorenprioritäten der Vergangenheit an, weil Ausdrücke bereits geparst in die Makroexpansion eingehen, wodurch sie eine implizite Klammerung erhalten. Außerdem ist das Makrosystem hygienisch (d.h. Bezeichner behalten ihre ursprüngliche Bedeutung bei), wodurch alle Arten von Namensverwechslungen ausgeschlossen sind, selbst wenn Makrodefinition und -gebrauch in verschiedenen Modulen erfolgen.
Bibliotheken und Module
Die meisten Entwicklungsprojekte können in kleine Abschnitte unterteilt werden, die parallel entwickelt werden können. Als allgemeines Prinzip einer solchen Gliederung hat sich die Aufteilung großer Programme in Bibliotheken durchgesetzt.
Diesen Ansatz verfolgt auch Dylan, mit einer Abweichung: In den Bibliotheken selbst befindet sich kein Objektcode, sondern Module. Diese beinhalten dann die verschiedenen (Laufzeit-)Objekte (dazu gehören Funktionen, Konstanten, etc.), aber auch Makrodefinitionen.
Der Compiler übersetzt stets eine gesamte Bibliothek auf einmal, wodurch die wichtigsten Laufzeit-Optimierungen überhaupt erst möglich werden. Dazu werden alle Elemente der Bibliothek, die Quellcode enthalten, eingelesen und in den Namensraum eines der vorhandenen Module abgelegt. Jedes gelesene Element muss daher angeben, in welches Modul es kompiliert werden soll, wobei es keine Rolle spielt, ob es sich um eine Textdatei oder einen Datensatz aus einer Datenbank handelt.
Im Falle von Textdateien geschieht dies durch einen (eMail-ähnlichen) Header. Welche Module in einer Bibliothek vorkommen und von welchen anderen Bibliotheken diese abhängig ist, bestimmt eine
define library Definition. Listing 8 könnte eine Textdatei sein, die eine Bibliothek
test-lib mit einem Modul
test-mod darin definiert.
Listing 8 module: dylan-user
define library test-lib
use dylan;
use format-out;
use other-lib, rename: {the-module => other-module}, exclude: {test};
export test-mod;
end library;
define module test-mod
use dylan;
use format-out;
use other-module;
export hello;
end module;
Gesteuerter Export für Zugriffskontrollen
Die Bibliothek
test-lib importiert alle Module aus der Bibliothek
dylan und alle Module aus den
other-lib und
format-out Bibliotheken bis auf das Modul
test. Zusätzlich wird
the-module in
other-module umbenannt. Die Bibliothek
dylan ist die Standardbibliothek und besitzt genau ein exportiertes Modul mit dem gleichen Namen. Es ist nicht ungewöhnlich, dass die Namen von Bibliothek, Modul und Funktionsobjekt übereinstimmen, wie im Falle von
format-out. Schließlich exportiert
test-lib das Modul
test-mod, das von anderen Bibliotheken importiert werden kann. Alle nichtexportierten Module sind nur innerhalb der
test-lib sichtbar.
Eine Besonderheit stellt das Modul
dylan-user dar: jede Bibliothek hat automatisch ein Modul mit diesem Namen, damit die
define library Definition überhaupt erst kompiliert werden kann. Dabei ist
dylan-user so aufgesetzt, dass alle Objekte aus dem
dylan Modul der Standardbibliothek immer verfügbar sind.
Moduldefinitionen sind ganz ähnlich zu Bibliotheksdefinitionen strukturiert. Sie importieren andere Module, wodurch sie Zugriff auf alle dort exportierten Objekte erhalten. Der folgende Code zeigt die Definition der Funktion
hello, die von dem Modul
test-mod als einzige exportiert wird:
module: test-mod
define function hello()
format-out("Hello World!");
end
Mit Hilfe der
rename Klausel in der
use Deklaration können Entitäten aus verschiedenen Bibliotheken oder Modulen umbenannt werden, um Namenskonflikten clientseitig zu begegnen.
Wenn man die
setter Funktionen nicht exportiert, kann man sicher sein, dass außer dem definierenden kein anderes Modul die Slots von Objekten ändern kann. Wird zusätzlich auf den Export der
getter Funktion verzichtet, so kann auf den entsprechenden Slot von der Außenwelt gar nicht mehr zugegriffen werden, er ist im Endeffekt
private geworden.
Optimierungen
Die Laufzeitoptimierungen, die ein Compiler für eine Applikation ausführen kann, sind davon abhängig, welche Informationen der Compiler über das Programm hat. Je mehr Wissen über die Aufruffolge, den Aufbau der Datenobjekte und über die Klassenhierarchie bekannt ist, umso mehr Optimierungen werden möglich.
Im idealen Fall besteht das gesamte Programm dabei aus genau einer Bibliothek (
closed-world-assumption), wodurch der Compiler alles weiß. Damit würden aber viele der Vorteil zunichte gemacht, über die Dylan verfügt:
- Dynamismus: Funktionen und Klassen können zur Laufzeit erzeugt werden, generische Funktionen können zur Laufzeit erweitert werden.
- Generische Typen: Typannotierungen sind außer bei den Signaturen von Implementationsmethoden generischer Funktionen optional, was die Entwicklungsgeschwindigkeit erhöht. Allerdings hat der Compiler kaum Informationen über den Objektaufbau, weshalb alle Datenzugriffe über Generische Funktionen (statt direkten Speicherzugriffen) abgewickelt werden müssen.
Abgeleitete Klassen und Methodenspezialisierungen in anderen Bibliotheken bieten immense Flexibilität, zwingen den Compiler allerdings dazu, den langsamen Dispatch zu wählen, da nicht alle Methoden zu bekannten Klassen zugeordnet werden können.
Natürlich stellt Dylan auch eine Lösung für solche Probleme bereit: Der Programmierer kann bestimmte Klassen versiegeln, wodurch diese nicht mehr abgeleitet werden können. Dass man trotzdem nicht in Javas final-Problematik versinkt, liegt daran, dass auch versiegelte Klassen in anderen Bibliotheken wieder mit
define open class als ableitbar deklariert werden können. In der definierenden Bibliothek kann immer abgeleitet werden, auch über Modulgrenzen hinweg.
Eine weitere, sensible Optimierung stellt das Versiegeln Generischer Funktionen dar: Mit
define sealed domain \+(<complex>, <complex>)
wird zum Beispiel verhindert, dass andere Bibliotheken die für diese GF weiter spezialisierte Methoden definieren.
Kurz gesagt, liegt es in der Hand des Programmierers, die Dynamik der Sprache gegen Optimierungsbestrebungen abzuwägen. Die zur Zeit nur unter Windows verfügbare Dylan-IDE von Functional Objects bietet so genannte performance colorization an, bei der verschiedene Farben im Quelltext angeben, welche Optimierungen der Compiler vornehmen konnte. Dabei stellt man allerdings oft fest, dass der Dylan-Compiler durch Kontextanalysen auch oft ohne Typannotierungen optimieren kann.
Fehlerbehandlung
Dylan verfügt auch über ein sehr leistungsfähiges Schema zur Behandlung von Laufzeitfehlern. Alle Ausnahmen werden von der Klasse
abgeleitet und in drei Hauptkategorien gruppiert:
- : Diese Fehler erfordern einen Abbruch des laufenden Programms.
- : Hier ist kein Abbruch notwendig, das Programm kann den aktuellen Kontext auswerten und entsprechend reagieren, eventuell auch ohne den Anwender über das Auftreten einer Warnung in Kenntnis zu setzen.
- : Restarts ermöglichen es, einen behebbaren Fehler auszubessern und dann die Fehlerstelle nochmals zu durchlaufen. Eine typische Situation wäre etwa: File does not exist. Create? -- y, n
Die Implementation erfolgt solcher Fehlerbehandlungen erfolgt zum Beispiel nach folgendem Schema:
block()
... überwachter Code
exception(e::<condition-type-1>)
... Fehlerbehandlung
exception(e::<condition-type-2>)
... Fehlerbehandlung
end;
Dylan und der Rest der Welt
Dylan braucht den Vergleich mit anderen Sprachen nicht zu scheuen, auch wenn die Sprache marketingtechnisch einen schweren Stand hat. Das dürfte aber bei der Kindheit der kommerziellen Versionen auch kein Wunder sein: Anfang der 90-er Jahre von Apple entworfen, dann mit vielen anderen guten Produkten verworfen, von Harlequin unter Windows implementiert, bei deren Insolvenz in die kleine Firma Functional Objects ausgegliedert, von der es seit letztem Jahr auch eine Alpha-Version für Linux zum Download gibt.
Mit den freien Versionen sieht es etwas rosiger aus, auch wenn hier keine polierte IDE das Image aufmöbelt. Neben einem Dylan-Interpreter (Marlais) gibt es einen sehr gut verwendbaren Compiler, der aus dem Gwydion-Projekt der Carnegie Mellon University hervorgegangen ist und aktiv weiter entwickelt wird. Er steht unter
www.gwydiondylan.org/ zum Download bereit.