Ideen der Aspektorientierung in Prolog

Transcription

Ideen der Aspektorientierung in Prolog
Universität Rostock,
Fakultät für Informatik
und Elektrotechnik,
Institut für Informatik
Diplomarbeit
Ideen der Aspektorientierung in Prolog
vorgelegt von
Guido Wachsmuth
Betreuer:
Prof. Dr. rer. nat. Dr.-Ing. habil. Günter Riedewald
Dipl. Inf. Wolfgang Lohmann
Gutachter:
Prof. Dr. rer. nat. Dr.-Ing. habil. Günter Riedewald
Prof. Dr.-Ing. habil. Peter Forbrig
Zusammenfassung
Unter Verwendung herkömmlicher Modularisierungskonzepte moderner Programmiersprachen lassen sich die Implementationen gewisser Eigenschaften einer Software nicht
innerhalb eines Moduls separat beschreiben. Stattdessen neigen solche Eigenschaften
dazu, modulare Implementationen anderer Eigenschaften mit ihrer Implementation zu
überschneiden. Dies erschwert Verständnis, Wartbarkeit, Erweiterbarkeit und Wiederverwendbarkeit der erstellten Software-Komponenten.
Die aspektorientierte Programmierung addressiert dieses Problem. Sie führt mit Aspekten ein Modularisierungskonzept für die Implementation solcher Crosscutting Concerns
ein. Die vorliegende Arbeit untersucht die Übertragung dieser Ideen auf Prolog. Es wird
gezeigt, dass Crosscutting Concerns auch in der Programmierung mit Prolog auftreten
und sich diese mit Hilfe von Prädikaten und Klauseln nicht modular implementieren lassen. Anschließend werden Lösungsansätze wie Port-Annotationen und die Methode des
Stepwise Enhancements, welche abseits der aspektorientierten Programmierung für dieses Problem in Prolog existieren, vorgestellt, mit Ideen der aspektorientierten Programmierung verglichen und bewertet. Darauf aufbauend wird ein Modell zur aspektorientierten Programmierung in Prolog entwickelt. Unter Verwendung dieses Modells werden
Programmiertechniken generisch beschrieben. Ferner wird gezeigt, wie die aspektorientierte Entwicklung von Sprachprozessoren generisch unterstützt werden kann.
Abstract
Some software properties can not be implemented as separated modules using common
concepts of modern programming languages. Instead implementations of those properties tend to crosscut modular implementations of other properties. This decreases
understanding, maintainibility, extensibility, and reusability of software components.
Aspect-oriented programming overcomes this problem by introducing aspects as a new
concept to implement these crosscutting concerns in isolation. This work investigates the
adaption of those ideas to Prolog. It is shown that crosscutting concerns occur in Prolog
programs and can not be modularised by the use of predicates and clauses. Existing
solutions addressing this problem for Prolog programs like port annotations and the
method of stepwise enhancement are presented, compared to aspect-oriented concepts,
and evaluated subsequently. Based on those reflections a model for aspect-oriented
programming in Prolog is developed. The model is used to formalise programming
techniques in a generic manner. Furthermore a generic approach to assist aspect-oriented
developing of language processors is presented.
CR-Klassifikation: D.1.6, D.2.3, D.2.7, D.2.13, D.3.3
Schlüsselwörter: Prolog, logische Programmierung, aspektorientierte Programmierung,
Programmsynthese, Erweiterbarkeit, Wiederverwendbarkeit, Wartbarkeit, Design by Contract, Stepwise Enhancement, Programmiertechniken, Sprachprozessoren
Inhaltsverzeichnis
1 Einleitung
1.1 Motivation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.2 Zielstellung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.3 Vorgehen und Aufbau . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1
1
1
2
2 Grundlagen der Aspektorientierung
2.1 Ausgangslage . . . . . . . . . . . . .
2.1.1 Software-Entwicklungsprozess
2.1.2 Separation of Concerns . . . .
2.1.3 Hierarchische Modularisierung
2.1.4 Crosscutting Concerns . . . .
2.2 Aspektorientierte Programmierung .
2.2.1 Komponenten und Aspekte .
2.2.2 Webevorgang . . . . . . . . .
2.2.3 Joinpoint-Modelle . . . . . . .
.
.
.
.
.
.
.
.
.
4
4
4
5
5
6
7
7
8
9
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
3 Aspektorientierung aus objektorientierter Sicht
3.1 Modularisierungskonzepte . . . . . . . . . .
3.2 Typische Anwendungen . . . . . . . . . . . .
3.3 AspectJ . . . . . . . . . . . . . . . . . . . .
3.3.1 Anliegen . . . . . . . . . . . . . . . .
3.3.2 Dynamisches Joinpoint-Modell . . . .
3.3.3 Übersetzung . . . . . . . . . . . . . .
3.4 Method-Call Interception . . . . . . . . . . .
3.4.1 Anliegen . . . . . . . . . . . . . . . .
3.4.2 Dynamisches Joinpoint-Modell . . . .
3.4.3 Übersetzung . . . . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
11
11
12
13
13
14
15
16
16
16
19
4 Aspektorientierung aus Prolog-Sicht
4.1 Modularisierungskonzepte . . . . . . .
4.2 Typische Anwendungen . . . . . . . . .
4.3 Stepwise Enhancement . . . . . . . . .
4.3.1 Methodik . . . . . . . . . . . .
4.3.2 Programmkomponenten . . . .
4.3.3 Komposition von Komponenten
4.3.4 Aspektorientierung . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
20
20
20
21
22
22
23
25
4
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
Inhaltsverzeichnis
4.4
4.3.5 Verwandte Konzepte
Port-Annotationen . . . . .
4.4.1 Portmodell . . . . .
4.4.2 Annotationen . . . .
4.4.3 Umsetzung . . . . .
4.4.4 Aspektorientierung .
4.4.5 Verwandte Konzepte
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
5 Statisches Joinpoint-Modell
5.1 Joinpoints . . . . . . . . . . . . . . . . . .
5.1.1 Erweitertes Portmodell . . . . . . .
5.1.2 Boxen und Joinpoints . . . . . . .
5.2 Pointcut-Beschreibungen . . . . . . . . . .
5.2.1 Erfassung von Joinpoints . . . . . .
5.2.2 Kontext . . . . . . . . . . . . . . .
5.2.3 Fixpunkt-Semantik . . . . . . . . .
5.3 Advices . . . . . . . . . . . . . . . . . . .
5.3.1 Port-Advices . . . . . . . . . . . .
5.3.2 Around-Advices . . . . . . . . . . .
5.3.3 Term-Advices . . . . . . . . . . . .
5.3.4 Reihenfolge von Advices . . . . . .
5.4 Aspekte . . . . . . . . . . . . . . . . . . .
5.4.1 Einfache Aspekte . . . . . . . . . .
5.4.2 Abhängigkeiten zwischen Aspekten
5.4.3 Generische Aspekte . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
6 Anwendungen
6.1 Design by Contract . . . . . . . . . . . . . . . . . . . . .
6.1.1 Spezifikation durch dynamische Tests . . . . . . .
6.1.2 Spezifikation von Sortierprädikaten . . . . . . . .
6.2 Techniken . . . . . . . . . . . . . . . . . . . . . . . . . .
6.2.1 Techniken und Aspekte . . . . . . . . . . . . . . .
6.2.2 Build- und Calculate-Technik . . . . . . . . . . .
6.2.3 Akkumulator-Techniken . . . . . . . . . . . . . .
6.2.4 Differenzstrukturen . . . . . . . . . . . . . . . . .
6.2.5 Kontextpropagierung . . . . . . . . . . . . . . . .
6.3 Sprachprozessoren . . . . . . . . . . . . . . . . . . . . . .
6.3.1 Implementation von Sprachprozessoren in Prolog
6.3.2 Parser . . . . . . . . . . . . . . . . . . . . . . . .
6.3.3 Unparser . . . . . . . . . . . . . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
28
30
31
32
34
37
38
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
40
40
40
42
43
43
45
48
49
49
53
54
56
58
58
59
59
.
.
.
.
.
.
.
.
.
.
.
.
.
61
61
61
62
63
64
65
71
76
80
84
84
84
89
5
Inhaltsverzeichnis
7 Abschließende Betrachtungen
7.1 Ergebnisse der Arbeit . . . . . . . . . .
7.2 Weiterführende Arbeiten . . . . . . . .
7.2.1 Konzeptuelle Weiterentwicklung
7.2.2 Tool-Support . . . . . . . . . .
7.2.3 Anwendungen . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
Literaturverzeichnis
A Implementation
A.1 Pointcut-Beschreibungen . . .
A.1.1 Kontext . . . . . . . .
A.1.2 Fixpunkt-Algorithmus
A.2 Advices . . . . . . . . . . . .
A.2.1 Port-Advices . . . . .
A.2.2 Term-Advices . . . . .
A.3 Aspekte . . . . . . . . . . . .
A.4 Weber . . . . . . . . . . . . .
A.4.1 Dateien . . . . . . . .
A.4.2 Programme . . . . . .
A.4.3 Klauseln . . . . . . . .
A.4.4 Ziele . . . . . . . . . .
A.4.5 Port-Advices . . . . .
A.4.6 Term-Advices . . . . .
A.4.7 Vereinfachungen . . . .
A.5 Hilfsprädikate . . . . . . . . .
A.5.1 Subsumption . . . . .
A.5.2 Mehrere Lösungen . .
A.5.3 Ein- und Ausgabe . . .
A.5.4 Variablennamen . . . .
6
92
92
92
92
93
94
97
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
106
106
106
107
108
108
108
109
109
109
109
110
111
112
113
114
114
114
115
115
115
1 Einleitung
1.1 Motivation
Das Paradigma der aspektorientierten Programmierung hat seit der Präsentation der
grundsätzlichen Ideen und Ziele [45] eine weite Akzeptanz gefunden und ist heute Gegenstand weitgehender Forschungen [5, 63, 25]. Ein Großteil dieser Forschung bezieht sich
nach wie vor auf eine Sicht der Aspektorientierung als Erweiterung für objektorientierte
Programmiersprachen. Seit ihrer Entstehung beeinflusst die Idee der Aspektorientierung
jedoch auch andere Paradigmen [17, 56], darunter auch das deklarative Paradigma [54].
Prolog stellt eine Ausprägung dieses Paradigmas da. Dabei dient die Sprache auch der
Entwicklung größerer Anwendungen wie Prototypen verschiedener Art [12, 75, 10, 57],
Expertensystemen [15, 32, 27, 80] oder wissenschaftlichen Modellierungen und Simulationen [2, 79]. Eine Strukturierung und Modularisierung solcher Anwendungen trägt
entscheidend zu deren Verständlichkeit, Wartbarkeit und Wiederverwendbarkeit bei.
Darüber hinaus stellt gerade die Entwicklung von Prototypen hohe Anforderungen an
die Erweiterbarkeit von Implementationen.
Aspektorientierte Ansätze adressieren genau diese Anforderungen. Hierbei ergeben
sich für objektorientierte Anwendungen ähnlicher Dimensionen die angestrebten Vorteile [45, 44, 31]. Konkrete Realisierungen für deklarative Beschreibungsformalismen [49, 39]
zeigen, dass diese Vorteile durch eine Übertragung aspektorientierter Ideen auch im deklarativen Paradigma zum Tragen kommen. Des Weiteren existiert mit der prozeduralen Semantik Prologs ein direkter Bezug zu prozeduralen und objektorientierten Programmiersprachen, welcher bei einer Übertragung existenter aspektorientierter Ansätze
ausgenutzt werden kann.
1.2 Zielstellung
Die vorausgehenden Überlegungen zeigen, dass eine Übertragung aspektorientierter Ideen
auf Prolog grundsätzlich möglich und mit den klassischen Vorteilen der Aspektorientierung verbunden sein könnte. Ziel dieser Arbeit ist es deshalb zunächst, die tatsächliche Relevanz aspektorientierter Ansätze für die Programmierung in Prolog aufzuzeigen.
Filman und Friedman als prominente Vertreter der AOP-Community bestreiten in [26]
eben diese Relevanz:
Rule-based systems like [...] Prolog are programming with purely dynamically quantified statements. [...] If we all programmed with rules, we wouldn’t have AOP discussions. We would just talk about how rules that expressed concerns X, Y , and Z
1
1 Einleitung
could be added to the original system, with some mention of the tricks involved in
getting those rules to run in the right order and to communicate with each other.
Vor der Diskussion zur Übertragung aspektorientierter Ideen auf Prolog oder der Entwicklung neuer Ansätze gilt es daher, diese Aussage zu widerlegen. Es wird daher gezeigt,
dass die Problematik der fehlenden Modularisierbarkeit von Crosscutting Concerns auch
in Prolog existiert. Des Weiteren wird nachgewiesen, dass konkrete Anwendungen von
dieser Problematik betroffen sind, Aspektorientierung diese auch in Prolog beheben kann
und so bei der Entwicklung konkreter Anwendungen die Vorteile der Aspektorientierung
zum Tragen kommen.
Im nächsten Schritt ist zu prüfen, inwiefern aspektorientierte Ansätze für Prolog bereits existieren. Dies beinhaltet auch die Untersuchung von Methoden zur Programmentwicklung in Prolog auf eventuell inhärente aspektorientierte Ideen. Ziel der Arbeit ist
dann die Entwicklung eines eigenen Ansatzes zur Aspektorientierung in Prolog. Anhand
von Anwendungsbeispielen wird gezeigt, dass durch diesen Ansatz konkrete Anwendungen von den Vorteilen der Aspektorientierung profitieren.
1.3 Vorgehen und Aufbau
Im Anschluss an dieses Einleitungskapitel widmet sich Kapitel 2 den Grundlagen der
Aspektorientierung. Aufbauend auf einer kurzen Darstellung verschiedener Anforderungen an den Software-Entwicklungsprozess werden die grundlegenden aspektorientierten Konzepte zur Erfüllung dieser Anforderungen erläutert. Dabei werden sowohl
Begriffe als auch die verschiedenen Bestandteile einer aspektorientierten Programmiersprache berücksichtigt.
Da das Paradigma der aspektorientierten Programmierung vor allem von objektorientierten Ansätzen geprägt ist, wird dieses in Kapitel 3 aus objektorientierter Sicht betrachtet. Den Ausgangspunkt bildet eine Darstellung objektorientierter Modularisierungskonzepte. Daraufhin werden Anwendungsfälle, welche sich für gewöhnlich nicht mit Hilfe
dieser Konzepte modularisieren lassen, erläutert und klassifiziert. Den Abschluss des Kapitels bilden die Beschreibungen von AspectJ und der Method-Call Interception. Beide
Ansätze beschreiben die aspektorientierte Erweiterung objektorientierter Programmiersprachen. AspectJ bezieht sich dabei nur auf die Programmiersprache Java und hat
die Diskussion um Aspektorientierung und dessen Begriffe entscheidend geprägt. Die
Method-Call Interception zeigt, dass sich eine beliebige objektorientierte Programmiersprache durch die Einführung eines einzigen neuen Sprachkonstrukts um ein Mittel zur
aspektorientierten Programmierung erweitern lässt.
Das anschließende Kapitel 4 enthält eine analoge Betrachtung des aspektorientierten
Paradigmas aus der Sicht von Prolog. Auch hier werden zunächst Konzepte zur Modularisierung präsentiert. Es wird untersucht, inwiefern sich gewisse Anwendungsfälle nicht
durch diese Konzepte modularisieren lassen. Dabei liefern einige Anwendungsfälle aus
der objektorientierten Betrachtung nützliche Hinweise. Im Anschluss werden mit der Methodik des Stepwise Enhancements und den Port-Annotationen zwei Konzepte logischer
Programmierung betrachtet, welche einerseits einen Bezug zu diesen Anwendungsfällen
2
1.3 Vorgehen und Aufbau
und andererseits aspektorientierte Lösungsansätze zur Modularisierung aufweisen. Dies
ist besonders interessant, da beiden Ansätzen die Idee der Aspektorientierung nicht bekannt ist.
Aufbauend auf den vorhergehenden Darstellungen wird in Kapitel 5 ein Modell zur
aspektorientierten Programmierung in Prolog schrittweise vorgestellt. Das Modell orientiert sich dabei an den Ideen AspectJs, der Port-Annotationen und des Stepwise Enhancements.
Im folgenden Kapitel 6 werden dann verschiedene Anwendungen aspektorientiert entwickelt. Zunächst werden aspektorientierte Beschreibungsmöglichkeiten von Spezifikationen logischer Prädikate beschrieben. Anschließend werden Implementationen der Techniken des Stepwise Enhancements als generische Aspekte vorgestellt sowie deren Anwendung an typischen Einführungsbeispielen aus der Literatur zum Stepwise Enhancement
erläutert. Abschließend wird gezeigt, wie sich diese Techniken zur aspektorientierten
Implementation von Sprachprozessoren verwenden lassen.
Kapitel 7 fasst die Ergebnisse der Arbeit zusammen und gibt einen Ausblick auf
weiterführende Themen.
3
2 Grundlagen der Aspektorientierung
In diesem Kapitel werden zunächst die Ursprünge der Aspektorientierung und ihre
Grundlagen vorgestellt. Dazu werden der Prozess der Software-Entwicklung und die
Rolle der Modularisierung von Software-Systemen kurz analysiert sowie das Problem
der Crosscutting Concerns identifiziert. Anschließend wird gezeigt, wie die Aspektorientierung dieses Problem zu lösen versucht. Dabei werden grundlegende Begriffe und Verfahrensweisen allgemein erläutert. Die folgenden Kapitel 3 und 4 enthalten dann spezifische Betrachtungen aus Sicht der objektorientierten Programmierung bzw. aus der
Programmierung in Prolog.
2.1 Ausgangslage
2.1.1 Software-Entwicklungsprozess
Mit der steigenden Komplexität von Software-Systemen wächst auch die Komplexität
der Entwicklung solcher Systeme. Dies führt zu einem enormen Bedarf an Ressourcen.
Wissenschaftliche Fundierungen des Software-Entwicklungsprozesses versuchen daher,
die Komplexität von Software-Systemen und deren Entwicklung beherrschbar zu halten
und somit den Bedarf an Ressourcen zu kontrollieren [7].
Einen zentralen Punkt bildet dabei die Strukturierung. Davon ist zunächst den Entwicklungsprozess selbst betroffen. Verschiedene Modelle unterscheiden hier Phasen der
Anforderungsanalyse, der Spezifikation des zu entwickelnden Systems, des Systementwurfs, der Implementation, des Tests und der Verwendung des Systems. Durch eine
Strukturierung des Systems kann dessen Komplexität übersichtlich gehalten werden.
Ein wesentliches Ziel ist es hierbei, auf allen Stufen der System-Entwicklung ein besseres Verständnis des Systems und seiner Umsetzung zu erhalten. Aus diesem besseren
Verständnis heraus ergeben sich dann weitere Vorteile. So kann es den Aufwand für die
Wartung erheblich reduzieren. Umgekehrt kann ein fehlerhaftes Verständnis die Wartung
eines Systems unmöglich machen. Ein strukturiertes und wohlverstandenes System ist
überdies leichter erweiterbar, was hilft, eine vollständige Neuentwicklung bei veränderten
Anforderungen zu vermeiden. Die Wiederverwendbarkeit von Teilen des Systems spielt
eine weitere wichtige Rolle in der Kontrolle von Ressourcen. Fehlende Strukturierung
und ein unvollständiges Verständnis des Systems behindern diese ebenfalls erheblich.
4
2.1 Ausgangslage
2.1.2 Separation of Concerns
Die Strukturierung eines Systems wird neben der Abstraktion durch eine weitere kognitive Fähigkeit des Menschen maßgeblich unterstützt. Die Fokussierung auf einen Teil
einer Gesamtheit ermöglicht es, diesen isoliert zu betrachten und Erkenntnisse über ihn
zu sammeln [23]. Dabei wird die Einbettung in die Gesamtheit zwar berücksichtigt, die
Gesamtheit selbst aber weitestgehend ausgeblendet.
Diese Fähigkeit gestattet es, komplexe Probleme und Strukturen aufzubrechen und somit zu beherrschen. Sie durchzieht sämtliche Phasen des Software-Entwicklungsprozesses.
So lassen sich bestimmte Anforderungen an ein System, Spezifikationsfragmente, Teile
eines Entwurfs oder einer Implementation oder auch gewisse Testfälle isoliert betrachten.
Ein solcher isoliert betrachteter Teil bildet ein Concern.
Definition 1 (Concern)
Ein Concern ist ein Teil einer Anforderung, einer Spezifikation, eines Entwurfs, einer
Implementation oder eines Tests, welches im Interesse oder Fokus einer Betrachtung
liegt.
Um die im vorherigen Abschnitt aufgeführten Vorteile eines strukturierten Systems zu
erreichen, muss in den einzelnen Phasen des Entwicklungsprozess eine Trennung identifizierter Concerns erfolgen (Separation of Concerns) [23]. Je besser diese Trennung
gelingt und je weniger sich verschiedene Concerns überlappen, desto eher lassen sich die
angestrebten Vorteile erzielen [72].
Verschiedene Software-Entwicklungsmethoden unterstützen daher die Trennung von
Concerns. Als besonders kritisch erweisen sich dabei die Phasen des Entwurfs und der
Implementation. Die Wahl einer Entwurfsmethode und einer Programmiersprache beeinflusst, ob Concerns, welche in den vorherigen Phasen der Anforderungsanalyse und
der Spezifikation separat betrachtet werden konnten, auch im Entwurf und in der Implementation getrennt voneinander umsetzbar sind.
2.1.3 Hierarchische Modularisierung
Für die Trennung von Concerns während des Entwurfs oder der Implementation sind
die Modularisierungskonzepte der verwendeten Entwurfsmethoden bzw. Programmiersprachen entscheidend. In Abschnitt 3.1 werden objektorientierte Programmiersprachen
auf solche Konzepte hin untersucht. Abschnitt 4.1 überträgt diese Überlegungen auf
Prolog.
An dieser Stelle sei vorweggenommen, dass sowohl objektorientierte Programmiersprachen als auch Prolog hierarchische Modularisierungskonzepte anbieten. Solche Konzepte bieten gewisse Basismodule, welche in immer größeren Moduleinheiten zusammengefasst werden können. Die dabei entstehende Hierarchie formt die Struktur des Systems.
So lassen sich beispielsweise in objektorientierten Prgrammiersprachen Daten und
auf ihnen auszuführende Operationen zu Objekten zusammenfassen. Objekte wiederum
können zu Klassen von Objekten mit ähnlichem Verhalten gruppiert werden. Durch die
5
2 Grundlagen der Aspektorientierung
1
2
3
class Account{
private int balance = 0;
private TransactionLog log = new TransactionLog();
4
public void deposit(int amount) {
if !(balance >= -amount)
throw new BalanceException(balance, amount);
balance += amount;
log.addEntry(balance);
}
...
5
6
7
8
9
10
11
12
}
Abbildung 2.1: Implementation zweier Crosscutting Concerns.
Nutzung von Vererbung entsteht eine Klassenhierarchie. Weitere übergeordnete Modularisierungskonzepte wie Pakete führen diese Hierarchisierung fort.
2.1.4 Crosscutting Concerns
Bei der Implementation eines Software-Systems kann es vorkommen, dass ein Concern
nicht modularisiert wird. Die Implementation dieses Concerns verteilt sich dann auf
die Implementation anderer Concerns. Eine Ursache hierfür kann darin liegen, dass das
Concern während der Implementation nicht als eigenständig berücksichtigt wurde. In
diesem Fall kann eine nachträgliche Modularisierung Abhilfe schaffen.
Im Gegensatz dazu gibt es Concerns, welche sich mit Hilfe hierarchischer Konzepte
nicht modularisieren lassen. Die Implementation dieser Crosscutting Concerns ist mit
den Implementationen anderer Concerns vermischt, ohne dass eine Trennung möglich
ist [43].
Definition 2 (Crosscutting Concern)
Ein Crosscutting Concern ist ein Concern, welches sich unter Verwendung hierarchischer
Modularisierungskonzepte nicht modularisieren lässt und daher die Modularisierung anderer Concerns beeinflusst.
Crosscutting Concerns führen zu einer engen Kopplung des Systems. Somit werden
Verständlichkeit, Wartbarkeit, Erweiterbarkeit und Wiederverwendbarkeit der Implementation erschwert. Oftmals ist das Crosscutting dabei im Wesen des Concerns verankert. Die Abschnitte 3.2 und 4.2 identifizieren typische Crosscutting Concerns für
objektorientierte Programmiersprachen respektive Prolog.
Abbildung 2.1 zeigt die Implementation einer Klasse Account zur Verwaltung eines
Bankkontos in der objektorientierten Programmiersprache Java. Die gezeigte Klasse modularisiert die Daten des Kontos und die auf dem Konto auszuführenden Operationen.
Neben dieser Implementation der Kernfunktionalität weist die Klasse die Implementation zweier weiterer Concerns auf, welche in unterschiedlichen Grautönen unterlegt sind.
6
2.2 Aspektorientierte Programmierung
1
2
3
4
5
6
7
class Account{
private int balance = 0;
public void deposit(int amount) {
balance += amount;
}
...
}
Abbildung 2.2: Komponente ohne Implementationen von Crosscutting Concerns.
Das erste Concern adressiert das Logging von Transaktionen. Jedes Konto erhält ein eigenes Logging-Objekt. Nach erfolgreicher Abarbeitung der Methode deposit(int) wird
der neue Kontostand aufgezeichnet. Dieses Concern ist ein typisches Crosscutting Concern. Die Implementation verteilt sich auf verschiedene Kontoklassen und -methoden.
Dies hat schwerwiegende Auswirkungen auf die Wartbarkeit und Erweiterbarkeit der
Implementation. Ändert sich beispielsweise die Signatur des Logging-Objekts, ergeben
sich Änderungen an allen Klassen, welche Methoden enthalten, die den Kontostand beeinflussen. Das zweite Concern beinhaltet die Prüfung einer Vorbedingung. Wie das
Logging bildet auch dieses ein typisches Crosscutting Concern.
2.2 Aspektorientierte Programmierung
2.2.1 Komponenten und Aspekte
Aspektorientierte Programmiersprachen bieten neben herkömmlichen hierarchischen Konzepten ein zusätzliches Modularisierungskonzept für Crosscutting Concerns. Concerns,
welche von Crosscutting Concerns beeinflusst werden, können so ohne deren Einflüsse
separat als Komponenten durch Nutzung hierarchischer Konzepte modularisiert werden.
Definition 3 (Komponente)
Eine Komponente modularisiert ein Concern mit Hilfe hierarchischer Modularisierungskonzepte.
Abbildung 2.2 zeigt die auf diese Art um die Implementation von Crosscutting Concerns bereinigte Klasse Account. Die Crosscutting Concerns können dann als Aspekte
modularisiert werden.
Definition 4 (Aspekt)
Ein Aspekt modularisiert ein Crosscutting Concern eines Programms.
Ein Aspekt beschreibt dabei den Einfluss eines Crosscutting Concerns auf die Implementation von Komponenten und anderen Aspekten. Als Modularisierungskonzept
bieten Aspekte eine zu hierarchischen Konzepten orthogonale Möglichkeit der Modularisierung [88]. Dabei weisen Aspekte neue, von hierarchischen Konzepten unbekannte
Eigenschaften auf. So überlappen Aspekte andere Module, sowohl Komponenten als auch
andere Aspekte. Dies ermöglicht erst die Implementation von Crosscutting Concerns.
7
2 Grundlagen der Aspektorientierung
aspect Logging{
1
2
private TransactionLog Account.log = new TransactionLog();
3
after(Account ac, int nb): set(int Account.balance) && target(ac) && args(nb) {
ac.log.addEntry(nb);
4
}
5
6
}
1
aspect DesignByContract{
before(Account ac, int amount): execution(Account.deposit(int)) &&
2
target(ac) && args(amount) {
3
if !(ac.balance >= -amount)
throw new BalanceException(ac.balance, amount);
4
5
}
6
}
7
Abbildung 2.3: Implementation von Crosscutting Concerns als Aspekte.
Abbildung 2.3 zeigt die Implementation der beiden Crosscutting Concerns aus dem
Kontobeispiel als Aspekte. Als Beschreibungssprache dient dabei AspectJ, eine aspektorientierte Erweiterung von Java, welche in Abschnitt 3.3 vorgestellt wird. Der LoggingAspekt erweitert die Klasse Account um eine zusätzliche Membervariable, welche das
Logging-Objekt aufnimmt. Des Weiteren spezifiziert der Aspekt, dass nach jeder Veränderung des Kontostandes der neue Wert dort vermerkt wird. Der zweite Aspekt implementiert die Prüfung einer Vorbedingung, welche sicherstellt, dass Abbuchungen den
aktuellen Kontostand nicht übersteigen.
2.2.2 Webevorgang
Eine wesentliche Fragestellung beim Entwurf einer aspektorientierten Programmiersprache ist die der Komposition von Modulen zu einem Gesamtprogramm. Diese Komposition erfolgt durch einen sogenannten Weber. Der Weber berücksichtigt dabei die
als Aspekte implementierten Crosscutting Concerns und webt diese in die Komponenten ein. Der Vorgang kann zu unterschiedlichen Zeitpunkten geschehen. Danach lassen
sich statische (Komposition zur Übersetzungszeit) und dynamische Weber (Komposition zur Laufzeit) unterscheiden. Eine weitere Möglichkeit besteht in der Komposition zur
Ladezeit eines Programms.
Eine aspektorientierte Programmiersprache kann sich aus mehreren Teilsprachen zusammensetzen. So kann es verschiedene Beschreibungssprachen für Komponenten, Aspekte und das durch den Weber erzeugte Programm geben. Vor allem die Beschreibungssprache für die Aspekte orientiert sich dabei zumeist an einer bestimmten Art von Crosscutting Concerns, welche modularisiert werden sollen. Spezielle Komponentensprachen
bieten sich an, wenn für eine Steuerung des Webevorgangs eine höhere Stufe der Abstraktion als in der Ausgabesprache von Nöten ist. Beispielsweise enthält die Kom-
8
2.2 Aspektorientierte Programmierung
ponentensprache in [45] Abstraktionen für Schleifen, welche bei der Optimierung des
Gesamtprogramms durch den Weber genutzt werden.
Oft wird jedoch ein allgemeinerer Ansatz gewählt und eine bestehende Programmiersprache um ein zusätzliches Modularisierungskonzept für Crosscutting Concerns erweitert. Komponenten können dann in der ursprünglichen Sprache implementiert werden,
während Aspekte mittels der neuen Sprachkonstrukte beschreibbar sind. Oftmals erzeugt
ein statischer Weber dann Programme in der Ausgangssprache, womit deren Übersetzer,
Laufzeitumgebungen, Bibliotheken und weitere Tools weiter nutzbar sind.
2.2.3 Joinpoint-Modelle
Für den Entwurf einer Beschreibungssprache für Aspekte ist es eine naheliegende Idee,
Beschreibungsmittel für das Crosscutting eines Aspekts zur Verfügung zu stellen und
diese Beschreibungen dann beim Webevorgang zu nutzen. Vor allem durch den Einfluss
von AspectJ [44, 6] haben sich Joinpoint-Modelle als Grundlage für dieses Vorgehen
etabliert. Ein Joinpoint-Modell legt zunächst Punkte fest, an denen ein Aspekt eine
Komponente oder einen anderen Aspekt generell beeinflussen kann. Lediglich an solchen
Joinpoints können Aspekte eingewoben werden.
Definition 5 (Joinpoint)
Ein Joinpoint ist ein wohldefinierter Punkt eines Programms, an dem ein Aspekt das
Verhalten des Programms beeinflussen kann.
Dabei kann sich ein Joinpoint-Modell auf die statische Struktur, d.h. auf Punkte
im Quelltext als Joinpoints, oder auf die dynamische Struktur, d.h. auf Punkte in der
Ausführung des Programms, beziehen. Hiernach lassen sich statische und dynamische
Joinpoint-Modelle unterscheiden. Ein statisches Joinpoint-Modell legt einen Webevorgang zur Übersetzungs- oder Ladezeit eines Programms nahe, kann unter Umständen
aber auch durch einen dynamischen Weber umgesetzt werden. Umgekehrt lässt sich ein
dynamisches Joinpoint-Modell durch das Einweben dynamischer Tests in den Quellcode
eines Programms statisch umsetzen.
Des Weiteren definiert ein Joinpoint-Modell, wie Teilmengen von Joinpoints beschrieben werden können. Die Beschreibung solcher Pointcuts spezifiziert dann Orte des Crosscuttings eines Aspekts.
Definition 6 (Pointcut)
Ein Pointcut ist eine Teilmenge aus der Menge aller Joinpoints eines Programms. Ein
Pointcut kann Werte aus dem Ausführungskontext dieser Joinpoints referenzieren.
Abschließend legt das Joinpoint-Modell fest, wie ein Aspekt das Verhalten an Joinpoints eines Pointcuts beeinflussen kann. Dieser Einfluss wird mit Hilfe von Advices
formuliert.
Definition 7 (Advice)
Ein Advice beschreibt den Einfluss eines Aspekts auf das Verhalten an allen in einem
Pointcut enthaltenen Joinpoints.
9
2 Grundlagen der Aspektorientierung
Damit ermöglicht ein Joinpoint-Modell die Beschreibung von Aspekten folgenderweise:
Zunächst werden mittels Pointcut-Beschreibungen die Stellen im Programm deklariert,
welche durch den Aspekt beeinflusst werden. Dann wird durch die Zuordnung von Advices zu Pointcuts bestimmt, wie das Verhalten an diesen Stellen verändert wird. Für
gewöhnlich enthalten Advices dabei zusätzliche oder alternative Implementationen, welche am Joinpoint zur Ausführung kommen.
10
3 Aspektorientierung aus
objektorientierter Sicht
Die Ideen der aspektorientierten Programmierung sind maßgeblich durch Erweiterungen
des objektorientierten Paradigmas geprägt. Daher werden in diesem Kapitel zunächst die
Modularisierungskonzepte objektorientierter Programmiersprachen betrachtet. Darauf
basierend werden typische Anwendungsfälle für Crosscutting Concerns identifiziert. Im
folgenden Kapitel werden diese Darstellungen aufgegriffen und zum Vergleich mit der
Situation in Prolog genutzt.
Im zweiten Teil dieses Kapitels werden mit AspectJ und der Method-Call Interception zwei Ansätze zur Integration aspektorientierter Programmierung in objektorientierte Sprachen vorgestellt. AspectJ erweitert die Programmiersprache Java und hat
das aspektorientierte Paradigma entscheidend geprägt. Die Method-Call Interception
verfolgt einen allgemeineren Ansatz und zeigt, dass sich durch ein zusätzliches Sprachkonstrukt Aspektorientierung in beliebige objektorientierte Sprachen integrieren lässt.
3.1 Modularisierungskonzepte
Objektorientierte Programmiersprachen bieten verschiedene hierarchische Konzepte zur
Modularisierung einer Implementation. Analog zum Prozedurkonzept imperativer Programmiersprachen lassen sich Algorithmen in Methoden modularisieren. Dabei hängen
Methoden von anderen Methoden, welche sie aufrufen, ab. Hieraus ergibt sich oftmals
eine hierarchische Organisation der Methoden. Objekte fassen Datenstrukturen und Methoden, welche Operationen auf diesen ausführen, zur Laufzeit zusammen. Klassen bieten ein korrespondierendes Modularisierungskonzept zur Übersetzungszeit. Durch die
Nutzung von Vererbung ergibt sich eine Klassenhierarchie. Übergeordnete Konzepte wie
Pakete fassen Klassen und andere Pakete zusammen, was erneut zu einer hierarchischen
Organisation führt.
Crosscutting Concerns können sich auf allen Modularisierungsebenen auswirken. So
ist es möglich, dass die Implementation eines Crosscutting Concerns die Modularisierung eines Algorithmus als Methode durchzieht. Unter Umständen sind davon mehrere
Methoden in unterschiedlichen Klassen betroffen. Darüber hinaus ist es möglich, dass
ein Crosscutting Concern nur ausgewählte Objekte einer Klasse, nicht aber die gesamte
Klasse, betrifft. Dies ist beispielsweise der Fall, wenn die Ausführung des Concerns vom
Zustand der betroffenen Objekte abhängt.
11
3 Aspektorientierung aus objektorientierter Sicht
3.2 Typische Anwendungen
Optimierungen Eines der ersten identifizierten Crosscutting Concerns adressiert die
Effizienz eines Systems [45]. Um ein System effizienter zu gestalten, wird die Implementation der Systemfunktionalität optimiert. Dabei kann es von Vorteil sein, getrennte
Implementationen zu koppeln, etwa bei der Optimierung von Schleifen. Diese Kopplung
führt somit zu Crosscutting, obwohl die Implementationen ohne Berücksichtigung der
Effizienz modularisierbar sind.
Tracing und Logging Die Ausgabe von Informationen an bestimmten Punkten eines
Programmes ist eines der Standardbeispiele für Crosscutting Concerns, da die Anweisungen zur Ausgabe die Implementation der eigentlichen Funktionalität des Programms
durchziehen. Solche Ausgaben können während des Debuggings nützlich sein, um die
Abarbeitungsreihenfolge des Programms nachvollziehen und eventuelle Fehler finden zu
können (Tracing). Ferner können individuelle Ausgaben über ausgeführte Systemaktionen zu Zwecken der Wartung oder zur Rückverfolgung eines Systemzustands genutzt
werden (Logging).
Fehlerbehandlung Eine einheitliche Strategie zur Behandlung von Fehlern ist ebenfalls
ein typisches Crosscutting Concern. Die entsprechende Umsetzung der Fehlerbehandlung
ist über Module, in denen die Fehler auftreten können, verteilt.
Persistenz Sollen die Daten eines Systems persistent gehalten werden, muss die Datenhaltung an zahlreichen Stellen im Programm entsprechend angepasst werden. Somit
handelt es sich auch hier um ein Crosscutting Concern.
Synchronisation Die Synchronisation von Systemoperationen stellt ein weiteres Crosscutting Concern dar, da zusätzliche Anweisungen in der Implementation dieser Operationen die Synchronität gewährleisten.
Design by Contract Die objektorientierte Programmiersprache Eiffel [62] stellt mit
dem Konzept des Design by Contract einen nahtlosen Ansatz zur Integration von Spezifikation, Entwurf und Implementation einer Software zur Verfügung. Dabei werden
Spezifikationen sowohl in den Entwurf als auch in die Implementation integriert. Dies
ist formal durch Zusicherungen in Form von Vor- und Nachbedingungen möglich. Durch
eine spezielle Compiler-Option ist es möglich, diese Bedingungen zur Laufzeit prüfen zu
lassen.
Das Konzept lässt sich auf andere objektorientierte Programmiersprachen übertragen [40]. Durch die fehlende Integration in die Syntax dieser Sprachen müssen die Prüfungen allerdings von Hand implementiert werden. Die Vor- und Nachbedingungen stellen
hierbei Crosscutting Concerns dar [82]. Ihre Implementation durchsetzt die eigentliche
Implementation des Entwurfs. Durch eine Modularisierung der Spezifikationsfragmente
12
3.3 AspectJ
als Aspekt ist es möglich, die Implementation von der Prüfung der Vor- und Nachbedingungen zu trennen. Darüber hinaus ist es so möglich, die reine Implementation als
effiziente Version ohne Prüfung der verschiedenen Bedingungen zu nutzen.
3.3 AspectJ
Eine der meistbeachteten Umsetzungen aspektorientierter Programmierung für objektorientierte Programmiersprachen ist AspectJ [44, 6]. Daher werden im folgenden Abschnitt die grundlegenden Ideen, welche sich im Umfeld von AspectJ gebildet haben,
vorgestellt. Dabei werden auch bereits eingeführte Begriffe, welche AspectJ entscheidend geprägt hat, nochmals erläutert. Im weiteren Verlauf der Arbeit werden dann
aspektorientierte Ansätze mit AspectJ in Verbindung gesetzt.
3.3.1 Anliegen
Mit AspectJ verbindet sich der Versuch, die Diskussion um aspektorientierte Programmierung durch empirische Untersuchungen voranzutreiben. Hauptziel ist es dabei, eine
Benutzergemeinschaft aufzubauen, welche aspektorientierte Programmierung betreibt.
Diese Gemeinschaft soll dann Informationen liefern, welche Eigenschaften aspektorientierte Programmierung in der reellen Verwendung aufweist. Dabei ist es auch beachtenswert, ob die erhofften Vorteile, wie sie in [45] beschrieben sind, tatsächlich zum Tragen
kommen.
AspectJ bildet in diesen Bemühungen lediglich die Grundlage, indem es eine aspektorientierte Programmiersprache bereitstellt. Hierbei wird der Ansatz verfolgt, keine eigene Sprache zur Beschreibung der Aspekte zu verwenden, sondern stattdessen eine existierende Sprache um Beschreibungsmöglichkeiten für Aspekte zu erweitern. Dadurch
werden sowohl Komponenten als auch Aspekte in einer Sprache beschrieben. Da die
Komponenten mit Sprachanteilen einer bereits existenten Programmiersprache beschrieben werden, ist der Einstieg in die neue Programmiersprache dementsprechend leicht.
Im Falle von AspectJ bildet Java die Grundlage der Erweiterung. Alle Java-Programme
formen so auch AspectJ-Programme, welche lediglich aus Komponenten bestehen. Neben dieser Aufwärtskompatibilität wurde bei der Erweiterung auf weitere Kompatibilität
geachtet, so dass der Umstieg von Java auf AspectJ möglichst einfach zu vollziehen ist.
Dies betrifft einerseits die Plattformkompatibilität, welche es ermöglicht, alle AspectJProgramme auf Java Virtual Machines auszuführen. Weitere wichtige Punkte im Aufbau
einer Benutzergemeinschaft bilden die Toolkompatibilität und die Benutzerkompatibilität.
Erstere ermöglicht die sukzessive Erweiterung von Java-Entwicklungstools zur Verwendung mit AspectJ. Letztere versucht dem AspectJ-Programmierer das Gefühl zu geben,
wie in Java zu programmieren.
13
3 Aspektorientierung aus objektorientierter Sicht
3.3.2 Dynamisches Joinpoint-Modell
Die Idee, zur Beschreibung von Aspekten Joinpoint-Modelle zu verwenden, wurde durch
AspectJ entscheidend geprägt. Dabei wurden zunächst verschiedene Modelle untersucht.
Letztlich ergab sich ein dynamisches Joinpoint-Modell, welches im Folgenden erläutert
wird.
Joinpoints
Das Joinpoint-Modell von AspectJ orientiert sich an der Ausführung des Programms.
Es beschreibt verschiedene Arten dynamischer Joinpoints als Punkte in der Programmausführung. Diese sind der Aufruf einer Methode oder eines Konstruktors, der Empfang
eines Methoden- oder Konstruktoraufrufs, die Ausführung einer Methode oder eines
Konstruktors, das Lesen oder Schreiben eines Feldes, die Ausführung einer Ausnahmebehandlung und die Initialisierung von Klassen oder Objekten. Jeder Joinpoint wird
dabei zweimal passiert. Zunächst geschieht dies vor der Ausführung der mit dem Joinpoint verknüpften Aktion. Anschließend wird der Joinpoint nach Ausführung der Aktion
erneut passiert.
Pointcuts
Pointcuts umfassen Joinpoints, an denen ein Aspekt durch Advices das Verhalten des
Programms beeinflusst. Sie lassen sich in AspectJ auf verschiedene Art und Weise beschreiben. Den Ausgangspunkt bilden dabei primitive Pointcut-Beschreibungen. Für jede
Art von Joinpoints ist eine solche Beschreibung vorgesehen. So beschreibt zum Beispiel
calls(Signature ) die Menge aller Joinpoints, an denen eine Methode der Signatur
Signature aufgerufen wird. In der Signatur werden dabei Ergebnistyp und Parametertypen berücksichtigt, wobei die üblichen Untertyp-Relationen zur Anwendung kommen.
Zusätzlich können sowohl bei den Typen als auch beim Methodennamen Wildcards benutzt werden.
Des Weiteren bietet AspectJ auch primitive Pointcut-Beschreibungen, welche Joinpoints unterschiedlicher Arten adressieren. So ist es möglich, mittels within(ClassName )
die Menge aller Joinpoints in Objekten der Klasse ClassName zu beschreiben.
Der Programmierer kann unter Verwendung primitiver Beschreibungen und logischer
Kombinatoren benutzerdefinierte Pointcut-Beschreibungen erstellen. Diese können entweder unbenannt sein oder zur weiteren Verwendung mit einem Namen versehen werden.
Darüber hinaus ist es möglich, Werte aus dem Ausführungskontext durch den Pointcut
zu referenzieren. Typische solcher Werte sind beispielsweise aktuelle Parameter eines
Methodenaufrufs oder das Empfängerobjekt eines solchen Aufrufs.
Advice
Ein Aspekt kann das Verhalten an den Joinpoints eines Pointcuts beeinflussen. Dieser
Einfluss wird durch die Zuordnung von Advices zu Pointcuts beschrieben. Das JoinpointModell von AspectJ ermöglicht als Advices zusätzlichen oder alternativen Programm-
14
3.3 AspectJ
code. Dabei sind drei Arten von Advices vorgesehen. Before-Advices beschreiben Programmcode, der beim Eintritt in einen durch den Pointcut adressierten Joinpoint, also
vor der Ausführung der mit dem Joinpoint verknüpften Aktion, ausgeführt werden soll.
After -Advices ermöglichen hingegen die Beschreibung von Programmcode, der bei Verlassen des Joinpoints, also nach der Ausführung der verknüpften Aktion, auszuführen ist.
Around -Advices bieten die Möglichkeit, Programmcode an Stelle der mit dem Joinpoint
verknüpften Aktion zur Ausführung kommen zu lassen. Dabei kann in einem solchen
Advice mittels einer Proceed -Anweisung die ursprüngliche Aktion des Joinpoints mit
eventuell veränderten Parametern zur Ausführung gebracht werden.
Aspekte
Mit Hilfe von Pointcut- und Advice-Deklarationen lassen sich in AspectJ Aspekte separat
beschreiben. Aspekte bilden damit ein neues Modularisierungskonzept zur Implementation von Crosscutting Concerns. Die Modularisierung der Crosscutting Concerns durch
Aspekte befindet sich dabei auf einer Ebene mit der Modularisierung durch Klassen.
Daher werden in AspectJ Aspekte auch ähnlich wie Klassen deklariert. Neben Pointcutund Advice-Deklarationen können diese weitere Deklarationen enthalten, wie sie auch
in Klassendeklarationen vorkommen. Zur Laufzeit gibt es gewöhnlich zu jedem Aspekt
genau eine Instanz, in deren Kontext die verschiedenen Advices ausgeführt werden.
3.3.3 Übersetzung
Damit AspectJ-Programme gemäß der geforderten Plattform-Kompatibilität auf Java
Virtual Machines ausgeführt werden können, müssen diese in Java Bytecode übersetzt
werden. AspectJ selbst schreibt dabei nicht vor, wann Aspekte und Komponenten, d.h.
Klassen, miteinander verwoben werden. Die Entwickler der Sprache bieten jedoch eine
Implementation an, welche den Webevorgang soweit wie möglich zur Übersetzungszeit
vollzieht.
Zur Umsetzung wird der Quellcode des Programms schrittweise transformiert. Programmteile, in denen keine Advices zur Anwendung kommen, bleiben dabei unverändert.
In allen anderen Programmteilen werden Stellen im Programmcode untersucht, welche mit dynamischen Joinpoints korrespondieren. Besteht die Möglichkeit, dass eine
Pointcut-Beschreibung einen Joinpoint adressiert, werden statische Veränderungen an
der entsprechenden Programmstelle vorgenommen. Hierzu wird der zu einem dynamischen Joinpoint gehörige Code durch eine korrespondierende Methode umhüllt. Eventuell kann für einen Joinpoint erst zur Laufzeit entschieden werden, ob dieser durch einen
Pointcut adressiert wird, und somit bestimmte Advices zur Ausführung gelangen. In
diesem Fall werden innerhalb der Methode entsprechende dynamische Tests generiert.
Ein Advice, das an einem durch einen Pointcut adressierten Joinpoint zur Ausführung
kommen soll, wird dann aus der korrespondierenden Methode heraus zur Ausführung
gebracht. Um dies zu ermöglichen, werden die Körper von Advice-Deklarationen in JavaMethoden transformiert.
15
3 Aspektorientierung aus objektorientierter Sicht
3.4 Method-Call Interception
Die Method-Call Interception [55] bietet eine weitere Möglichkeit, objektorientierte Programmiersprachen um ein Konzept zur Beschreibung von Crosscutting Concerns zu erweitern. Im Ansatz ähnelt die Beschreibung zwar AspectJ, jedoch erfolgt die Integration
in die existierende Sprache auf völlig anderem Wege, weshalb diese Idee hier kurz Beachtung finden soll.
3.4.1 Anliegen
Die Idee der Method-Call Interception ist es, an bestimmten Punkten um die Ausführung
von Methoden zusätzliche Funktionalität zur Ausführung kommen zu lassen. Dieses
Konzept kann durch ein zusätzliches Sprachkonstrukt in eine existente objektorientierte Programmiersprache eingeführt werden. Damit unterscheidet sich die Method-Call
Interception im Ansatz nicht von anderen Ansätzen, wie z.B. AspectJ, welche ähnliche
Ausdrucksmöglichkeiten bieten.
Während in AspectJ und anderen Implementationen jedoch Programmtransformationen und Reflection zur Umsetzung des Webevorgangs zur Anwendung kommen, zeigt
die Method-Call Interception, dass es auf einfache Art und Weise möglich ist, aspektorientierte Sprachkonzepte in die Semantikdefinition einer Sprache einzubinden. Damit
haben solche Konstrukte keinen Sonderstatus mehr innerhalb der Sprache.
3.4.2 Dynamisches Joinpoint-Modell
Dem Ansatz der Method-Call Interception liegt ein Modell zugrunde, welches beschreibt,
an welchen Punkten zusätzliche Funktionalität zur Ausführung kommen kann. Darüber
hinaus bildet dieses Modell die Grundlage zur Koordination von Basis- und zusätzlicher
Funktionalität. Im Folgenden wird dieses als dynamisches Joinpoint-Modell betrachtet
und unter der Verwendung der durch AspectJ geprägten Begriffe beschrieben.
Joinpoints
In AspectJ gibt es drei Arten von dynamischen Joinpoints, welche bei der Ausführung
von Methoden passiert werden. Dies sind der Aufruf einer Methode, der Empfang eines
Methodenaufrufs und die Ausführung der Methode. Jeder dieser Joinpoints wird dabei
zweimal durchschritten, jeweils vor und nach der Ausführung der mit ihm verbundenen
Aktion.
Im Joinpoint-Modell der Method-Call Interception wird jede dieser beiden Möglichkeiten separat betrachtet. Von den somit sechs Möglichkeiten werden lediglich drei als
Joinpoints aufgefasst, da sowohl auf Joinpoints vor und nach dem Empfang eines Methodenaufrufs, als auch auf Joinpoints nach dem Aufruf einer Methode verzichtet wird.
Die verbleibenden drei Arten von Joinpoints veranschaulicht Abbildung 3.1. Den Zusammenhang zu Joinpoint- und Advice-Arten in AspectJ zeigt Tabelle 3.1 auf.
16
3.4 Method-Call Interception
enter
<o, mn>
<o’, mn’>
exp.mn’(...)
dispatch
exit
Abbildung 3.1: Joinpoints bei der Abarbeitung von Methodenaufrufen in objektorientierten Programmiersprachen.
Die erste Art bilden Dispatch-Punkte. Diese werden nach der Berechnung des Empfängerobjekts für einen Methodenaufruf (Dispatch), aber vor der Berechnung der Argumente der Methode, passiert. Zusätzliche Funktionalität, welche an einem solchen
Punkt zur Ausführung gelangt, entspricht einem Before-Advice für einen Methodenaufruf in AspectJ. Nach der Berechnung der zusätzlichen Argumente wird der Methodenkörper einer aufgerufenen Methode betreten. Dabei durchschrittene Enter -Punkte
bilden die zweite Art von Joinpoints. In AspectJ kann an diesen Punkten mittels BeforeAdvice für Methodenausführungen zusätzliche Funktionalität zur Ausführung gelangen.
Die dritte Art von Joinpoints bilden die Exit-Punkte, welche beim Austritt aus einem
Methodenkörper erreicht werden. AspectJ bietet die Möglichkeit, über After -Advice für
Methodenausführungen an diesen Punkten zusätzliche Funktionalität anzubringen.
Pointcuts
Zur Platzierung von Advice wird auch bei der Method-Call Interception eine PointcutBeschreibung für Mengen von Joinpoints verwendet. Dabei werden grundsätzlich nur
Joinpoints einer Art beschrieben, d.h. entweder Dispatch- oder Enter - oder Exit-Punkte.
Diese Beschränkung ist jedoch willkürlich. Unter Verwendung einer anderen Syntax ist
eine Pointcut-Beschreibung, welche Joinpoints unterschiedlicher Art umfasst, möglich.
Neben der Art der Joinpoints qualifiziert eine Pointcut-Beschreibung über Eigenschaften von Methodenaufrufen. Hierzu gehören das Empfängerobjekt des MethodenaufAspectJ-Joinpoint
Methodenaufruf
Advice
before
after
Methodenempfang
before
after
Methodenausführung before
after
MCI-Joinpoint
dispatch
—
—
—
enter
exit
Tabelle 3.1: Zusammenhang zwischen den Joinpoint-Modellen von AspectJ und MethodCall Interception.
17
3 Aspektorientierung aus objektorientierter Sicht
rufs, die Klasse dieses Objektes, der Methodenname, der Ergebnistyp und die Argumente
der Methode. Für jede dieser Eigenschaften existieren primitive Pointcut-Beschreibungen,
welche wie in AspectJ mittels logischer Operatoren kombiniert werden können. Eine Parametrisierung mit Daten aus dem Kontext des Methodenaufrufs ist im Gegensatz zu
AspectJ nicht nötig, da die Interaktion mit dem Kontext auf anderem Wege realisiert
wurde.
Darüber hinaus kann eine Pointcut-Beschreibung durch eine weitere Pointcut-Beschreibung qualifiziert werden, welche angibt, innerhalb welches Kontextes eine Methode
aufgerufen wird. So umfasst der Pointcut
enter method a within method b
alle Enter -Punkte der Methode a, welche während der Abarbeitung der Methode b
durchschritten werden.
Pointcut-Beschreibungen der Method-Call Interception sind grundsätzlich anonym,
da sie nur in Kombination mit Advice, welches an den enthaltenen Joinpoints zur
Ausführung gelangen soll, verwendet werden. Auch dies ist zunächst nur der Syntax
der Beispielsprache zur Method-Call Interception geschuldet. Eine entsprechende Erweiterung der Syntax und Semantik um Pointcut-Variablen ist theoretisch möglich.
Advices
Advices folgen einer anonymen Pointcut-Beschreibung und enthalten einen Ausdruck,
welcher an Joinpoints, welche durch diese Beschreibung erfasst werden, evaluiert werden
soll. Innerhalb dieses Ausdrucks ist es dabei möglich, mit dem Kontext eines Methodenaufrufs zu interagieren. Hierzu werden in die Ausgangssprache zusätzliche Ausdrücke
zum Zugriff auf den Sender und den Empfänger eines Methodenaufrufs sowie für den
Zugriff auf die Argumente und das Ergebnis des Aufrufs eingeführt.
Aspekte
Die Method-Call Interception verzichtet auf ein gesondertes Sprachkonstrukt für Aspekte. Stattdessen wird in die Ausgangssprache ein zusätzlicher Ausdruck eingeführt, welcher die Platzierung von Advices ermöglicht. Solche Platzierungsausdrücke können also
an allen Stellen eines Programms auftreten, an denen auch andere Ausdrücke erlaubt
sind.
Um einen Aspekt zu modularisieren, kann man jedoch auf die Modularisierung durch
eine Klasse zurückgreifen. In der Initialisierungsmethode können dann sämtliche Platzierungsausdrücke des Aspektes zusammengefasst werden. Zusätzlich bietet eine solche
Modularisierung die Möglichkeit, aus den Advice heraus auf Methoden und Felder der
Aspekt-Klasse zurückzugreifen. Auf diese Art lassen sich Informationen eines Crosscutting Concerns zentral verwalten.
18
3.4 Method-Call Interception
3.4.3 Übersetzung
Die in AspectJ zusätzlich in Java eingeführten Sprachkonstrukte zur Beschreibung von
Aspekten werden bei der Übersetzung gesondert behandelt. Einfache Quellcode-Transformationen überführen AspectJ-Code in Java-Code, der dann wie gewöhnlich übersetzt
werden kann. Die Method-Call Interception zeigt, dass diese gesonderte Behandlung
nicht notwendig ist sondern Sprachkonstrukte zur Platzierung zusätzlicher Funktionalität direkt in die Semantik der Ausgangssprache integriert werden können [60].
Dazu kann auf einfache Art die statische Semantik der Ausgangssprache ergänzt werden. Hiermit wird sichergestellt, dass die Erweiterung die Typsicherheit der Ausgangssprache bewahrt. Die dynamische Semantik der Ausgangssprache muss an zwei Punkten
erweitert werden. Zunächst muss der Ausdruck zur Platzierung von Advices behandelt
werden. Dies geschieht, indem in einem erweiterten Umgebungsspeicher der entsprechende Pointcut und das zugehörige Advice registriert werden. Darüber hinaus muss bei der
Behandlung von Methodenaufrufen berücksichtigt werden, dass registrierte Advice für
Pointcuts, welche den aktuellen Joinpoint enthalten, zur Ausführung gelangen. Auf der
Basis der erweiterten Syntax- und Semantikdefinition lässt sich dann ein Übersetzer für
die erweiterte Sprache entwickeln.
Dieser völlig neue Ansatz bringt verschiedene Vor- und Nachteile. So gestattet er einerseits eine separate Übersetzung von Aspekten und die Platzierung von Advices zur
Laufzeit. Andererseits können vorhandene Übersetzer und Laufzeitumgebungen nicht
weiterverwendet werden, sondern müssen entweder ebenfalls erweitert oder neu implementiert werden. Bei AspectJ wird gerade dies vermieden, da durch die vorgeschalteten
Quellcode-Transformationen sowohl Standard-Java-Compiler als auch Java Virtual Machines wiederverwendet werden können.
19
4 Aspektorientierung aus Prolog-Sicht
Dieses Kapitel untersucht, inwiefern das Problem des Crosscuttings auch in Prolog
relevant ist. Dazu werden analog zur Vorgehensweise im vorherigen Kapitel zunächst
die Modularisierungskonzepte Prologs erläutert. Anschließend werden Anwendungsfälle,
welche in der objektorientierten Programmierung Crosscutting Concerns darstellten, aus
der Sicht von Prolog untersucht. Dabei wird gezeigt, dass einige dieser Anwendungsfälle
auch in Prolog auftreten und dort ebenfalls Crosscutting Concerns repräsentieren.
Im zweiten Teil des Kapitels werden dann zwei Konzepte vorgestellt, welche die separate Beschreibung solcher Concerns adressieren. Dabei wird herausgearbeitet, dass sowohl die Methode des Stepwise Enhancements als auch die Port-Annotationen aspektorientierte Ideen für Prolog beinhalten. Hierfür werden Verbindungen zu aspektorientierten Begriffen und Ansätzen gezogen sowie den Konzepten implizit zugrundeliegende
Joinpoint-Modelle identifiziert.
4.1 Modularisierungskonzepte
Prolog bietet einfache hierarchische Modularisierungskonzepte. Klauseln bilden hierbei
die Grundlage für die Modularisierung. Ein Prädikat wird durch eine oder mehrere Klauseln beschrieben. Prädikate sind dabei von anderen Prädikaten, welche in ihren Klauseln
auftreten, abhängig. Oftmals ergibt sich ähnlich zu Prozeduren und Methoden in imperativen respektive objektorientieren Programmiersprachen eine Hierarchie. Module fassen
mehrere Prädikate zusammen. Durch Abhängigkeiten sind sie in einer Modulhierarchie
organisiert.
Jedes dieser Modularisierungskonzepte kann durch Crosscutting Concerns betroffen
sein. Im einfachsten Fall besteht die Implementation eines Prädikats aus einer einzelnen Klausel. Bereits hier kann die Implementation eines Crosscutting Concerns die Implementation eines anderen Concerns durchdringen. Eine weitere Möglichkeit ist die
Durchdringung mehrerer Klauseln eines oder mehrerer Prädikate. Dabei können sowohl
ausgewählte als auch alle Klauseln eines Prädikats betroffen sein.
4.2 Typische Anwendungen
Datentypen Obwohl Prolog eine ungetypte Sprache ist, besteht durch die Bildung von
Termstrukturen die Möglichkeit einer impliziten Typung von Daten. In der Definition
von Prädikaten über solchen Daten wird oftmals deren Struktur aufgegriffen, womit die
Datenstruktur selbst ein Crosscutting Concern bildet.
20
4.3 Stepwise Enhancement
Optimierungen Optimierungen sind in Prolog eng mit Datenstrukturen verbunden.
Prädikate, welche eine Struktur auf gleiche Art traversieren, können dabei zusammengefasst werden. Dadurch werden mehrfache Traversierungen vermieden, was zu einer
erhöhten Effizienz führt. Wie im Falle von Schleifenoptimierungen in prozeduralen oder
objektorientierten Programmiersprachen werden hierfür jedoch die verschiedenen Implementationen vermischt.
Tracing und Logging Beim Debugging von Prolog-Programmen spielt die Ausgabe von
individuellen Informationen eine wesentliche Rolle, da sich durch diese Informationen die
Abarbeitung von Prädikaten nachvollziehen lässt. Die dazu notwendigen Ausgabeprädikate durchdringen dabei die Implementation der eigentlichen Funktionalität. Ähnlich zu
diesem individuellen Tracing bildet auch Logging in Prolog ein typisches Crosscutting
Concern.
Fehlerbehandlung Prolog bietet mit den Prädikaten throw/1 und catch/3 Möglichkeiten zur Fehlererzeugung und -behandlung. Eine Strategie zur Behandlung von Fehlern stellt dabei in Prolog ähnlich wie in objektorientierten Implementationssprachen ein
Crosscutting Concern dar. Die Implementation der Strategie verteilt sich hierbei über
die verschiedenen Prädikate, welche an Fehlersituationen beteiligt sein können.
Design by Contract Spezifikation und Implementation eines Entwurfs unterscheiden
sich auch in der Programmierung mit Prolog [64]. Trotz der Deklarativität logischer Programme lässt sich deren intendierte Bedeutung teilweise nur schwer präzise bestimmen.
Extra-logische Erweiterungen in Prolog verschärfen diese Problematik zusätzlich.
Das Konzept des Design by Contracts kann auf Prolog übertragen werden und Spezifikation und Implementation verbinden. Mittels Vor- und Nachbedingungen lassen sich
Spezifikationen in die Implementation von Prolog-Programmen einbinden. Wie bei der
Übertragung auf andere objektorientierte Sprachen als Eiffel bildet die Prüfung der
Bedingungen ein Crosscutting Concern, da sie mit der eigentlichen Implementation vermengt ist.
4.3 Stepwise Enhancement
Die Methode des Stepwise Enhancements [87, 38] beschreibt einen inkrementellen Prozess zur Konstruktion von Prolog-Programmen. Dabei wird ein Programm mittels eines
Bottom-Up-Verfahrens schrittweise erweitert. Das Verfahren zeigt in seinen Ansätzen
große Ähnlichkeit zur aspektorientierten Programmierung. Die entsprechenden Zusammenhänge werden in diesem Abschnitt erarbeitet. Hierzu werden zunächst die generelle Verfahrensweise bei der Programmentwicklung, die einzelnen Programmbestandteile
und deren Komposition erläutert. Anschließend wird die Methode aus aspektorientierter Sicht untersucht. Den Abschluss des Abschnitts bildet eine Darstellung verwandter
Konzepte, welche ebenfalls aspektorientierte Ansätze beinhalten.
21
4 Aspektorientierung aus Prolog-Sicht
1
2
list([]).
list([X|Xs]):- list(Xs).
3
4
5
search([X|Xs]).
search([X|Xs]):- search(Xs).
Abbildung 4.1: Skelette zur Traversierung von Listen.
4.3.1 Methodik
Im ersten Schritt wird ein so genanntes Skelett identifiziert, welches den Kontrollfluss
des Programms beschreibt. In einem weiteren Schritt werden dann durch die Anwendung von Standard-Programmiertechniken zusätzliche Berechnungen um den Kontrollfluss des Skeletts herum platziert. Auf diese Art werden verschiedene Erweiterungen des
Skeletts gebildet. Im dritten Schritt werden diese Erweiterungen dann zu einem Gesamtprogramm zusammengesetzt. Das so erhaltene Programm kann erneut als Skelett
betrachtet werden und in weiteren Schritten durch weitere Techniken erweitert werden.
Ziel dieses Verfahrens ist es, die Verständlichkeit von Programmen zu verbessern. Den
Ausgangspunkt bildet das Verständnis des zentralen Kontrollflusses und der einzelnen
Programmiertechniken. Die schrittweise Komposition führt dann zu einem Verständnis
des Gesamtprogramms. In der Folge dessen werden Programme leichter erklär- und analysierbar. Skelette und Techniken weisen als Programmkomponenten einen hohen Grad
der Wiederverwendbarkeit auf und erleichtern die Wartung. Die theoretische Fundierung
der Methode durch Abbildungen zwischen Programmen [47] ermöglicht darüber hinaus
Korrektheitsbeweise für entwickelte Programme [37].
4.3.2 Programmkomponenten
Skelette
Ein Skelett, welches den Kontrollfluss des zu entwickelnden Programms beschreibt, bildet
den Ausgangspunkt für die Entwicklung eines Programms mittels der Methode des Stepwise Enhancements. Der beschriebene Kontrollfluss ist die Grundlage für das Verständnis des Programms und sollte daher wohlverstanden und leicht zu erklären sein. Damit
ergibt sich folgende Definition aus [38]:
Definition 8 (Skelett)
Ein Skelett ist ein einfaches Prolog-Programm mit einem wohlverstandenen, leicht zu
erklärendem Kontrollfluss.
Oftmals ist der Kontrollfluss eines Skeletts eng mit einem rekursivem Datentyp verbunden. Für einen gegebenen rekursiven Datentyp existieren dann je nach Aufgabe des
Programms unterschiedliche Skelette zur Traversierung des Datentyps. Abbildung 4.1
zeigt zwei Skelette zur Traversierung von Listen. list/1 beschreibt die vollständige
Traversierung einer Liste. search/1 hingegen traversiert eine Liste unvollständig.
22
4.3 Stepwise Enhancement
Die Definition eines Skeletts als einfaches Prolog-Programm“ ist relativ vage. Die
”
Traversierung eines rekursiven Datentyps ist ohne weiteres als grundlegend und einfach
akzeptierbar. Parser [87] und Interpreter [86, 53] sind hingegen Beispiele für komplexere Klassen von Skeletten. Auch sie bilden einen zentralen Kontrollfluss, welcher durch
verschiedene Techniken erweitert werden kann. Darüber hinaus ist es auch möglich, dass
ein Programm, welches durch Techniken erweitert wurde, in einem größeren Kontext
wiederum als Skelett betrachtet wird.
Techniken
In der Literatur zum Stepwise Enhancement werden Techniken oft als standardisier”
te Programmierpraktiken“ beschrieben. Eine Technik ist an ein Ziel gebunden, welches
durch die Anwendung der Technik erreicht werden soll. Ein oft angeführtes Beispiel ist
das Zählen von Elementen in rekursiven Datenstrukturen. Dies lässt sich auf standardisierte Weise erreichen, indem ein zusätzliches Argument als Kontext mitgeführt und
für jedes Element inkrementiert wird. Diese Technik lässt sich generisch für verschiedene Datentypen und sogar für verschiedene Traversierstrategien desselben Datentyps
verwenden. Die folgende Definition aus [38] unterstreicht diese Generizität:
Definition 9 (Technik)
Eine Technik ist eine Abstraktion für generische, häufig ausgeführte, strukturelle Änderungen an Programmen.
Das eingangs erwähnte Beispiel zum Zählen von Datenelementen lässt sich noch weiter
verallgemeinern. Statt das zusätzliche Argument für jedes Element zu inkrementieren,
sind andere Operationen denkbar. So kann durch die Addition des aktuellen Elements
zum Argument die Summe aller Elemente einer Datenstruktur ermittelt werden. Das
Ergebnis der Verallgemeinerung ist die Calculate-Technik [85, 46]. Die Technik lässt sich
anhand der strukturellen Änderungen, welche ein Skelett durch Anwendung der Technik
erfährt, semiformal beschreiben.
Zunächst wird das definierende Prädikat des Skeletts um ein zusätzliches Argument
erweitert. In nicht-rekursiven Klauseln des Prädikats wird dieses Argument durch ein
zusätzliches Initialisierungsprädikat im Klauselkörper initialisiert. In rekursiven Klauseln stellt ein zusätzliches Prädikat im Klauselkörper den Zusammenhang zwischen den
zusätzlichen Argumenten der rekursiven Aufrufe und dem zusätzlichen Argument des
Klauselkopfes her. Die verwendeten zusätzlichen Prädikate bilden dabei generische Parameter der Technik.
4.3.3 Komposition von Komponenten
Erweiterungen
Wendet man die durch eine Technik beschriebenen strukturellen Änderungen auf ein
gegebenes Skelett an, erhält man eine Erweiterung des Skeletts:
23
4 Aspektorientierung aus Prolog-Sicht
1
2
length([], Length):- Length is 0.
length([X|Xs], Length):- length(Xs, TLength), Length is TLength + 1.
3
4
5
sumlist([], Sum):- Sum is 0.
sumlist([X|Xs], Sum):- sumlist(Xs, TSum), Sum is TSum + X.
Abbildung 4.2: Anwendung der Calculate-Technik zur Berechnung der Länge bzw. der
Summe der Elemente einer Liste.
Definition 10 (Erweiterung)
Die Anwendung einer Technik auf ein Skelett erzeugt eine Erweiterung des Skeletts.
Durch unterschiedliche Instanziierungen der generischen Parameter einer Technik lassen sich mit derselben Technik verschiedene Erweiterungen eines Skeletts erzeugen. So
entstehen die Prädikate length/2 und sumlist/2 in Abbildung 4.2 durch die Anwendung der Calculate-Technik auf das Skelett list/1 aus Abbildung 4.1.
In beiden Fällen initialisiert das Prädikat is/2 in der nichtrekursiven Klausel das
zusätzliche Argument mit 0. Die Instanziierung des generischen Parameters für das
Berechnungsprädikat für rekursive Klauseln unterscheidet hingegen die beiden Fälle.
Durch die Berechnung des Nachfolgers im Falle von length/2 ermittelt dieses Prädikat
die Länge der Liste. Im Falle von sumlist/2 wird hingegen das aktuelle Listenelement
addiert, womit dieses Prädikat die Summe der Elemente einer Liste berechnet.
Das Prädikat search count/2 aus Abbildung 4.3 entsteht durch die Anwendung der
Calculate-Technik auf das Skelett search/1. Die generischen Parameter wurden dabei
wie im Falle von length/1 initialisiert. In beiden Fällen werden Elemente gezählt. Durch
die Anwendung auf das Skelett search/1 zählt search count/2 jedoch lediglich die bei
der Suche traversierten Elemente.
Das Prädikat search count/2 kann nun selbst als Skelett dienen und durch die Anwendung zusätzlicher Techniken wiederum Erweiterungen liefern. Eine mögliche Erweiterung um ein Argument, welches das zu suchende Element enthält, liefert das Prädikat
nth0/3. Es ermittelt den Index eines Elements der Liste, beginnend bei 0. Soll die Indizierung bei 1 beginnen, muss bei der Anwendung der Calculate-Technik der generische
Parameter für das Initialisierungsprädikat entsprechend angepasst werden.
1
2
search_count(N, [X|Xs]):- N is 0.
search_count(N, [X|Xs]):- search_count(M, Xs), N is M + 1.
3
4
5
nth0(N, [X|Xs], Y):- N is 0, X = Y.
nth0(N, [X|Xs], Y):- nth0(M, Xs), N is M + 1.
Abbildung 4.3: Anwendung der Calculate-Technik zur Berechnung des Index eines gesuchten Elements.
24
4.3 Stepwise Enhancement
1
2
sum_length([], S, L):- S is 0, L is 0.
sum_length([X|Xs], S, L):- sum_length(Xs, TS, TL), S is TS + X, L is TL + 1.
Abbildung 4.4: Komposition der
sum length/3.
Erweiterungen
length/2
und
sumlist/2
zu
Komposition mehrerer Erweiterungen
Um die Ziele der Methode des Stepwise Enhancements nicht zu gefährden, darf die Anwendung einer Technik auf ein Skelett dieses nicht beliebig verändern. Vielmehr muss sie
das Berechnungsverhalten des Skeletts erhalten [47, 37]. Damit wird gewährleistet, dass
sich das Verständnis einer Erweiterung sich aus dem Verständnis des zugrundeliegenden
Skeletts ergibt.
Die Erhaltung des Berechnungsverhaltens ist theoretisch wohlfundiert. Ausgangspunkt
bilden dabei Abbildungen zwischen Prolog-Programmen [47]. Eine Erweiterung lässt sich
dabei stets auf das zugrundeliegende Skelett abbilden [38]. Darauf aufbauend lassen sich
Abbildungen zwischen Berechnungen definieren, welche für Korrektheitsbeweise verwendet werden können [37].
Des Weiteren bilden Abbildungen zwischen Erweiterungen und Skeletten die Grundlage für die Komposition mehrerer Erweiterungen desselben Skeletts. Da jede Erweiterung den Kontrollfluss des Skeletts erhält, können die einzelnen Erweiterungen zu einem
Programm zusammengefügt werden, welches wiederum eine Erweiterung des Skeletts
darstellt, d.h. dessen Berechnungsverhalten ebenso bewahrt [37]. Abbildung 4.4 zeigt
die Komposition der Erweiterungen sumlist/2 und length/2 aus Abbildung 4.2 zum
Programm sum length/3, welches ebenfalls eine Erweiterung des Skeletts list/1 aus
Abbildung 4.1 darstellt.
4.3.4 Aspektorientierung
Separation of Concerns
Die Methode des Stepwise Enhancements wird oftmals mit einer besseren Erklärbarkeit der entwickelten Programme, Möglichkeiten der Software-Analyse und der besseren
Wartbarkeit der entwickelten Software motiviert [38, 46, 85]. Dabei stellt sich die Frage,
warum diese Möglichkeiten ohne die Methodik nicht bestehen.
Die Antwort hierauf findet sich in der Betrachtung des Gesamtprogramms. In diesem
sind verschiedene Erweiterungen mit dem ursprünglich klar verständlichen Kontrollfluss
des Skeletts vermengt. Die Aufgaben der einzelnen Erweiterungen und der eigentliche
Kontrollfluss lassen sich im Gesamtprogramm nur schwer erklären, analysieren, warten
oder erweitern. Die separate Beschreibung des Skeletts und der einzelnen Erweiterungen
hingegen führt zu einer Trennung der verschiedenen Concerns.
25
4 Aspektorientierung aus Prolog-Sicht
Aspektorientierte Begriffe
Aspektorientierte Ansätze verfolgen eben diese Trennung von Crosscutting Concerns. Die
Anwendung einer Technik ist nichts anderes als ein separate Beschreibung eines solchen
Concerns, welches andere Erweiterungen crosscuttet. Somit stellt die Anwendung einer
Technik einen Aspekt des Gesamtprogramms dar.
Als problematisch stellt sich jedoch heraus, dass jede Erweiterung das ursprüngliche
Skelett implizit enthält. Aus aspektorientierter Sicht bildet dieses die Komponente, in
welche die Aspekte eingewoben werden. Eine Trennung der angewandten Technik und
des Skeletts erweist sich jedoch als schwierig, da die Beschreibung von Techniken bisher
nur informal [85] oder semiformal [38] gelang. Der Abschnitt 6.2 zeigt jedoch, dass unter
Verwendung eines statischen Joinpoint-Modells, wie es in Kapitel 5 vorgestellt wird,
Techniken als generische Aspekte formuliert werden können.
Die Anwendung einer Technik weist weitere typische Merkmale von Aspekten auf. So
sollte ein Aspekt den Kontrollfluss anderer Aspekte und der Komponente, in welche er
eingewoben wird, in gewissem Maße bewahren. Da Erweiterungen unabhängig voneinander auf ein Skelett angewendet werden und dessen Kontrollfluss erhalten, sind diese
Eigenschaften gegeben.
Statisches Weben
Techniken können als Programmtransformationen betrachtet werden [38, 46], welche sich
auf die statische Struktur eines Programms beziehen. Da auch die Komposition mehrerer
Erweiterungen zu einem Gesamtprogramm statischer Natur ist [37, 38], wird somit aus
aspektorientierter Sicht ein statischer Webevorgang beschrieben.
Im ersten Schritt dieses Vorgangs werden Aspekte, d.h. Techniken, einzeln in die Komponente, d.h. das Skelett, eingewoben. Im zweiten Schritt werden die so entstandenen
Erweiterungen zu einer Erweiterung zusammengefasst. In weiteren Schritten können weitere Aspekte in diese Erweiterung, welche dann als neues Skelett fungiert, eingewoben
werden.
Der mehrstufige Webevorgang bringt eine neue aspektorientierte Sichtweise, da es
mehrere Stufen von Komponenten gibt. Damit ist es möglich, dass sich Aspekte auf
Komponenten beziehen, die in vorherigen Schritten durch das Einweben von Aspekten
in eine Komponente niedrigerer Stufe entstanden sind. Diese Art des Vorgehens führt
in komplexen Anwendungsfällen [84, 53] zu einer getrennten Beschreibung voneinander
abhängiger Aspekte.
Anwendungen
Die im aktuellen Abschnitt gezeigten Beispiele sind typische Anwendungsfälle aus der
Literatur zum Stepwise Enhancement. Die Auswahl dieser Beispiele ist vorallem in ihrer Kompaktheit und klaren Verständlichkeit begründet. Der Hauptanwendungsfall liegt
hier in der Optimierung der Effizienz von Programmen, indem Erweiterungen desselben
Skeletts zu einer Erweiterung zusammengefügt werden. Dies entspricht der Optimie-
26
4.3 Stepwise Enhancement
rung separat beschriebener Schleifenoperationen in prozeduralen Sprachen, welche als
typischer aspektorientierter Anwendungsfall gilt [45].
Die Wiederverwendbarkeit und bessere Verständlichkeit der Erweiterungen wirkt hingegen eher konstruiert. Dieser Eindruck wird durch fehlende formale Beschreibungen für
Techniken noch verstärkt. Die Beispiele in Abschnitt 6.2 zeigen jedoch, dass eine solche
Beschreibung von Techniken tatsächlich einen hohen Grad der Wiederverwendbarkeit
von Skeletten und Techniken ermöglicht.
In komplexeren Anwendungsfällen wird dies ohnehin deutlich. So lässt sich der VanillaMeta-Interpreter [87] per Stepwise Enhancement zu verschiedenen Meta-Programmen
erweitern [86, 53, 87]. Dabei lassen sich sowohl der Vanilla-Meta-Interpreter, welcher als
Skelett fungiert, als auch verschiedene Erweiterungen zur Bestimmung der Tiefe eines
Ableitungsbaums oder zur Nummerierung von Zielen wiederverwenden und zu neuen
Erweiterungen kombinieren.
Modularisierung
Mit der Methodik des Stepwise Enhancements lässt sich auch ein Modulkonzept für
Prolog entwerfen [84]. Grundbestandteil eines Moduls ist dabei ein Skelett, was den
Kontrollfluss aller vom Modul exportierten Prädikate beschreibt. Das Modul exportiert
dann verschiedene Erweiterungen dieses Skeletts. Darüber hinaus enthält es eine Erweiterungsstruktur, welche die Abhängigkeiten zwischen verschiedenen Erweiterungen
stufenweise repräsentiert [37].
Aus aspektorientierter Sicht entspricht eine solche Modularisierung der Zusammenfassung einer Komponente und aus dieser durch Einweben von Aspekten entstandener
Programme. Zusätzlich bedarf es jedoch einer modularen Beschreibungsmöglichkeit für
die Aspekte selbst. Diese ergibt sich aus einem Formalismus zur Beschreibung von Techniken und wird in Abschnitt 6.2 detailliert besprochen.
Abhängigkeit von Aspekten
Generell stellt die erfolgreiche Verwendung des mehrstufigen Webens für komplexe Anwendungsfälle die Unabhängigkeit von Aspekten [26] in Frage. In späteren Stufen eingewobene Aspekte beziehen sich auf solche, welche in vorherigen Stufen eingewoben
wurden. Trotz dieses Bezugs führt die mehrstufige Vorgehensweise zu einer separaten
Beschreibbarkeit der Aspekte. In einer einstufigen Beschreibung lässt sich eine solche
getrennte Beschreibung ebenfalls erreichen, wenn Aspekte Informationen anderer Aspekte gezielt nutzen können. Komplexere Anwendungsfälle offenbaren also Abhängigkeiten
zwischen Aspekten, welche sowohl in deren Beschreibung als auch in der Entwicklung
von geeigneten Beschreibungsmitteln berücksichtigt werden müssen.
Weitere Abhängigkeiten ergeben sich zwischen Skeletten und anzuwendenden Techniken, also Komponenten und einzuwebenden Aspekten. Die Wahl eines Skeletts bestimmt den fundamentalen Kontrollfluss des Gesamtprogramms. Dabei kann dieser für
die Anwendung bestimmter Techniken vorbereitet werden. Ähnliche Überlegungen zur
27
4 Aspektorientierung aus Prolog-Sicht
1
2
3
calc_list(Init, Calc, [], Result):- call(Init, Result).
calc_list(Init, Calc, [X|Xs], Result):- calc_list(Init, Calc, Xs, TResult),
call(Calc, X, TResult, Result).
Abbildung 4.5: Prädikat höherer Ordnung zur Ausführung von Berechnungen während
der Traversierung einer Liste.
Abhängigkeit von Aspekten untereinander und zwischen Aspekten und Komponenten
lassen sich auch für objektorientierte Ansätze finden [41].
4.3.5 Verwandte Konzepte
Prädikate höherer Ordnung
Die Programmierung höherer Ordnung, wie sie aus funktionalen Programmiersprachen
bekannt ist, lässt sich auch auf die logische Programmierung übertragen [65]. Dazu
werden (eventuell unvollständige) Ziele als Argumente von Prädikaten höherer Ordnung
verwendet, zu neuen Zielen zusammengesetzt und ausgewertet.
Prädikate höherer Ordnung können dabei zur Trennung von Concerns genutzt werden.
Als Beispiel dient das Prädikat calc list/4 aus Abbildung 4.5, welches eine separate
Beschreibung einer Berechnung während der Traversierung einer Liste ermöglicht. Als
die ersten beiden Argumente erwartet das Prädikat zwei unvollständige Ziele, welche zur
Initialisierung bzw. zur Berechnung des Ergebnisses genutzt werden. Die letzten beiden
Argumente bilden die zu traversierende Liste und das Ergebnis der Berechnung. Durch
individuelle Berechnungsprädikate können dann verschiedene Berechnungen separat beschrieben werden. Abbildung 4.6 zeigt solche separate Beschreibungen zur Berechnung
der Länge einer Liste und der Summe der Listenelemente.
Aus aspektorientierter Sicht lässt sich das Prädikat calc list/4 als Komponente betrachten. Das Prädikat call/N kennzeichnet dabei explizit Joinpoints, an welchen Advices, welche dem Prädikat als Argumente übergeben werden, zur Ausführung gelangen
sollen. Die Advices selbst lassen sich separat beschreiben.
Die Kombination mehrerer Berechnungen kann dabei auf Ebene der Advices erfolgen.
Das Prädikat sum length/3 aus Abbildung 4.6 verwendet dazu ein Ergebnispaar und als
Advices Prädikate, welche die Advices für length/2 und sumlist/2 zusammenfassen.
Diese Komposition lässt sich mit Hilfe von weiteren Prädikaten höherer Ordnung auch
ohne zusätzliche neue Prädikate beschreiben [65].
Andererseits lässt sich das Prädikat auch als generische Anwendung der CalculateTechnik auf das Skelett list/1 aus Abbildung 4.1 interpretieren. Die generischen Parameter der Technik bleiben dabei unberührt und treten als die ersten beiden Argumente
der Erweiterung auf. Ein Aufruf des Prädikats mit konkreten Belegungen dieser Argumente korrespondiert dann mit einer Anwendung der Calculate-Technik mit entsprechender Belegung der generischen Parameter. Dieser Zusammenhang wird noch deutlicher,
wenn man das Prädikat höherer Ordnung, Aufrufe von call/N und die Prädikate, welche
die Advices beschreiben, partiell evaluiert. Für die Prädikate aus Abbildung 4.6 liefert
28
4.3 Stepwise Enhancement
1
length(List, Length):- calc_list(zero, plus1, List, Length).
2
3
4
zero(Result):- Result is 0.
plus1(X, TResult, Result):- Result is TResult + 1.
5
6
sumlist(List, Sum):- calc_list(zero, plus, List, Sum).
7
8
plus(X, TResult, Result):- Result is TResult + X.
9
10
sum_length(List, Sum, Length):- calc_list(dzero, dplus, List, (Sum, Length)).
11
12
13
dzero((R1, R2)):- zero(R1), zero(R2).
dplus(X, (TR1, TR2), (R1, R2)):- plus1(X, TR1, R1), plus(X, TR2, R2)).
Abbildung 4.6: Generische Anwendung der Calculate-Technik als Prädikat höherer Ordnung.
dieses Vorgehen die konkreten Erweiterungen aus Abbildung 4.2.
Ein Prädikat höherer Ordnung stellt somit eine formale Beschreibung einer Technik
dar, welche bereits an ein Skelett gebunden ist. Damit lassen sich wesentliche Merkmale einer Technik formal erfassen. Durch die Bindung an ein Skelett handelt es sich
allerdings um keine reine Beschreibung der Technik, wie sie aus aspektorientierter Sicht
wünschenswert wäre.
Generell stehen Prädikate höherer Ordnung und Stepwise Enhancement in engem Zusammenhang [66, 67]. Die Anwendung einer Technik mit konkreten Werten für deren generische Parameter führt beim Stepwise Enhancement zu einer Erweiterung, welche dann
mit anderen Erweiterungen zusammengefügt werden kann. Prädikate höherer Ordnung
stellen hingegen eine Verbindung zwischen einer Technik und einem Skelett her, ohne
die generischen Parameter zu belegen. Durch verschiedene Belegungen der Argumente
eines Prädikats höherer Ordnung lassen sich dann konkrete Erweiterungen beschreiben.
Durch partielle Evaluation des Prädikats höherer Ordnung, der Aufrufe von call/N und
der Prädikate, welche die Advices definieren, gelangt man auf einem anderen Weg zum
gleichen Ergebnis wie die Methode des Stepwise Enhancements.
Aspekte als Metaprogramme
Die Betrachtung von Techniken als Programmtransformationen [38, 46] legt eine Beschreibung von Aspekten als Metaprogramme nah. So repräsentierte Aspekte werden
dann durch Ausführung der Programmtransformationen in eine Komponente eingeworben. In [54] wird diese Idee in allgemeiner Form zur Beschreibung von Aspekten in deklarativen Programmen (logische Programme, Attributgrammatiken, algebraische Spezifikationen, natürliche Semantikbeschreibungen) aufgegriffen.
Den Ausgangspunkt bilden dabei Komponenten, welche in einer deklarativen Beschreibung vorliegen. Aspekte können separat als funktionale Metaprogramme beschrieben
werden. Dazu stehen verschiedene Operatoren für Programmtransformationen, sowie
für die Analyse und Komposition von Programmen zur Verfügung. Alle Operatoren
29
4 Aspektorientierung aus Prolog-Sicht
haben gemeinsam, dass ihre Semantik formal beschrieben ist. Auf Basis dieser Beschreibung lässt sich die Korrektheit des Webevorgangs nachweisen. Ähnlich zu den formalen
Betrachtungen zur Korrektheit mittels Stepwise Enhancements entwickelter Programme [47, 37] lässt sich so nachweisen, dass Aspekte den Kontrollfluss der Komponenten
erhalten.
Mit Hilfe der Prädikate term expansion/2 und goal expansion/2 lässt sich in verschiedenen Prolog-Versionen [93, 35] ein auf Metaprogrammierung basierender Webevorgang zur Ladezeit eines Programms spezifizieren. Wird eine Klausel eines Programms
eingelesen, versucht das Prädikat expand term/2 diese in eventuell mehrere neue Klauseln zu transformieren. Die Transformationen werden dabei durch Klauseln des benutzerdefinierten Prädikats term expansion/2 beschrieben. Schlägt die Transformation fehl
oder ist das Prädikat nicht definiert, versucht expand term/2 die aktuelle Klausel als Regel einer Definite Clause Grammar [73] zu interpretieren und in eine gewöhnliche Klausel
zu übersetzen. Schlägt auch dies fehl, wird die ursprüngliche Klausel in der Datenbasis
abgelegt. Ist das Prädikat hingegen erfolgreich, wird das Ergebnis der Transformation,
welches aus mehreren Klauseln bestehen kann, in der Datenbasis abgelegt.
Die Prädikate expand goal/2 und goal expansion/2 arbeiten auf vergleichbare Art.
Nach dem Aufruf von expand term/2 und vor der Ablage einer Klausel in der Datenbasis
wird expand goal/2 für jedes Teilziel im Klauselkörper aufgerufen. Davon sind auch
Argumente der Standard-Prädikate höherer Ordnung wie beispielsweise not/1, call/1
oder findall/3 betroffen. Das Prädikat expand goal/2 verwendet seinerseits dann das
benutzerdefinierte Prädikat goal expansion/2 zur Transformation von Zielen. Schlägt
dieses fehl oder ist undefiniert, lässt expand goal/2 das Ziel unverändert.
Die vorgestellten Prädikate lassen sich zur separaten Beschreibung von Aspekten nutzen. Ein Aspekt besteht dann aus Klauseln für die Prädikate term expansion/2 und
goal expansion/2. Soll sichergestellt werden, dass Aspekte Komponenten nur durch
wohldefinierte, semantikerhaltende Programmtransformationen [54] beeinflussen können,
lassen sich diese durch Prädikate beschreiben, welche zur Umsetzung der Transformationen auf term expansion/2 bzw. goal expansion/2 zurückgreifen. Der Ansatz bleibt
jedoch limitiert, da Operatoren zur Analyse oder Komposition nicht umsetzbar sind,
weil der Zugriff auf das gesamte einzulesende Programm nicht gewährleistet ist.
4.4 Port-Annotationen
Port-Annotationen [50, 51] weisen in Beschreibung, Umsetzung und Anwendung viele
Parallelen zu aspektorientierten Ansätzen der objektorientierten Welt auf. Sie stellen ein
Mittel zur separaten Beschreibung von Crosscutting Concerns in logischen Programmen
dar und zielen in ihrer Verwendung auf typische aspektorientierte Anwendungsfälle vor allem auf Debugging-Aspekte und die Spezifikation von Typen, Modi sowie Vorund Nachbedingungen von Prädikaten. Im Folgenden werden daher das Portmodell als
Grundlage der Annotationen, die Beschreibung der Annotationen und ihre Umsetzung
vorgestellt. Anschließend wird dann der Zusammenhang zur Aspektorientierung und
deren Begrifflichkeiten hergestellt.
30
4.4 Port-Annotationen
call
exit
fail
redo
Abbildung 4.7: Portmodell nach Byrd. Eine Box repräsentiert ein Ziel und kann durch
verschiedene Ports betreten (call, redo) oder verlassen (exit, fail ) werden.
4.4.1 Portmodell
Das Portmodell, wie es Abbildung 4.7 zeigt, geht auf Arbeiten von Byrd [14] zurück.
Es modelliert die Ausführung von Prolog-Programmen und bildet die Basis für verschiedene Debugging-Tools. Das am häufigsten genutzte Tool ist dabei ein Tracer. Dieses
Tool ermöglicht es, den Fortschritt der Programmausführung anhand der Ausgabe aktuell bearbeiteter Ziele nachzuvollziehen. Die dabei ausgegebene Spur (engl. trace) der
abgearbeiteten Ziele gab dem Tool seinen Namen.
Für das Verständnis dieser Spur ist es wichtig zu wissen, warum das aktuelle Ziel bearbeitet wird. Das Portmodell soll dieses Verständnis ermöglichen und wird daher auch des
öfteren als Trace-Modell bezeichnet. Dabei wird ein Ziel als Box modelliert, welche durch
zwei Ports betreten, und zwei weitere Ports verlassen werden kann. Durch den Call -Port
tritt Prolog in die Box ein, wenn es erstmalig versucht, das durch die Box repräsentierte
Ziel zu erfüllen. Gelingt dies, verlässt Prolog die Box über den Exit-Port. Aus prozeduraler Sicht entsprechen diese beiden Ports dem Aufruf bzw. dem Verlassen einer Prozedur
in imperativen Programmiersprachen. Zusätzlich kann ein Ziel aber auch durch Backtracking erneut bearbeitet werden. Kann Prolog ein Ziel nicht erfüllen, verlässt es die
entsprechende Box über den Fail -Port und startet den Backtracking-Vorgang. Dazu wird
das vorher erfüllte Ziel erneut bearbeitet und nach einer alternativen Lösung für dieses
Ziel gesucht, indem die Box dieses Ziels durch den Redo-Port erneut betreten wird.
Ein Tracer kann mit Hilfe des Portmodells dem Benutzer Informationen darüber geben, welches Ereignis in Bezug auf das aktuelle Ziel eingetreten ist. Dadurch ist es dem
Benutzer möglich, die Ausführung des Programms besser zu verstehen. Des Weiteren
kann der Benutzer lediglich für ihn relevante Ports beobachten und andere Ports ausblenden.
Aufgrund der Tatsache, dass Ziele im Portmodell durch Boxen repräsentiert werden,
wird zu den bereits erwähnten Begriffen Port- und Trace-Modell in der Literatur auch der
Begriff Boxmodell verwendet. Darüber hinaus lassen sich weitere Begriffe wie ProzedurBox-Kontrollfluss-Modell finden. In dieser Arbeit wird der Begriff Portmodell bevorzugt.
1
2
:- call merge(X, Y, Z) => sorted(X), sorted(Y).
:- exit merge(X, Y, Z) => sorted(Z).
Abbildung 4.8: Vor- und Nachbedingungen als Port-Annotationen.
31
4 Aspektorientierung aus Prolog-Sicht
4.4.2 Annotationen
Zusätzliche Tests an Ports
Die Idee der Port-Annotationen ist es, bei der Passage eines Ports bei bestimmten Zielen zusätzliche Tests ausführen zu lassen. Diese Tests beschreiben mittels gewöhnlicher
Prolog-Prädikate, welche Bedingungen für diese Ziele an diesem Port gelten müssen.
Um einen Port für ein bestimmtes Ziel zu annotieren, wird ein spezielles Infix-Prädikat
=>/2 verwendet. Das erste Argument beschreibt dabei den Port und ein Muster für Ziele, an denen der annotierte Test durchzuführen ist. Das zweite Argument enthält das
Testprädikat.
Abbildung 4.8 zeigt als Beispiel die Annotation des Prädikats merge/3 mit Vor- und
Nachbedingungen. Die Vorbedingung, dass die ersten beiden Argumente bereits sortierte
Listen enthalten, wird dabei am Call -Port annotiert. Analog wird die Nachbedingung,
dass das letzte Argument eine sortierte Liste liefert, am Exit-Port geprüft.
Ein annotierter Test kommt nun immer dann zur Anwendung, wenn bei der Bearbeitung des Ziels der annotierte Port passiert wird und das aktuelle Ziel vor seiner
Ausführung vom Muster der Port-Annotation subsumiert wird. Die Subsumption ersetzt dabei die für diesen Fall zu starke Unifikation. Unifiziert man Muster und aktuelles
Ziel, kann dies Instanziierungen von Variablen im aktuellen Ziel zur Folge haben. Dies
ist jedoch nicht die Intention der Verwendung des Musters. Vielmehr soll es beschreiben, in welchen Fällen ein annotierter Test ausgeführt werden soll. Dafür muss getestet
werden, ob das aktuelle Ziel dem Muster genügt. Subsumption als Unifikation ohne
Instanziierung von Variablen im aktuellen Ziel bietet hierfür das geeignete Mittel.
Grundsätzlich sind alle Ziele annotierbar. Dies beinhaltet also auch Konjunktionen,
Disjunktionen, Negationen sowie mittels ->/2 zusammengesetzte Ziele. Aus Effizienzgründen werden jedoch Konjunktionen, Disjunktionen und ->/2 standardmäßig als transparente Prädikate behandelt. Annotationen für diese Prädikate werden somit ignoriert.
Alle übrigen System- und Benutzerprädikate, auch solche höherer Ordnung, sind hingegen beliebig annotierbar.
Templates
Mit den bisher vorgestellten Beschreibungsmitteln ist es lediglich möglich, zu annotierende Ziele über ein subsumierendes Muster auszuwählen. Somit können das zugrundeliegende Prädikat und die Argumente des Ziels überprüft werden. Bisher ist es aber
nicht möglich, einen Test nur unter bestimmten zusätzlichen Bedingungen auszuführen.
Als Ausweg könnte versucht werden, eine zusätzliche Bedingung in den Test zu integrieren. Dann wird die Bedingung allerdings erst beim Passieren des Ports getestet. Zu
diesem Zeitpunkt können Informationen vom Zeitpunkt vor der Ausführung des Ziels bereits durch Unifikationen verloren gegangen sein. Die Überprüfung des Musters hingegen
geschieht unabhängig vom annotierten Port stets vor der Bearbeitung des Ziels.
Daher können nach dem Zielmuster mittels with zusätzlich Templates angegeben werden, welche vor Ausführung des Ziels erfüllt sein müssen, bevor der entsprechende Port
32
4.4 Port-Annotationen
1
2
3
:- exit plus(X, Y, Z) with ground(X), ground(Y) => ground(Z).
:- exit plus(X, Y, Z) with ground(X), ground(Z) => ground(Y).
:- exit plus(X, Y, Z) with ground(Y), ground(Z) => ground(X).
Abbildung 4.9: Nachbedingungen für unterschiedliche Instanziierungssituationen mit
Hilfe von Templates.
tatsächlich annotiert wird. Auch für Templates gilt, dass diese keine Instanziierungen
von Variablen des aktuellen Ziels zur Folge haben dürfen.
Abbildung 4.9 zeigt ein einfaches Beispiel für die Verwendung von Templates. Das
Beispiel spezifiziert die Instanziierung der Argumente durch das Prädikat plus/3. Sind
mindestens zwei Argumente vor der Ausführung vollständig instanziiert, so ist es das
verbleibende dritte Argument nach der Ausführung ebenso.
Abbildung 4.10 zeigt, wie auf Werte von Variablen vor und nach der Ausführung eines
Ziels zugegriffen werden kann. Das Beispiel spezifiziert, dass die Negation eines Ziels
dieses selbst nicht beeinflusst. Hierzu wird der Exit-Port von negierten Zielen annotiert.
Mittels eines Templates lässt sich dabei der Zustand vor der Bearbeitung der Negation
konservieren. Dieser konservierte Zustand kann dann beim Verlassen der Negation zur
Überprüfung, ob das ursprünglich zu negierende Ziel unverändert geblieben ist, genutzt
werden.
Kontext
Zusätzlich zur Qualifikation über das auszuführende Ziel und dessen Argumente mittels
Subsumptionsmuster und Templates besteht die Möglichkeit, den Kontext des aktuellen
Ziels ebenfalls als Bedingung für eine Annotation zu berücksichtigen. Den Kontext bildet
dabei das übergeordnete Ziel. Mittels within ist es nun möglich, ein Subsumptionsmuster
für diesen Kontext zu formulieren. Darüber hinaus ist es auch möglich, einen negativen
Kontext anzuführen. In diesem Fall kommt die Annotation nur dann zum Tragen, wenn
das aktuelle Ziel außerhalb des angegebenen Kontextes liegt.
Mit Hilfe von Kontextinformationen lässt sich zum Beispiel überprüfen, ob bei der
Abarbeitung eines rekursiven Prädikats der Fall eintritt, dass der rekursive Aufruf allgemeiner als der vorherige Aufruf ist. Dies ist im Allgemeinen ein Fehler. Abbildung 4.11
zeigt, wie dies für ein rekursives Prädikat p/1 spezifiziert werden kann. Dazu wird der
Call -Port von rekursiven Aufrufen des Prädikats mit einem Test annotiert. Der rekursive
Aufruf wird mit Hilfe des Kontextes des aktuellen Ziels beschrieben. Dabei kann sowohl
auf das Argument des übergeordneten Ziels als auch auf das Argument des aktuellen
Ziels zurückgegriffen werden. Der annotierte Test nutzt beide Argumente und testet unter Verwendung des Hilfsprädikats subsumes/2, ob das Argument des rekursiven Aufrufs
1
:- exit \+ X with copy_term(X, Copy) => X =@= Copy.
Abbildung 4.10: Zugriff auf Variablenwerte vor und nach der Ausführung eines Ziels mit
Hilfe eines Templates.
33
4 Aspektorientierung aus Prolog-Sicht
1
:- call p(X) within p(Y) => \+ subsumes(X, Y).
2
3
subsumes(X, Y):- copy_term(Y, YCopy), numbervars(YCopy, YGround), X = YGround.
Abbildung 4.11: Prüfung korrekter Rekursion unter Verwendung von Kontextinformationen.
allgemeiner als das des übergeordneten Ziels ist. Hierfür wird eine Grundinstanz des Arguments des übergeordneten Ziels erzeugt und getestet, ob das Argument des rekursiven
Aufrufs mit dieser Grundinstanz unifizierbar ist.
Parametrisierung
Das Beispiel aus Abbildung 4.11 erlaubt es, ein Prädikat um einen Test auf korrekte
Rekursion zu erweitern. Möchte man mehrere solcher Prädikate annotieren, muss die
Annotation mit jedem einzelnen Prädikat wiederholt werden. Dieser Vorgang lässt sich
mit einem parametrisierten Prädikat zur Annotation, wie es Abbildung 4.12 zeigt, vereinfachen.
Grundlage hierfür ist, dass das Prädikat =>/2, welches die Annotation vollzieht, wie jedes andere Prolog-Prädikat in Klauseln verwendet werden kann. Darüber hinaus können
die Argumente des Prädikats zur Parametrisierung der Annotation genutzt werden. Somit lässt sich ein Prädikat recursion/1 definieren, welches für einen als Argument
übergebenen Prädikataufruf die Annotation veranlasst. Bei der Annotation werden Subsumptionsmuster aus dem Argument des Prädikats recursion/1 erzeugt. Soll nun ein
bestimmtes Prädikat annotiert werden, muss dazu lediglich recursion/1 mit dem entsprechenden Prädikat als Argument aufgerufen werden.
4.4.3 Umsetzung
Programm-Transformation
Das Beispielsystem Nope integriert Port-Annotationen in ein gegebenes Prolog-Programm, indem es den Quellcode des Programms transformiert. Die Transformationen
erfolgen dabei innerhalb der Klauselkörper durch die Ersetzung von Teilzielen durch
komplexere Ziele. Abbildung 4.13 zeigt die Transformationsregeln für Annotationen an
verschiedenen Ports.
Ob eine Transformationsregel zur Anwendung kommt, kann dabei durch statische
Tests entschieden werden. Zunächst muss das zu transformierende Ziel innerhalb des
1
2
recursion(P):- functor(P, F, N), functor(P0, F, N),
( call P within P0 => \+ subsumes(P, P0) ).
3
4
:- recursion(p(X)).
Abbildung 4.12: Parametrisierte Annotation zur Prüfung korrekter Rekursion.
34
4.4 Port-Annotationen
Goal
call
;
call soft(FullTemplate, Goal ) →
(Constraint else Warning), Goal ; Goal
Goal
exit
;
call soft(FullTemplate, Goal ) →
Goal , (Constraint else Warning) ; Goal
Goal
;
fail
call soft(FullTemplate, Goal ) →
(Goal ; (Constraint else Warning), fail) ; Goal
Goal
redo
call soft(FullTemplate, Goal ) →
Goal , (true ; (Constraint else Warning), fail) ; Goal
;
Abbildung 4.13: Transformationen
Annotationen.
von
Prädikaten
zur
Integration
von
Port-
Kontextes der Port-Annotation liegen. Des Weiteren muss das Subsumptionsmuster dasselbe Prädikat wie das zu transformierende Ziel aufweisen, da sonst eine Subsumption
bereits statisch ausgeschlossen werden kann.
Subsumption
Der verbleibende dynamische Subsumptionstest bezieht sich dann nur noch auf die Subsumption der Argumentpositionen. Zusätzlich müssen die Templates dynamisch getestet
werden. Dabei kann die Subsumption der Argumentpositionen als zusätzliches Template
angesehen werden, so dass beide Tests gemeinsam vollzogen werden können. Für ein zu
transformierendes Ziel
p(A1 , . . . , An )
und eine Port-Annotation
← Port p(P1 , . . . , Pn ) with Template ⇒ Constraint
ergibt sich so
FullTemplate = (A1 = P1 , . . . , An = Pn , Template)
als dynamisches Testprädikat.
Bei der Ausführung des dynamischen Test ist es wichtig, dass Variablen im annotierten Ziel nicht instanziiert werden. Daher wird in den Transformationsregeln dieser Test
mittels des Prädikats call soft/2 ausgeführt. Die Definition dieses Prädikats zeigt Abbildung 4.14. Zunächst wird eine Kopie des aktuellen Ziels erstellt. Danach wird der Test
ausgeführt, wobei durch die Verwendung von once/1 weitere Lösungen ausgeschlossen
werden können, da lediglich der Erfolg des Tests für die Annotation relevant ist. Nach
der Ausführung des Tests wird geprüft, ob das aktuelle Ziel nachwievor strukturell äquivalent zu der zuvor erstellten Kopie ist. Ist dies der Fall, ist der Test erfolgreich. Im
35
4 Aspektorientierung aus Prolog-Sicht
1
2
3
call_soft(Goal, Invariant):- copy_term(Invariant, Copy),
once(Goal),
Invariant =@= Copy.
Abbildung 4.14: Implementation zur Vermeidung von Variableninstanziierungen bei
Tests auf Subsumption.
transformierten Ziel kommt in diesen Fällen dann die Annotation zum Tragen. Anderenfalls wird das Ziel ohne Annotation ausgeführt.
Warnungen
In den transformierten Zielen wird der annotierte Test Constraint durch das InfixPrädikat else/2 gemeinsam mit einer Warnung Warning ausgeführt. Abbildung 4.15
zeigt die Implementation dieses Prädikats. Ist Constraint nicht erfolgreich, wird versucht, Warning aufzurufen. Diese Warnung kann entweder in der Annotation durch den
Benutzer angegeben werden oder vom System erzeugt werden. Sollte auch das Warnprädikat fehlschlagen, bleibt else/2 dennoch erfolgreich. Darüber hinaus hinterlässt es
durch die Verwendung von Negationen weder Choicepoints, noch werden Variableninstanziierungen durchgeführt.
Einfluss auf die Semantik von Programmen
Die Transformationsregeln garantieren, dass unter bestimmten Bedingungen die Semantik eines annotierten Programms erhalten bleibt, d.h. das annotierte Programm produziert dieselben Antworten in derselben Reihenfolge wie das Originalprogramm. Diese
Bedingungen betreffen Templates und die annotierten Tests.
Zunächst müssen beide ohne extralogische Seiteneffekte auf das Originalprogramm
sein. Dennoch können Prädikate mit solchen Seiteneffekten verwendet werden. Dabei
liegt es allerdings in der Verantwortung des Programmierers, sicherzustellen, dass diese
Seiteneffekte das Programm nicht beeinflussen.
Zusätzlich müssen sowohl Templates als auch annotierte Tests terminieren. Die Überprüfung der Templates mittels call soft/2 und der Aufbau der Transformationsregeln
stellt dann sicher, dass das Originalziel nicht beeinflusst wird und garantiert zur Ausführung kommt. Darüber hinaus garantiert die Verwendung von once/1 in call soft/2,
dass durch die Überprüfung keine zusätzlichen Choicepoints erzeugt werden. Ist die
Überprüfung erfolgreich, wird das Originalziel in Kombination mit dem annotierten Test
ausgeführt. Anderenfalls wird das Originalziel ohne Annotation ausgeführt. Die Termi1
2
Constraint else Warning :- \+ Constraint ->
( \+ Warning -> true ; true ) ; true.
Abbildung 4.15: Implementation zur Vermeidung von Variableninstanziierungen und
Choicepoints bei der Ausführung annotierter Tests und Warnungen.
36
4.4 Port-Annotationen
nation der Templates garantiert dabei, dass einer der beiden Fälle in der Ausführung
erreicht wird.
Für den Fall, dass die Überprüfung der Templates erfolgreich ist, wird der annotierte
Test mittels else/2 so platziert, dass er den entsprechenden Port repräsentiert. Dabei
wird durch else/2 verhindert, dass der annotierte Test das Originalziel beeinflusst. Die
Terminierung des annotierten Tests garantiert hier, dass das transformierte Ziel genau
dann terminiert, wenn das Originalziel terminiert. Darüber hinaus garantiert else/2,
dass auch innerhalb des Tests keine Choicepoints erzeugt werden.
4.4.4 Aspektorientierung
Port-Annotationen bieten eine eingeschränkte Möglichkeit aspektorientierter Programmierung in Prolog. Im Folgenden werden daher Beschreibung, Umsetzung und Anwendung der Port-Annotationen mit Begriffen und Modellen der Aspektorientierung in Verbindung gesetzt.
Pointcut-Beschreibungen und Advice
Den Port-Annotationen liegt ein dynamisches Joinpoint-Modell zugrunde, welches mit
dem Modell der Method-Call Interception aus Abschnitt 3.4.2 vergleichbar ist. Ein Joinpoint ist dabei ein Port eines Ziels. Die Notation mittels =>/2 führt direkt zu weiteren bekannten Begriffen. Die Beschreibung der linken Seite, mit Subsumptionsmuster, Templates und Kontext, stellt eine Pointcut-Beschreibung dar. Die rechte Seite beschreibt das einzufügende Advice. Auch diese Notation weist Ähnlichkeiten zur MethodCall Interception aus der Objektorientierung auf. In den Pointcut-Beschreibungen der
Method-Call Interception lassen sich nur Joinpoints einer Art zusammenfassen. Für die
Port-Annotationen gilt das Gleiche. Auch hier lässt sich nur jeweils ein Port in einer
Pointcut-Beschreibung verwenden. Eine weitere Gemeinsamkeit bildet die Kombination
von Pointcut-Beschreibung und Advice. Wie bei der Method-Call Interception lassen
sich auch bei Port-Annotationen Pointcuts nur zusammen mit dem zu platzierenden
Advice beschreiben. In den Pointcut-Beschreibungen der Port-Annotationen lassen sich
durch die Verwendung von Templates ähnlich wie in AspectJ und der Method-CallInterception auch Informationen aus dem Kontext der adressierten Joinpoints referenzieren. Des Weiteren können Port-Annotationen parametrisierte Pointcut-Beschreibungen
enthalten, was weder in AspectJ noch bei der Method-Call Interception möglich ist.
Statisches Weben
Die Umsetzung der Port-Annotation durch Programmtransformationen entspricht einem
statischen Weben von Komponente und Aspekten. Das zu annotierende Programm stellt
dabei die Komponente dar, während Port-Annotationen sich zu verschiedenen Aspekten gruppieren lassen. Dabei werden wesentliche Voraussetzungen der aspektorientierten
Programmierung erfüllt. So wird der Kontrollfluss der Komponente erhalten und Aspekte kommen unabhängig voneinander zur Ausführung, wobei diese Ausführung garantiert
37
4 Aspektorientierung aus Prolog-Sicht
ist.
Für die Implementation weiterführender aspektorientierter Ideen in Prolog liefern die
Port-Annotationen nützliche Hinweise. So wird deutlich, dass bei der Überprüfung, ob
ein Joinpoint Teil einer Pointcut-Beschreibung ist, ein Subsumptionstest der Unifikation
vorzuziehen ist, da ansonsten der aktuelle Joinpoint durch Variableninstanziierungen beeinflusst werden kann. Darüber hinaus zeigen Templates, dass Pointcut-Beschreibungen
auf einfache Art Informationen aus den adressierten Joinpoints referenzieren können eine Möglichkeit, wie sie z.B. auch in AspectJ (vgl. Abschnitt 3.3.2) existiert. Letztlich offenbart die Umsetzung parametrisierter Annotationen, dass auch parametrisierte
Pointcut-Beschreibungen leicht in Prolog zu unterstützen sind. Diese Überlegungen fließen in das in Kapitel 5 vorzustellende Joinpoint-Modell ein.
Anwendungen
Port-Annotationen ermöglichen in gewissem Maße aspektorientierte Prolog-Programmierung. Ursprünglich zum Debugging von Prolog-Programmen [51] entwickelt, lassen
sich mittels Port-Annotationen typische Crosscutting Concerns (vgl. Abschnitt 4.2) separat beschreiben. Die Anwendungsfälle gehen dabei über reine Debugging-Zwecke weit
hinaus. Insbesondere wird die Idee des Design-by-Contract durch Spezifikationen unterstützt . Die Publikationen zum Thema beziehen sich vorrangig auf die Spezifikation
und Verifikation von Prolog-Programmen, was in entsprechenden Beispielen zu Vor- und
Nachbedingungen, Typen und Modi dokumentiert wird.
Die Beschränkung auf reine Tests als Advices wird einigen anderen, typisch aspektorientierten Anwendungsfällen jedoch nicht gerecht. Die Restriktionen der PortAnnotationen bezüglich der Semantikerhaltung erweisen sich hier als zu streng. Dennoch bieten
sie eine gute Diskussionsgrundlage darüber, wie Aspekte sowohl Komponenten als auch
andere Aspekte in ihrer Semantik beeinflussen können.
4.4.5 Verwandte Konzepte
Neben den Port-Annotationen gibt es weitere Möglichkeiten, zusätzliche Prädikataufrufe
an Ports zu platzieren.
Port-Advices
So enthalten die Bibliotheken von DEC-10-Prolog ein von Richard O’Keefe implementiertes Prädikat advice/3, welches es erlaubt, Advices an Ports eines Ziels zu platzieren [70]. Der Ansatz ist durch fehlende Kontextinformationen und die Beschränkung von
Mustern auf den aktuellen Port in seinen Beschreibungsmitteln limitiert. Dennoch weist
er bereits 1984 fundamentale aspektorientierte Konzepte auf. Hierzu gehört vor allem die
Übernahme des Advice-Konzepts und -Begriffs von Teitelman [89] aus Interlisp [90]. Ein
ähnlicher Transfer auf prozedurale und objektorientierte Programmiersprachen führt
1997 zum Konzept der Aspektorientierten Programmierung [45] und prägt dort den
Advice-Begriff [42].
38
4.4 Port-Annotationen
1
pointcut(Joinpoint, Pointcut):- ... % Pointcut-Beschreibung
2
3
4
5
6
prolog_trace_interception(Port, Frame, Choice, Action):pointcut(jp(Port, Frame, Choice), pc(P1, ..., Pn)) ->
..., % Advice
Action = ... % weiterer Kontrollfluss
Abbildung 4.16: Schematische Verwendung von prolog trace interception/4 zur
aspektorientierten Programmierung.
Die Before-After -Around -Ontologie, welche sowohl in Interlisp als auch in späteren
aspektorientierten Ansätzen wie AspectJ [44] verwendet wird, wird durch die PortAdvices erstmals auf das Port-Modell übertragen. Diese Übertragung prägt neben den
Port-Annotationen auch das in Kapitel 5 entwickelte Joinpoint-Modell.
Nutzung des Tracers
SWI-Prolog [93] bietet mit dem Prädikat prolog trace interception/4 eine weitere
Möglichkeit einer auf dem Port-Modell basierenden aspektorientierten Programmierung.
Die ursprüngliche Intention des Prädikats ist es, dem Anwender eine individuelle Beeinflussung des Tracing-Vorgangs zu ermöglichen. Vor der Ausgabe des aktuellen Ports führt
der Tracer das Prädikat aus. Der Anwender kann durch Klauseln für dieses Prädikat das
Verhalten des Tracers beeinflussen. Die ersten drei Argumente gestatten dabei einen
Zugriff auf den aktuellen Port, den aktuellen Frame und den letzten Choicepoint. Der
Tracer führt dann den Klauselkörper aus und setzt seine Arbeit gemäß der im letzten
Argument angegebenen Aktion fort. Diese Aktion kann die Fortsetzung des Tracings,
der Versuch, das aktuelle Ziel erneut zu bearbeiten, oder ein Fehlschlagen des aktuellen
Ziels sein.
Das Prädikat lässt sich auch zur Platzierung beliebiger zusätzlicher Ziele an Ports verwenden. Durch die Informationen über den aktuellen Frame und den letzten Choicepoint
lassen sich darüber hinaus Kontextinformationen berücksichtigen. Die im Klauselkörper
enthaltenen Advices können dabei die Semantik des ursprünglichen Programms beeinflussen. Des Weiteren lässt sich mittels der Fortsetzungsaktion des Tracers der Kontrollfluss beeinflussen.
Aspekte lassen sich so separat mittels Klauseln des Prädikats beschreiben. Führt man
zusätzlich ein Prädikat pointcut/2 zur Beschreibung von Pointcuts ein, erhält man ein
relativ ausdrucksstarkes Joinpoint-Modell. Ein Joinpoint wird dabei durch den aktuellen
Port, den aktuellen Frame und den letzten Choicepoint eindeutig bestimmt. Mittels des
Prädikats pointcut/2 lassen sich diese Informationen nutzen, um parametrisierte Pointcuts zu beschreiben. Klauseln für das Prädikat prolog trace interception/4 platzieren dann Advices an den durch einen Pointcut erfassten Joinpoints. Abbildung 4.16 zeigt
die schematische Verwendung.
39
5 Statisches Joinpoint-Modell
In diesem Kapitel wird schrittweise ein statisches Joinpoint-Modell für Prolog-Programme entwickelt. Dabei wird die Idee verfolgt, zusätzliche Prädikataufrufe an den verschiedenen Ports von Boxen eines erweiterten Portmodells zu ermöglichen. Der Ansatz ähnelt
damit dem der Port-Annotationen aus Abschnitt 4.4. Im Gegensatz zu diesen bezieht
sich das vorgestellte Joinpoint-Modell aber nicht auf die dynamische Struktur von Zielen während der Ausführung eines Programms, sondern auf dessen statische Struktur im
Quelltext.
Das Modell wird über Definitionen von Joinpoints, Pointcuts, Advices und Aspekten
entwickelt. Als laufendes Beispiel dient dabei die separate Beschreibung der Summierung
von Listenelementen. Abbildung 5.1 zeigt dieses Beispiel ohne eine separate Beschreibung. Den Ausgangspunkt bildet das Prädikat list/1, welches zum Prädikat sumlist/2
erweitert wird. Die Erweiterung manifestiert sich in einem zusätzlichen Argument, der
Umbenennung des Prädikats und zusätzlichen Aufrufen des Prädikats is/2. Alle diese Erweiterungen lassen sich als Aspekt separat beschreiben, wobei das vorzustellende
Modell die Grundlage für diese Beschreibung bildet.
5.1 Joinpoints
Die Grundlage für das zu entwickelnde Joinpoint-Modell bildet eine Erweiterung des
in 4.4.1 vorgestellten Portmodells von Byrd. Diese Erweiterung wird im Folgenden
erläutert, bevor anschließend eine auf den Boxen des Modells basierende JoinpointDefinition präsentiert wird.
5.1.1 Erweitertes Portmodell
Das klassische Portmodell, wie es Byrd vorstellte, erfuhr zur Umsetzung konkreter Tracer
in einigen Prolog-Systemen verschiedene Erweiterungen. So bieten die Tracer in SWIProlog [93] und Sicstus-Prolog [35] seit der Einführung der Ausnahmebehandlung in
1
2
list([]).
list([H|T]):- list(T).
3
4
5
sumlist([], Sum):- Sum is 0.
sumlist([H|T], Sum):- sumlist(T, TSum), Sum is TSum + H.
Abbildung 5.1: Laufendes Beispiel zur Summierung von Listenelementen.
40
5.1 Joinpoints
call
trymatch
enterbody
exitbody
exit
failmatch
failbody
trymatch
redobody
enterbody
exitbody
failmatch
fail
failbody
redobody
redo
error
Abbildung 5.2: Erweitertes Portmodell des IF/Prolog-Tracers. Die äußere Zielbox beinhaltet zwei Klauselboxen. Mögliche Kontrollflüsse sind durch Pfeile gekennzeichnet.
den Prolog-Standard [20, 36] einen zusätzlichen Error -Port zum Verlassen der Box des
aktuellen Ziels an, falls während der Abarbeitung dieses Ziels eine unbehandelte Ausnahme ausgelöst wurde. Zusätzlich verfügen beide Tracer über einen Unify-Port innerhalb
der Box, welcher immer dann passiert wird, wenn ein Klauselkopf erfolgreich mit dem
aktuellen Ziel unifiziert werden konnte.
Das dem Tracer der kommerziellen Prolog-Version IF/Prolog [81] zugrundeliegende
erweiterte Portmodell aus Abbildung 5.2 ist erheblich komplexer. Wesentlicher Bestandteil der Erweiterung ist die Unterscheidung zweier Arten von Boxen für Ziele und für
Klauseln. Die Ports zum Betreten und Verlassen einer Box, welche ein Ziel repräsentiert,
entsprechen denen des Modells von Byrd, ergänzt um einen Error -Port. Wird eine solche
Zielbox durch den Call -Port betreten, werden innerhalb dieser Zielbox mehrere Klauselboxen erzeugt. Dabei wird für jede Klausel des Prädikats genau eine korrespondierende
Klauselbox erzeugt.
Bei der Abarbeitung des aktuellen Ziels können diese Klauselboxen nun durch verschiedene Ports betreten und verlassen werden. Um das aktuelle Ziel zu erfüllen, wird
zunächst versucht, das aktuelle Ziel mit dem Klauselkopf zu unifizieren. Dazu wird die
erste Klauselbox über den Trymatch-Port betreten. Sind Klauselkopf und aktuelles Ziel
nicht unifizierbar, wird die Klauselbox durch den Failmatch-Port wieder verlassen und
stattdessen versucht, mit der folgenden Klauselbox fortzufahren. Anderenfalls wird der
Klauselkörper durch den Enterbody-Port betreten.
In diesem Klauselkörper werden nun sukzessive Zielboxen für Teilziele abgearbeitet,
um das aktuelle Ziel zu erfüllen. Die im Klauselkörper erzeugten Zielboxen sind dabei
durch die verwendete Klausel und die Unifkation des aktuellen Ziels mit dem Klauselkopf vorgegeben. Gelingt es, diese Teilziele erfolgreich abzuarbeiten, wird zunächst die
Klauselbox durch den Exitbody-Port und anschließend die umgebende Zielbox durch den
41
5 Statisches Joinpoint-Modell
Exit-Port verlassen. Gelingt dies jedoch nicht, wird die Klauselbox durch den FailbodyPort verlassen und versucht, die nächste Klausel zur Abarbeitung des aktuellen Ziels zu
verwenden.
Wird die letzte Klauselbox innerhalb einer Zielbox durch den Failmatch- oder Failbody-Port verlassen, schlägt das aktuelle Ziel fehl und die umgebende Zielbox wird durch
den Fail -Port verlassen. Durch das Betreten des Redo-Ports der umgebenden Zielbox
wird die zuletzt durch einen Exitbody-Port verlassene Klauselbox durch deren Redobody-Port erneut betreten. Innerhalb der Klauselbox wird dann – wiederum durch den
Redo-Port – das letzte Teilziel erneut betreten und somit der Backtracking-Vorgang
umgesetzt.
5.1.2 Boxen und Joinpoints
Den Ausgangspunkt für das Modell bilden die Boxen des erweiterten Portmodells aus
dem vorangegangenen Abschnitt. An den Ports dieser Boxen sollen Advices platziert werden können. Es bieten sich somit zwei Verfahrensweisen zur Bestimmung von Joinpoints
an. Bei der ersten Variante wird jeder Port als Joinpoint betrachtet. Dieses Vorgehen
ist mit dem Joinpoint-Modell der Method-Call Interception vergleichbar, auch wenn jenes dynamischer Natur ist. Alternativ können auch Ziele und Klauseln als Joinpoint
betrachtet werden und die Ports durch verschiedene Advice-Arten adressiert werden.
Diese Variante ähnelt dem dynamischen Joinpoint-Modell von AspectJ. Sie soll an dieser Stelle bevorzugt werden.
Damit bilden zwei Arten von Joinpoints die Grundlage des statischen JoinpointModells. Ein atomares Ziel
p(X1 , . . . , Xn )
bildet einen Ziel-Joinpoint der Form
goal (p(X1 , . . . , Xn ))
Zusammengesetzte Ziele oder Ziele als Argumente von Metaprädikaten sind hingegen
keine Joinpoints. Eine Klausel
p(X1 , . . . , Xn ) ← B1 , . . . , Bm .
stellt einen Klausel-Joinpoint der Form
clause (p(X1 , . . . , Xn ), (B1 , . . . , Bm ))
dar. Fakten werden als Klauseln mit dem Klauselkörper true behandelt. Anfragen bilden
keine Joinpoints. Im weiteren Verlauf dieser Arbeit werden die Begriffe Ziel, Zielbox und
Ziel-Joinpoint weitestgehend synonym verwendet. Auf gleiche Art wird mit den Begriffen
Klausel, Klauselbox und Klausel-Joinpoint verfahren.
42
5.2 Pointcut-Beschreibungen
1
% pointcut(+Joinpoint, +Context, +Aspect, -Pointcut)
2
3
4
5
pointcut(clause(list([]), Body), Context, sample1, init_clause).
pointcut(clause(list([H|T]), Body), Context, sample1, rec_clause).
pointcut(goal(list(L)), Context, sample1, rec_goal).
Abbildung 5.3: Einfache Pointcut-Beschreibungen.
5.2 Pointcut-Beschreibungen
5.2.1 Erfassung von Joinpoints
Im vorliegenden Modell werden Pointcuts durch ein Prädikat pointcut/4 beschrieben.
Es setzt Joinpoints, den Kontext ihres Auftretens und Pointcuts verschiedener Aspekte
in Relation. Durch den Aufruf dieses Prädikats können Pointcuts eines Aspekts ermittelt
werden, welche einen gegebenen Joinpoint erfassen. Der Kontext des Joinpoints wird in
den folgenden Darstellungen zunächst vernachlässigt.
Abbildung 5.3 zeigt einfache Pointcut-Beschreibungen eines Aspekts sample1. Klauseln des Prädikats list/1 werden von einem Pointcut init clause erfasst, wenn der
Klauselkopf die leere Liste als Argument enthält. Wird im ersten Argument eine Liste
in ihren Kopf und eine Restliste zerlegt, wird die Klausel hingegen von einem Pointcut rec clause erfasst. In beiden Fällen wird der Klauselkörper nicht zur Beschreibung
des Pointcuts herangezogen. Eine weitere pointcut/4-Klausel beschreibt den Pointcut
rec goal welcher Ziele des Prädikats list/1 erfasst.
Die Pointcut-Beschreibungen des Aspekts sample/1 können zur Klassifikation der
Joinpoints des Programms list/1 aus Abbildung 5.1 genutzt werden. Dabei wird die
erste Klausel des Prädikats vom Pointcut init clause erfasst. Die zweite Klausel ist
hingegen Teil des Pointcuts rec clause. Der Pointcut rec goal erfasst den rekursiven
Aufruf des Prädikats list/1 im Körper dieser Klausel.
Subsumption
Der Aufruf des Prädikats pointcut/4 ist allerdings auch für einen Klausel-Joinpoint
list(X) ← var (X). erfolgreich. Die Klausel wird von den Pointcuts init clause und
rec clause erfasst, wobei X mit der leeren Liste bzw. mit der in Listenkopf und Restliste
zerlegten Liste unifiziert wird. Durch eine solche Unifikation beeinflusst die Prüfung, ob
ein Joinpoint Teil eines Pointcuts ist, den Joinpoint. Darüber hinaus werden weitere
Prüfungen beeinflusst. So verhindert die Unifikation von X mit der leeren Liste, dass
der Joinpoint auch vom Pointcut rec clause erfasst wird. Des Weiteren beeinflusst die
Unifikation den Ziel-Joinpoint var (X).
Die Verwendung einer Kopie des Joinpoints bei der Überprüfung kann solche Einflüsse zunächst verhindern, bringt aber neue Nachteile mit sich. Durch die Erstellung
einer Kopie sind Variablen des Joinpoints für Pointcut-Beschreibungen und Advices unzugänglich. Oft werden diese aber in zusätzlichen Prädikatsaufrufen benötigt. So wird im
43
5 Statisches Joinpoint-Modell
1
2
3
4
5
pointcut(clause(list(L), Body), Context, sample2, init_clause):nonvar(L), L = [].
pointcut(clause(list(L), Body), Context, sample2, rec_clause):nonvar(L), L = [H|T].
pointcut(goal(list(L)), Context, sample2, rec_goal).
Abbildung 5.4: Berücksichtigung der Subsumption in Pointcut-Beschreibungen.
Beispiel der Summierung von Listenelementen auf das jeweilige Listenelement zurückgegriffen.
Einen Ausweg bildet die von den Port-Annotationen bekannte Subsumption. Zur
Überprüfung, ob ein Joinpoint durch einen Pointcut erfasst wird, werden Aufrufe des
Prädikats pointcut/4 gekapselt. Die Kapselung wacht darüber, dass im Joinpoint auftretende Variablen uninstanziiert bleiben. Pointcut-Beschreibungen stellen somit Subsumptionsmuster für Joinpoints dar. Eine entsprechende Implementation enthält Anhang A.5.1.
Instanziiert ein Aufruf von pointcut/4 eine im Joinpoint auftretende Variable, wird
der Joinpoint nicht von dem beschriebenen Pointcut erfasst. Stattdessen wird eine Warnung ausgegeben. Solche Warnungen können durch den Verzicht auf Unifikationen in
Pointcut-Beschreibungen vermieden werden. Dies betrifft sowohl explizite Unifikationen im Klauselkörper als auch implizite Unifikationen im Klauselkopf. Meist genügen
jedoch zusätzliche Tests, um Instanziierungen von Variablen in Joinpoints zu vermeiden. Abbildung 5.4 enthält Pointcut-Beschreibungen des Aspekts sample2, welche keine
Warnungen erzeugen. Hierzu wurden die impliziten Unifikationen im Klauselkopf in den
Klauselkörper verschoben und um zusätzliche Tests ergänzt.
Da unter solchen Anpassungen die Lesbarkeit von Pointcut-Beschreibungen leiden
kann, sollte jedoch zwischen der allgemeinen Anwendbarkeit eines Aspekts und dessen
Verständlichkeit abgewogen werden. Für das Beispiel der Summierung von Listenelementen ist die Angabe eines Subsumptionsmusters im Klauselkopf ausreichend, da das
Programm, auf welches der Aspekt angewendet werden soll, bekannt ist. Eine Wiederverwendung des Aspekts ist durch die Beschränkung auf das Prädikat list/1 ohnehin
erschwert und nicht zu erwarten.
Pointcut-Argumente
In einigen Fällen kann es notwendig sein, dass Advices Informationen über einen Joinpoint benötigen, die über die reine Zugehörigkeit zu einem Pointcut hinausgehen. Diese
Informationen können als Argumente eines Pointcuts geführt und dann in Advices verwendet werden. Beispielsweise wird für die Summierung der Elemente einer Liste das
aktuelle Listenelement benötigt. Abbildung 5.5 zeigt eine Erweiterung der PointcutBeschreibungen des Aspekts sample1 aus Abbildung 5.3. Der Pointcut rec clause/1
führt nun das aktuelle Listenelement, welches zur Summierung benötigt wird, als Argument.
Ein solcher Referenzierungsmechanismus für Daten eines Joinpoints durch Pointcuts
44
5.2 Pointcut-Beschreibungen
1
2
3
pointcut(clause(list([]), Body), Context, sample3, init_clause).
pointcut(clause(list([H|T]), Body), Context, sample3, rec_clause(H)).
pointcut(goal(list(L)), Context, sample3, rec_goal).
Abbildung 5.5: Referenzierung von Daten eines Joinpoints in Pointcut-Beschreibungen.
findet sich auch in den aspektorientierten Ansätzen von AspectJ und der Method-Call
Interception. Darüber hinaus bieten diese Ansätze durch den Zugriff auf Membervariablen des Aspekts oder von Objekten die Möglichkeit, Daten zwischen Advices auszutauschen. Da sich das Variablenkonzept Prologs fundamental von denen objektorientierter Sprachen unterscheidet, ist ein anderer Ansatz notwendig.
In einer Prolog-Klausel werden Daten durch gemeinsame Variablen zwischen Zielen
ausgetauscht. Advices können die Daten dieser Ziele durch Pointcut-Argumente ebenfalls
nutzen. Der gleiche Mechanismus kann auch genutzt werden, um Daten zwischen Advices
auszutauschen. Dazu stellt ein Pointcut als Argument eine ungebundene Variable zur
Verfügung, welche mehrere Advices gemeinsam nutzen können.
Ein Aspekt zur Summierung von Listenelementen führt in den betroffenen Klauseln
des Prädikats list/1 mehrere neue Variablen ein. In der Initialisierungsklausel ist dies
lediglich die Ergebnisvariable. Die rekursive Klausel enthält zwei neue Variablen für das
Ergebnis des rekursiven Aufrufs und das Gesamtergebnis. Beide werden durch einen
zusätzlichen Prädikataufruf in Verbindung gesetzt. Allgemein wird also für jedes Ergebnis eine neue Variable eingeführt. Die Pointcut-Beschreibungen des Aspekts sample4
aus Abbildung 5.6 stellen diese Variablen später einzuführenden Advices zur Verfügung.
5.2.2 Kontext
Die Berücksichtigung des Kontextes, in welchem ein Joinpoint liegt, bietet weitere Beschreibungsmöglichkeiten für Pointcuts. Das zweite Argument des Prädikats pointcut/4
führt hierfür eine Termrepräsentation des Joinpoint-Kontextes. Diese Repräsentation
enthält Pointcuts, welche den aktuellen Joinpoint, benachbarte oder übergeordnete Joinpoints erfassen, in separaten Listen. Über das Prädikat context/4 kann auf diese Pointcuts zugegriffen werden.
Paralleler Kontext
Ein Joinpoint kann von mehreren Pointcuts erfasst werden. Dabei kann ein Pointcut Informationen anderer Pointcuts, welche denselben Joinpoint erfassen, kombinieren. Solche
Pointcuts bilden den parallelen Kontext eines Joinpoints. Eine einfache Möglichkeit, auf
Informationen von Pointcuts aus dem parallelen Kontext zurückzugreifen, besteht in
1
2
pointcut(clause(list(L), Body), Context, sample4, sum(Sum)).
pointcut(goal(list(L)), Context, sample4, sum(Sum)).
Abbildung 5.6: Einführung neuer Variablen zum Austausch von Daten zwischen Advices.
45
5 Statisches Joinpoint-Modell
1
% context(+Context, +Selector, -Aspect, -Pointcut).
2
3
4
5
6
7
8
pointcut(clause(list([]), _), Context, sample5, init_clause(Sum)):context(Context, parallel, sample4, sum(Sum)).
pointcut(clause(list([H|_]), _), Context, sample5, rec_clause(H, Sum)):context(Context, parallel, sample4, sum(Sum)).
pointcut(goal(list(_)), Context, sample5, rec_goal(Sum)):context(Context, parallel, sample4, sum(Sum)).
Abbildung 5.7: Berücksichtigung des parallelen Kontextes eines Joinpoints.
rekursiven Aufrufen von pointcut/4. Dies führt jedoch zu Problemen bei ungebundenen Variablen. Führt ein Pointcut eine solche Variable ein, muss sichergestellt werden,
dass andere Pointcuts, welche diese Variable ebenfalls verwenden, dieselbe Variable referenzieren. Durch Aufrufe von pointcut/4 werden jedoch immer neue ungebundene
Variablen generiert.
Einen Ausweg bietet der Zugriff auf die Kontextrepräsentation durch das Prädikat
context/4. Als erstes Argument erhält dieses die Termrepräsentation des Kontextes des
aktuellen Joinpoints. Das zweite Argument spezifiziert die Art des Kontextes, in der
nach Pointcuts gesucht werden soll. Durch die Angabe der Konstante parallel wird
der parallele Kontext durchsucht. In den beiden letzten Argumenten liefert das Prädikat
dann sukzessive die Pointcuts, welche im Kontext liegen. Das erste der beiden Argumente
enthält dabei den Aspekt, zu welchem der Pointcut gehört. Das zweite Argument führt
den eigentlichen Pointcut. Der Anhang A.1.1 zeigt eine Implementation des Prädikats
context/4.
Abbildung 5.7 zeigt eine Weiterentwicklung des Aspekts sample3. Die Pointcut-Beschreibungen referenzieren nun zusätzlich die durch den Aspekt sample4 eingeführten
Variablen. Dazu wird mit Hilfe von context/4 der Pointcut sum/1 dieses Aspekts im
parallelen Kontext des Joinpoints gesucht und dessen Argument übernommen.
Übergeordneter Kontext
Der im letzten Beispiel beschriebene Pointcut rec goal/1 erfasst alle Aufrufe des Prädikats list/1. Eine Pointcut-Beschreibung, welche lediglich rekursive Aufrufe berücksichtigt, muss die Klauselbox berücksichtigen, welche die Zielbox umgibt. Pointcuts, welche
Boxen erfassen, die den aktuellen Joinpoint umschließen, bilden den übergeordneten Kontext des Joinpoints. Es gibt zwei Möglichkeiten der Schachtelung von Boxen. Zunächst
liegen die Zielboxen der Prädikataufrufe eines Klauselkörpers innerhalb einer Klauselbox. Des Weiteren erzeugen Aspekte durch zusätzliche Prädikataufrufe neue Zielboxen
innerhalb von Zielboxen, an deren Ports sie platziert wurden.
Das Prädikat context/4 kann auch zur Suche nach Pointcuts im übergeordneten
Kontext eines Joinpoints verwendet werden. Dazu wird diesem neben der Repräsentation des Kontextes ein Term inside/1 als zweites Argument übergeben. Das Argument
dieses Terms beschränkt die Tiefe der Suche. Durch die Angabe von first wird nur
nach Pointcuts gesucht, welche die nächste umschließende Box erfassen, während near
46
5.2 Pointcut-Beschreibungen
1
2
3
4
5
6
7
pointcut(clause(list([]), _), Context, sample6, init_clause(Sum)):context(Context, parallel, sample4, sum(Sum)).
pointcut(clause(list([H|_]), _), Context, sample6, rec_clause(H, Sum)):context(Context, parallel, sample4, sum(Sum)).
pointcut(goal(list(_)), Context, sample6, rec_goal(Sum)):context(Context, parallel, sample4, sum(Sum)),
context(Context, inside(first), sample6, rec_clause(_, _)).
Abbildung 5.8: Berücksichtigung des übergeordneten Kontextes eines Joinpoints.
die Suche stoppt, wenn eine umschließende Box von einem gesuchten Pointcut erfasst
wird. Weitere alternative Lösungen werden dadurch nicht erreicht. Die Angabe von all
ermöglicht eine unbeschränkte Suche.
Abbildung 5.8 zeigt die weitere Entwicklung eines Aspekts zur Summierung von Listenelementen. Die Beschreibung des Pointcuts rec goal/1 überprüft unter Verwendung
von context/4, ob das Ziel innerhalb einer Klausel liegt, welche ihrerseits durch den
Pointcut rec clause/2 erfasst wird.
Nebengeordneter Kontext
Die Beschreibung des Pointcuts rec clause/2 ist weiterhin unvollständig. Er erfasst die
rekursive Klausel und referenziert das aktuelle Listenelement sowie eine neue Variable,
welche die Summe der Listenelemente aufnehmen soll. Um diese durch einen zusätzlichen
Prädikatsaufruf berechnen zu können, muss jedoch das Ergebnis des rekursiven Aufrufs
bekannt sein. Dieses wird durch den Pointcut sum/1 referenziert, welcher den rekursiven
Aufruf erfasst.
Dieser Pointcut liegt im nebengeordneten Kontext der Klausel. Dieser Kontext wird
durch die Zielboxen der Prädikatsaufrufe im Klauselkörper bestimmt. Auch für Ziele
ist ein nebengeordneter Kontext definiert. Alle Zielboxen, welche sich innerhalb derselben Box wie der aktuelle Joinpoint befinden und links von diesem liegen, formen diesen
Kontext. Die Motivation hierfür liegt in der Möglichkeit, in Prädikatsaufrufen auf Daten
bereits abgearbeiteter Aufrufe zurückgreifen zu können. Ein an einer Zielbox platziertes
Advice hat somit Zugang zu allen Daten, die bis zum Erreichen der Zielbox bekannt sind.
Ein an einer Klauselbox platziertes Advice kann sogar sämtliche in der Klausel enthaltenen Daten verwenden, da der gesamte Klauselkörper den nebengeordneten Kontext
bestimmt.
Durch die Übergabe eines Terms beside/1 an context/4 kann der nebengeordnete
Kontext eines Joinpoints durchsucht werden. Die möglichen Argumente für diesen Term
bestimmen wie beim übergeordneten Kontext das Suchverhalten. Die Angabe von first
führt zu einer Suche nach Pointcuts, welche die linke benachbarte Zielbox erfassen. Für
einen Klausel-Joinpoint ist dies die letzte Zielbox im Klauselkörper. Mittels near wird
die Suche von links nach rechts durchgeführt, bis eine Zielbox von einem gesuchten
Pointcut erfasst ist. Alternative Lösungen werden auch hier nicht berücksichtigt. Die
Angabe von all ermöglicht hingegen die Suche nach alternativen Lösungen. Eine neue
Möglichkeit der Suche bietet almost first. Dabei wird nach Pointcuts gesucht, welche
47
5 Statisches Joinpoint-Modell
1
2
3
4
5
pointcut(clause(list([]), _), Context, sample7, init_clause(Sum)):context(Context, parallel, sample4, sum(Sum)).
pointcut(clause(list([H|_]), _), Context, sample7, rec_clause(H, LSum, Sum)):context(Context, parallel, sample4, sum(Sum)),
context(Context, beside(near), sample7, sum(LSum)).
Abbildung 5.9: Berücksichtigung des nebengeordneten Kontextes eines Joinpoints.
die nächstgelegene Zielbox erfassen, welche weder den Cut, noch einen Aufruf von true/0
repräsentiert.
Mit Hilfe des nebengeordneten Kontextes kann nun die Beschreibung des Pointcuts
rec clause/3 vervollständigt werden. Abbildung 5.9 enthält die endgültigen PointcutBeschreibungen für den Aspekt sample7 zur Summierung von Listenelementen.
5.2.3 Fixpunkt-Semantik
Die Berücksichtigung der Kontexte von Joinpoints führt zu zyklischen Abhängigkeiten
zwischen diesen Kontexten. Um für einen Klausel-Joinpoint entscheiden zu können, ob
dieser von einem Pointcut erfasst wird, muss der nebengeordnete Kontext des Joinpoints
einbezogen werden. Dieser wird durch Ziel-Joinpoints des Klauselkörpers bestimmt. Bei
der Ermittlung der Pointcuts, welche diese Ziel-Joinpoints erfassen, muss der übergeordnete Kontext berücksichtigt werden. Diesen Kontext bilden aber gerade Pointcuts,
welche den umgebenen Klausel-Joinpoint erfassen.
Algorithmus
Durch einen Fixpunkt-Algorithmus können trotz dieser zyklischen Abhängigkeit die
Pointcuts, welche einen Joinpoint erfassen, bestimmt werden. Der Algorithmus ermittelt Pointcuts für einen Klausel-Joinpoint und in dessen Klauselkörper enthaltene ZielJoinpoints. Zunächst werden der parallele, über- und nebengeordnete Kontext als leer
betrachtet. Mit diesen Kontextinformationen werden dann alle Pointcuts ermittelt, welche den Klausel-Joinpoint erfassen. Diese werden als neuer paralleler Kontext übernommen. Dann werden weitere Pointcuts gesucht, welche den Klausel-Joinpoint enthalten.
Diese gehen erneut in den parallelen Kontext ein. Dies wird solange wiederholt, bis sich
der parallele Kontext nicht mehr ändert.
Dieser wird dann als übergeordneter Kontext für die Ziel-Joinpoints verwendet. Die
Ziel-Joinpoints werden von links nach rechts bearbeitet. Dabei wird für jeden dieser
Joinpoints geprüft, welche Pointcuts diesen erfassen. Erneut bilden diese im nächsten
Schritt den parallelen Kontext für diesen Joinpoint. Jeder Joinpoint wird solange bearbeitet, bis sich dieser parallele Kontext nicht mehr ändert. Anschließend wird der nächste
Joinpoint bearbeitet, wobei der nebengeordnete Kontext um den parallelen Kontext des
abgearbeiteten Joinpoints ergänzt wird. Sind alle Ziel-Joinpoints abgearbeitet, erhält
man einen neuen nebengeordneten Kontext für den Klausel-Joinpoint. Mit diesem wird
ein erneuter Lauf des gesamten Algorithmus gestartet. Der Algorithmus bricht ab, wenn
48
5.3 Advices
1
% port_advice(+Aspect, +Pointcut, +Port):- Advice.
2
3
4
port_advice(sample7, init_clause(Sum), exitbody):- Sum is 0.
port_advice(sample7, rec_clause(El, TSum, Sum), exitbody):- Sum is El + TSum.
Abbildung 5.10: Port-Advices zur Summierung von Listenelementen.
der nebengeordnete Kontext des Klausel-Joinpoints und damit die Pointcuts, welche ZielJoinpoints erfassen, unverändert bleiben. Eine Implementation des Algorithmus wird in
Anhang A.1.2 präsentiert.
Terminierung
Die Terminierung des Algorithmus kann unter bestimmten Bedingungen garantiert werden. Eine entscheidende Voraussetzung ist die Terminierung der einzelnen PointcutBeschreibungen. Eine weitere Bedingung ist die Vermeidung zyklischer Abhängigkeiten von Pointcut-Beschreibungen. Entstehen solche Abhängigkeiten lediglich durch unnegierte Aufrufe von context/4, bereiten sie keine Probleme, da in jedem Schritt nur
endlich viele Pointcuts hinzukommen. Wird der Zyklus geschlossen, ändert sich die Berechnung nicht mehr, da der Algorithmus nur bisher unberücksichtigte Pointcuts in neue
Kontexte aufnimmt. Diese Situation ändert sich durch negierte Aufrufe von context/4.
Dadurch werden innerhalb des Zyklus Pointcuts möglicherweise wieder entfernt, da sie
durch den neuen Kontext nicht mehr gültig sind. Werden solche Pointcuts an anderer
Stelle im Zyklus erneut eingeführt, verhindert dies die Terminierung des Algorithmus.
Daher sind negierte Kontextbedingungen vorsichtig zu verwenden.
5.3 Advices
5.3.1 Port-Advices
Zusätzliche Prädikatsaufrufe sind ein wesentlicher Bestandteil logischer Aspekte. Im Beispiel der Summierung von Listenelementen wird durch zusätzliche Aufrufe des Prädikats
is/2 die Summierung der Elemente beschrieben.
Platzierung
Mit Hilfe von Port-Advices lassen sich durch einen Aspekt solche zusätzlichen Prädikatsaufrufe an Ports von Zielen oder Klauseln, welche von einem Pointcut erfasst werden,
platzieren. Hierfür werden Klauseln des Prädikats port advice/3 definiert. Die ersten
beiden Argumente beschreiben den betroffenen Aspekt sowie den Pointcut, dessen Joinpoints durch die Anbringung des Advice betroffen sind. Das letzte Argument enthält
den Port, an welchem das Advice platziert wird. Das eigentliche Advice beschreibt der
Klauselkörper. Dabei können generische Argumente eines Aspekts und Argumente aus
dem Anwendungskontext eines betroffenen Joinpoints genutzt werden.
49
5 Statisches Joinpoint-Modell
Goal
call
Goal
exit
; Advice, Goal
; Goal , Advice
Abbildung 5.11: Transformationen von Zielen zur Platzierung von Advices an vorwärtsgerichteten Ports.
Abbildung 5.10 zeigt die Platzierung von Port-Advices am laufenden Beispiel der
Summierung von Listenelementen. Das erste Advice betrifft Initialisierungsklauseln und
beschreibt die Initialisierung der Summe. Das zweite Advice bezieht sich auf rekursive
Klauseln und enthält einen Prädikatsaufruf zur Berechnung der Summe. Beide Advices
werden am Exitbody-Port von Klauseln, welche durch den jeweiligen Pointcut erfasst
werden, platziert.
Vorwärtsgerichtete Ports
Die Platzierung zusätzlicher Prädikatsaufrufe an vorwärtsgerichteten Ports steht in engem Zusammenhang mit Advices aspektorientierter Ansätze für prozedurale oder objektorientierte Programmiersprachen. Dieser Zusammenhang ergibt sich aus einer prozeduralen Sicht auf die Ausführung von Prolog-Programmen, welche sich auch im erweiterten
Portmodell wiederfindet.
Die Abarbeitung eines Ziels korrespondiert in dieser Sicht mit dem Aufruf einer Prozedur oder Methode [87]. Daraus ergibt sich ein Zusammenhang zwischen den ZielJoinpoints des vorgestellten statischen Joinpoint-Modells für Prolog-Programme und
Methodenaufruf-Joinpoints in AspectJ. Vor der Abarbeitung des Ziels wird der Call Port der Zielbox passiert. Ein hier platziertes Advice entspricht somit einem BeforeAdvice an einem Methodenaufruf-Joinpoint in AspectJ. Nach erfolgreicher Bearbeitung
des Ziels wird der Exit-Port der Zielbox passiert. Advices, welche an diesem Port platziert werden, bilden damit ein Pendant zu After -Advices an Methodenaufruf-Joinpoints
in AspectJ.
Abbildung 5.11 enthält Transformationsregeln zur Platzierung von Advices an vorwärtsgerichteten Ports eines Ziels. Entsprechend der vorhergehenden Überlegungen wird
ein am Call -Port platziertes Advice vor dem eigentlichen Ziel eingewoben. Ein Advice,
welches am Exit-Port angebracht werden soll, wird entsprechend hinter dem betroffenen
Ziel angefügt.
Analog zu den Betrachtungen zu Ziel-Joinpoints bilden Klausel-Joinpoints ein Pendant zu Methodenausführung-Joinpoints in dem AspectJ zugrundeliegenden JoinpointModell. An Enterbody- bzw. Exitbody-Ports platzierte Advices entsprechen auch hier
Before- respektive After -Advices. Zusätzlich können Advices am Trymatch-Port einer
Klausel angebracht werden. Diese sind ohne Entsprechung in AspectJ, da sie sich auf
die Unifikation eines Klauselkopfes mit dem aktuellen Ziel beziehen.
Die in Abbildung 5.12 gezeigten Transformationen spezifizieren den Webevorgang für
an vorwärtsgerichteten Klauselports platzierte Advices. Advices an Enterbody-bzw. Exit-
50
5.3 Advices
p(X1 , . . . , Xn ) ← Body.
trymatch
p(Y1 , . . . , Yn ) ← Advice,
X1 = Y1 , . . . , Xn = Yn →
Body.
p(X1 , . . . , Xn ) ← Body.
enterbody
;
p(X1 , . . . , Xn ) ← Advice, Body.
p(X1 , . . . , Xn ) ← Body.
exitbody
p(X1 , . . . , Xn ) ← Body, Advice.
;
;
Abbildung 5.12: Transformationen von Klauseln zur Platzierung von Advices an
vorwärtsgerichteten Ports.
body-Ports werden entsprechend des Zusammenhangs zu Before- und After -Advices in
AspectJ vor bzw hinter dem Klauselkörper eingewoben. Das Einfügen von Advices an
Trymatch-Ports gestaltet sich etwas komplizierter. Die Ursache hierfür liegt darin, dass
die Unifikation eines aktuellen Ziels mit einem Klauselkopf implizit vollzogen wird. Um
vor der Unifikation ein Advice zur Ausführung bringen zu können, wird im Klauselkopf
für jedes Argument eine neue Variable eingeführt und die Unifikation explizit durch Aufrufe des Prädikats =/2 formuliert. Vor diesen Aufrufen wird dann das Advice platziert.
Der ursprüngliche Klauselkörper wird dabei von der expliziten Unifkation mittels ->/2
getrennt. Dadurch wird eine einmal erfolgreiche Unifikation nicht durch Backtracking
neu angestoßen. Schlägt der Klauselkörper fehl, wird stattdessen versucht, eine andere
Klausel zur Abarbeitung des aktuellen Ziels zu verwenden.
Ausnahmebehandlung
Der Prolog-Standard [20, 36] sieht im Rahmen eines Konzepts zur Behandlung von
Ausnahmen einen Error -Port für Ziele vor. Wird bei der Abarbeitung eines Ziels eine
Ausnahme mittels throw/1 ausgelöst, wird die Zielbox durch diesen Port verlassen und in
der umgebenden Zielbox ebenfalls eine Ausnahme ausgelöst. Eine solche Ausnahme kann
mit catch/3 abgefangen werden. Dem Prädikat werden als Argumente das eigentliche
Ziel, ein Muster für die abzufangende Ausnahme und ein Recover-Ziel übergeben. Tritt
keine Ausnahme bei der Abarbeitung eines Ziels auf, verhält sich catch/3 wie call/1.
Im Falle einer Ausnahme, welche mit dem Ausnahmemuster eines catch/3-Aufrufes
unifizierbar ist, werden sämtliche durch die Ausführung des Ziels erzeugte Choicepoints
entfernt und ein Backtracking-Prozess bis zum Punkt vor dessen Ausführung angestoßen,
wobei der Term, welche die Ausnahme beschreibt, erhalten wird. Anschließend wird das
Recover-Ziel aufgerufen.
Die Behandlung von Ausnahmen stellt ein typisches Crosscutting Concern dar. So
definiert das dynamische Joinpoint-Modell von AspectJ die Behandlung einer Ausnahme
als Joinpoint. Auch im vorliegenden statischen Joinpoint-Modell für Prolog-Programme
lässt sich die Behandlung von Ausnahmen mittels an Error -Ports von Zielen platzierten
Advices in separaten Aspekten beschreiben. Zur Platzierung der Advices werden wie bei
51
5 Statisches Joinpoint-Modell
error(Error )
;
Goal
catch(Goal , Error , Advice)
Abbildung 5.13: Transformationen von Zielen zur Platzierung von Advices an Error Ports.
Advices an vorwärtsgerichteten Ports Klauseln des Prädikats port advice/3 genutzt.
Das Muster der abzufangenden Ausnahme wird dabei als Argument des Ports error/1
angegeben. Beim Weben der Aspekte werden solche Advices dann in catch/3-Aufrufe
transformiert. Abbildung 5.13 zeigt die entsprechende Transformationsregel.
Rückwärtsgerichtete Ports
Während des Backtracking-Prozesses werden rückwärtsgerichtete Ports von Ziel- und
Klauselboxen passiert, an denen ebenfalls Advices mittels port advice/3-Klauseln platziert werden können. Da das Konzept des Backtrackings sowohl prozeduralen als auch
objektorientierten Programmiersprachen fremd ist, definieren aspektorientierte Ansätze
für diese Sprachen keine vergleichbaren Advice-Arten.
Die in Abschnitt 4.4 vorgestellten Port-Annotationen gestatten ebenfalls zusätzliche
Prädikataufrufe an rückwärtsgerichteten Ports. Ein zentrales Konzept dieser Annotationen ist die Erhaltung der Semantik annotierter Ziele. Daher werden durch die annotierten
Prädikataufrufe erzeugte Choicepoints ignoriert und der Backtracking-Prozess in jedem
Fall fortgesetzt. Damit sind an rückwärtsgerichteten Ports nur Aufrufe von Prädikaten
mit Seiteneffekten sinnvoll. Eine typische Anwendung ist die Ausgabe von DebuggingInformationen.
Für einen aspektorientierten Ansatz ist eine solche Semantik-Erhaltung zu restriktiv.
Aspekte beeinflussen die Semantik von Komponenten und anderen Aspekten. Durch
an rückwärtsgerichteten Ports platzierten Advices lässt sich der Backtracking-Vorgang
beeinflussen. So gestattet die erfolgreiche Abarbeitung eines Advice am Fail -Port eines
Ziels den Abbruch des Backtrackings und einen erneuten Versuch der Abarbeitung des
Ziels. Auf dieselbe Art verhindert eine erfolgreiche Abarbeitung eines Advices am RedoPort eines Ziels die Suche nach alternativen Lösungen für dieses Ziel. Stattdessen setzt
die vorwärtsgerichtete Abarbeitung folgender Zielboxen erneut ein.
Dieses Verhalten wird durch die in Abbildung 5.14 gezeigten Transformationsregeln
spezifiziert. Advices an rückwärtsgerichteten Ports werden ähnlich zu Advices an vor-
Goal
;
fail
(true; Advice), Goal
Goal
redo
Goal , (true; Advice)
;
Abbildung 5.14: Transformationen von Zielen zur Platzierung von Advices an rückwärtsgerichteten Ports.
52
5.3 Advices
p(X1 , . . . , Xn ) ← Body.
failmatch
p(Y1 , . . . , Yn ) ← (true; Advice),
X1 = Y1 , . . . , Xn = Yn →
Body.
p(X1 , . . . , Xn ) ← Body.
failbody
;
p(X1 , . . . , Xn ) ← (true; Advice), Body.
p(X1 , . . . , Xn ) ← Body.
redobody
p(X1 , . . . , Xn ) ← Body, (true; Advice).
;
;
Abbildung 5.15: Transformationen von Klauseln zur Platzierung von Advices an
rückwärtsgerichteten Ports.
wärtsgerichteten Ports auf derselben Seite der Zielbox eingewoben. So ähneln die Transformationsregeln für Fail - und Redo-Ports denen für Call - und Exit-Ports. Im Unterschied zu Advices an vorwärtsgerichteten Ports wird allerdings eine Disjunktion des stets
erfolgreichen Ziels true/0 und des eigentlichen Advices eingewoben. Dies stellt sicher,
dass die vorwärtsgerichtete Abarbeitung durch das Advice nicht beeinflusst wird. Wird
die Disjunktion jedoch durch einen Backtracking-Prozess erreicht, führt die Suche nach
einer alternativen Lösung zur Abarbeitung des Advices.
Auch die Transformationsregeln für rückwärtsgerichtete Ports von Klauselboxen lassen sich so aus denen für vorwärtsgerichtete Ports ableiten. Das Ergebnis zeigt Abbildung 5.15. Das Vorgehen für Failbody- und Redobody-Ports entspricht dabei dem für
Fail - und Redo-Ports von Zielboxen. Die entsprechenden Transformationsregeln entstehen durch die Einführung der bereits erläuterten Disjunktion in die Transformationsregeln für Enterbody- und Exitbody-Ports. Für Advices am Failmatch-Port muss analog
zum vorwärtsgerichteten Trymatch-Port die Unifikation des Klauselkopfs explizit aufgeführt werden. Auch hier entsteht die entsprechende Transformationsregel aus der des
vorwärtsgerichteten Ports durch die Einführung einer Disjunktion aus true/0 und dem
einzuwebenden Advice.
5.3.2 Around-Advices
Neben Before- und After -Advices bietet AspectJ in Form von Around -Advices eine weitere Art von Advices. Ein Around -Advice umgibt dabei einen Joinpoint. Auch für logische Aspekte können Around -Advices sinnvoll sein. Daher sieht das statische JoinpointModell diese als spezielle Port-Advices vor. Around -Advices werden wie gewöhnliche
Port-Advices durch port advice/3-Klauseln platziert. Die Angabe eines Ports wird dabei durch die Konstante around ersetzt. Beim Einweben von Around -Advices werden
Ziele durch das entsprechende Advice ersetzt. Around -Advices für Klauseln ersetzen den
Klauselkörper. Abbildung 5.16 enthält die entsprechenden Transformationsregeln.
In AspectJ geht ein Around -Advice über eine reine Ersetzung des Joinpoints hinaus,
da der ursprüngliche Joinpoint durch eine Proceed -Anweisung zur Ausführung gebracht
werden kann. Eine solche Anweisung lässt sich nicht direkt auf das hier vorgestellte
53
5 Statisches Joinpoint-Modell
Goal
around
Advice
p(X1 , . . . , Xn ) ← Body.
around
p(X1 , . . . , Xn ) ← Advice.
;
;
Abbildung 5.16: Transformationen von Zielen und Klauseln zur Platzierung von Around Advices.
Modell übertragen. Die Ursache hierfür liegt in der freien Beschreibung von Pointcuts.
Während Pointcut-Beschreibungen in AspectJ aus vorgegebenen elementaren Beschreibungselementen zusammengesetzt werden, können im vorliegenden Modell Pointcuts
durch beliebige Ziele beschrieben werden. Dies verhindert eine Veränderung der Parameter eines Joinpoints, welcher durch einen Pointcut erfasst wird.
Dennoch lässt sich ein ähnlicher Effekt unter Ausnutzung der Meta-Variable Facility
erzielen. Dazu wird in der Pointcut-Beschreibung das auszuführende Prädikat konstruiert
und als Argument des Pointcuts geführt. Ein Around -Advice kann auf dieses Argument
zurückgreifen. Die Meta-Variable Facility von Prolog ermöglicht es dabei, eine Variable
an Stelle eines Prädikataufrufes zu verwenden [8]. Genaugenommen ist diese Verfahrensweise mächtiger als die Proceed -Anweisung in AspectJ, da sie in allen Advices zur
Anwendung kommen kann.
Ihr Nachteil liegt in der Konstruktion von Advice-Bestandteilen innerhalb von PointcutBeschreibungen. Die Trennung zwischen Pointcut-Beschreibungen als Orte, an denen
Aspekte Komponenten und andere Aspekte beeinflussen, und Advices als Teile der Implementation von Aspekten wird dadurch verletzt. Ein weiterer Nachteil liegt in einem
Verlust der Generizität, da die Anzahl der Argumente des konstruierten Advices bekannt
sein muss. Dies ist jedoch nicht immer der Fall, insbesondere wenn mehre Aspekte die
Arität eines Ziels beeinflussen.
5.3.3 Term-Advices
Neben der Platzierung zusätzlicher Prädikatsaufrufe beeinflussen typische logische Aspekte auch die Struktur bestehender Prädikate. So muss im laufenden Beispiel der Summierung von Listenelementen das Prädikat list/1 um ein Argument erweitert werden,
welches die Summe der Listenelemente aufnimmt. Eine weitere Auswirkung des Aspekts
sollte die Umbenennung des Prädikats sein, welche der neuen Semantik Rechnung trägt.
Advice-Arten
Um solche und ähnliche Auswirkungen eines Aspekts beschreiben zu können, stehen
verschiedene Term-Advices zur Verfügung. Grundlage der Advices ist die äquivalente
Behandlung von Termen und Prädikaten in Prolog. Es stehen vier verschiedene Arten von Advices zur Verfügung. Ein rename/1-Advice ändert das Prädikatsymbol eines
Prädikats. Das Argument des Advices enthält das neue Symbol.
54
5.3 Advices
1
% term_advice(+Aspect, +Pointcut, +Advice)
2
3
4
term_advice(sample4, sum(Sum), extra_arg(Sum, last)).
term_advice(sample4, sum(_), rename(sumlist)).
Abbildung 5.17: Term-Advices zur Umbenennung und Erweiterung um ein zusätzliches
Argument.
Zur Erweiterung eines Prädikats um zusätzliche Argumente stehen extra args/2Advices zur Verfügung. Das erste Argument solcher Advices enthält eine Liste von
zusätzlichen Argumenten. Das zweite Argument legt die Position fest, hinter welchem
bereits existenten Argument diese im Prädikat eingefügt werden sollen. Als besondere
Positionen sind first für eine Platzierung als erstes Argument und last für eine Platzierung als letztes Argument definiert. Letzteres ist besonders dann nützlich, wenn die
Zahl der Argumente der durch einen Pointcut erfassten Prädikate nicht bekannt ist oder
variiert. Soll ein Prädikat lediglich um ein einzelnes Argument erweitert werden, bilden
extra arg/2-Advices einen Spezialfall der extra args/2-Advices.
Die dritte Art von Term-Advices sind die select/1-Advices. Sie dienen der Auswahl
bestimmter Argumente eines Prädikats, können aber auch zur Veränderung der Reihenfolge von Argumenten genutzt werden. Eine weitere Anwendungsmöglichkeit ist die
mehrfache Verwendung eines Arguments. Dem Advice wird als Argument eine Liste von
Argumentpositionen übergeben. Das Ergebnis der Anwendung ist dann ein Prädikat
mit dem ursprünglichen Prädikatsymbol. Die Argumente des Prädikats bilden dabei die
Argumente, welche anhand der Positionsangaben aus dieser Liste dem ursprünglichen
Prädikat entnommen werden. Durch entsprechende Positionsangaben ist es möglich, bestimmte Argumente auszuwählen, Argumente zu permutieren oder mehrfach zu verwenden.
Die Verwendung von unify/2-Advices erlaubt statische Unifikationen zur Webezeit
eines Aspekts. Als Argumente enthält ein solches Advice die statisch zu unifizierenden
Terme. Schlägt die Unifikation fehl, wird eine Warnung ausgegeben und der Webevorgang
abgebrochen. Der Vorteil von unify/2-Advices gegenüber Port-Advices kann in einer
besseren Lesbarkeit von Aspekten und verwobenen Programm liegen. Betrachtet man
im laufenden Beispiel die Berechnung der Summe in der Initialisierungsklausel, fällt
auf, dass die Initialisierung dynamisch durch einen Aufruf des Prädikats is/2 erfolgt,
obwohl die Auswirkungen dieses Aufrufes statisch bekannt sind. Derselbe Effekt kann
durch eine statische Unifikation der entsprechenden Variable mit dem konstanten Wert
erzielt werden.
Platzierung
Term-Advices werden an Pointcuts von Aspekten platziert. Die Platzierung wird durch
Klauseln des Prädikats term advice/3 definiert. Der Klauselkopf enthält in den ersten
beiden Argumenten den Aspekt und den Pointcut, an welchem ein Advice platziert werden soll. Das Term-Advice selbst wird im letzten Argument geführt. Der Klauselkörper
55
5 Statisches Joinpoint-Modell
p(X1 , . . . , Xn )
p(X1 , . . . , Xn )
p(X1 , . . . , Xn )
rename(q)
;
extra args([Y1 ,...,Ym ],i)
;
select([i1 ,...,im ])
;
q(X1 , . . . , Xn )
p(X1 , . . . , Xi , Y1 , . . . , Ym , Xi+1 , . . . , Xn )
p(Xi1 , . . . , Xim )
Abbildung 5.18: Transformationen von Zielen und Klauselköpfen zur Umsetzung von
Term-Advices.
kann zur Formulierung zusätzlicher Bedingungen genutzt werden.
Mit Hilfe von Term-Advices lässt sich das Beispiel des Aspekts zur Summierung von
Listenelementen vervollständigen. Abbildung 5.17 enthält die dafür notwendigen Advices. Zunächst werden Prädikate, welche an der Summierung beteiligt sind, um ein
Argument erweitert, welches die Summe der Elemente aufnimmt. Dies betrifft sowohl
Ziele als auch Klauselköpfe. Anschließend werden diese Prädikate umbenannt. Durch
die Anwendung des so vervollständigten Aspekts auf das Programm list/1 entsteht
das Programm sumlist/2 aus Abbildung 5.1.
Umsetzung
Der Webevorgang lässt sich wie bei Port-Advices mittels Transformationsregeln beschreiben. Abbildung 5.18 zeigt die notwendigen Regeln, welche die Anwendung der verschiedenen Term-Advices auf einen Joinpoint beschreiben. Ein auf einen Ziel-Joinpoint angewandtes Term-Advice betrifft den Prädikatsaufruf als Term. Wird ein Term-Advice
an einem Klausel-Joinpoint angewandt, wirkt sich dieses auf den Klauselkopf aus. Eine
Ausnahme bilden unify-Advices, da sich statische Unifikationen - auch auf Ziele bezogene – auf die ganze Klausel auswirken. Der Anhang A.4.6 enthält eine Implementation
der jeweiligen Transformationen.
5.3.4 Reihenfolge von Advices
Die bisher präsentierten Transformationsregeln spezifizieren den Webevorgang für einzelne Advices. Besteht ein Aspekt aus mehreren Advices oder sollen mehrere Aspekte
eingewoben werden, stellt sich die Frage nach der Reihenfolge der einzelnen Transformationsschritte. Diese beeinflusst maßgeblich die Reihenfolge der Advices im Ergebnis
des Webevorgangs und somit die Semantik des verwobenen Programms.
Reihenfolge von Term-Advices
Ein typisches Reihenfolge-Problem ergibt sich bei mehreren Term-Advices, welche am
selben Joinpoints angewendet werden sollen. Die Reihenfolge der Advices wird dabei
zunächst durch die Reihenfolge der einzuwebenden Aspekte bestimmt. Dabei werden die
56
5.3 Advices
Advices früher angeführter Aspekte vor den Advices später angeführter Aspekte eingewoben. Für mehrere Advices eines Aspekts entscheidet die Reihenfolge der term advice/3Klauseln, welche die Advices platzieren. Die in Anhang A.2.2 gezeigte Implementation
berücksichtigt die hier dargestellte Reihenfolge.
Reihenfolge an gleichen Ports
Reihenfolge-Konflikte zwischen Port-Advices, welche an einem gemeinsamen Port eines Joinpoints platziert werden sollen, lassen sich ähnlich lösen. Auch hier entscheidet
zunächst die Reihenfolge der einzuwebenden Aspekte. Port-Advices früher angeführter
Aspekte werden vor den Advices später angeführter Aspekte platziert. Bei mehreren
Advices desselben Aspekts wird die Reihenfolge der port advice/3-Klauseln berücksichtigt. Eine entsprechende Implementation zeigt Anhang A.2.1.
Von dieser Regelung sind Around -Advices, welche als spezielle Port-Advices betrachtet
wurden, nicht betroffen. Mehrere Around -Advices für denselben Joinpoint werden als
Fehlersituation betrachtet.
Reihenfolge an verschiedenen Ports
Ein weiteres Reihenfolge-Problem entsteht durch Port-Advices, welche an vor- und rückwärtsgerichteten Ports auf derselben Seite einer Ziel- oder Klauselbox platziert werden
sollen. So platzieren beispielsweise die Transformationsregeln für Call - und Fail -Ports die
Advices an derselben relativen Position zum ursprünglichen Ziel. Das gleiche Phänomen
tritt bei allen korrespondierenden Paaren von vor- und rückwärtsgerichteten Ports auf,
da sich die Transformationsregeln für rückwärtsgerichtete Ports gerade aus denen der
vorwärtsgerichteten Pendants ergeben hatten.
Werden die Advices des Fail -Ports vor den Advices des Call -Ports platziert, kommen diese erst zur Ausführung, wenn der Backtracking-Prozess neben dem betroffenen
Ziel auch die Call -Advices passiert hat. Dieses Verhalten ist nicht intuitiv, da die Fail Advices bei der Passage des Fail -Ports ausgeführt werden sollen. Bei umgekehrter Reihenfolge tritt allerdings ein anderes Problem auf. Nach der Ausführung der Fail -Advices
wird das betroffene Ziel erneut ausgeführt. Durch die Platzierung der Call - vor den Fail Advices werden erstgenannte bei der erneuten Ausführung des Ziels nicht berücksichtigt.
Auch diese Problematik findet sich bei anderen Paaren vor- und rückwärtsgerichteter
Ports. Sie lässt sich nur durch eine dynamische Lösung auflösen. Für eine statische
Lösung ist die zweite Variante zu bevorzugen, da sie intuitiver ist. Wird ein Ziel über
den Call -Port betreten, kommen dort platzierte Advices zur Ausführung. Versucht der
Backtracking-Prozess, das Ziel über den Fail -Port zu verlassen, werden zunächst die dort
angebrachten Advices versucht auszuführen. Gelingt dies, wird das Ziel ohne Passage
des Call -Ports erneut ausgeführt. Schlagen die Advices am Fail -Port hingegen fehl, wird
das Ziel tatsächlich verlassen. Wird es im weiteren Verlauf erneut betreten, wird der
Call -Port ebenfalls wieder passiert und die entsprechenden Advices gelangen erneut zur
Ausführung. Mit derselben Argumentation lassen sich die Konflikte an weiteren PortPaaren lösen, so dass die Advices rückwärtsgerichteter Ports stets hinter den Advices
57
5 Statisches Joinpoint-Modell
vorwärtsgerichteter Ports platziert werden. Im Anhang A.4 wird eine solche Umsetzung
des Webevorgangs dargestellt.
Ein weiterer Reihenfolge-Konflikt betrifft Error - und Around -Advices für Ziel-Joinpoints. In diesem Fall wird zuerst das Ziel durch das Around -Advice ersetzt. Anschließend werden die Transformationen für die Error -Advices vollzogen.
5.4 Aspekte
In den vorhergegangenen Abschnitten wurden Pointcut-Beschreibungen und Advices
stets in Verbindung mit einem Aspekt in Verbindung gebracht. Dabei wurde offengelassen, was ein solcher Aspekt im vorliegenden Modell ist und wie dieser in eine Komponente
eingewoben werden kann. Dies soll in diesem Abschnitt nachgeholt werden.
5.4.1 Einfache Aspekte
Aspekte stellen im aktuell betrachteten Modell zunächst einen Gruppierungsmechanismus für Pointcuts dar. In der Beschreibung eines Pointcuts durch pointcut/4 wird als
drittes Argument der Aspekt angegeben, zu welchem der im vierten Argument geführte
Pointcut gehört. Ein Advice, welches an durch einen Pointcut erfassten Joinpoints zur
Anwendung gelangen soll, spezifiziert neben dem Pointcut auch den Aspekt, zu welchem
dieser gehört.
Diese Gruppierung kann beim Webevorgang ausgenutzt werden, indem dem Weber
eine Liste einzuwebender Aspekte übergeben wird. Dieser berücksichtigt dann lediglich
Beschreibungen von Pointcuts, welche zu diesen Aspekten gehören. Somit lassen sich
Aspekte zur Zusammenfassung von Pointcut-Beschreibungen und Advices verwenden.
Die entsprechenden Deklarationsklauseln können in einer oder mehreren Dateien separat zusammengefasst werden, was eine separate Beschreibung von Aspekten und deren
Wiederverwendung ermöglicht. Ein Aspekt fasst aus dieser Sicht also die Implementation
eines Crosscutting Concerns zusammen.
Das Prädikat weave/3 realisiert den Webevorgang. Es erwartet im ersten Argument eine Liste von Aspekten und im zweiten Argument ein Prolog-Programm, in welches diese
eingewoben werden sollen. Im dritten Argument liefert das Prädikat dann Ergebnis des
Webevorgangs. Ein- und Ausgabe-Programm werden dabei als Listen von Klauseln, Fakten und Anfragen repräsentiert. Als zweites Prädikat steht dem Anwender weave file/3
zur Verfügung. Dieses baut auf weave/3 auf, liest das Eingabe-Programm aber aus der
im zweiten Argument angegebenen Datei und schreibt die Ausgabe in die im dritten
Argument bezeichnete Datei. Dabei berücksichtigt es die Variablennamen, welche im
Eingabeprogramm und in den Advices der einzuwebenden Aspekte verwendet werden.
Dies führt zu einer erhöhten Lesbarkeit des Ausgabeprogramms. Eine vollständige Implementation der Prädikate enthält der Anhang A.4.
58
5.4 Aspekte
1
2
3
pointcut(Joinpoint, Context, rename(Name/Arity, NewName), rename):( Joinpoint = goal(Goal) ; Joinpoint = clause(Goal, _) ),
nonvar(Goal), functor(Goal, Name, Arity).
4
5
term_advice(rename(Name/Arity, NewName), rename, rename(NewName)).
Abbildung 5.19: Generischer Aspekt rename/2 zur Umbenennung von Prädikaten.
5.4.2 Abhängigkeiten zwischen Aspekten
Häufig kommt es vor, dass die Verwendung eines Aspekts auf der eines anderen basiert.
Mit Hilfe von Klauseln für das Prädikat require aspects/2 lassen sich solche Abhängigkeiten spezifizieren. Der im ersten Argument enthaltene Aspekt hängt dabei von allen im
zweiten Argument enthaltenen Aspekten ab, für die ein Aufruf von require aspects/2
erfolgreich ist.
Beim Webevorgang werden Aspektabhängigkeiten berücksichtigt. Aspekte, von denen
die einzuwebenden Aspekte abhängen, werden ebenfalls eingewoben. Grundlage hierfür
bildet eine Mengensemantik, d.h. durch Abhängigkeiten mehrfach einzuwebende Aspekte
werden lediglich einfach berücksichtigt. Anhang A.3 enthält eine entsprechende Umsetzung. Dadurch sind auch zyklische Abhängigkeiten realisierbar. Solche treten beispielsweise auf, wenn die Verwendung einer Gruppe von Aspekten nur gemeinsam sinnvoll ist,
die Beschreibung aber dennoch separat erfolgen soll.
Die Berücksichtigung abhängiger Aspekte führt zu einem Unterschied zwischen einem
Aspekt als Gruppierungsmechanismus für Pointcuts und einem Aspekt als Implementation eines Crosscutting Concerns. Ein Aspekt A kann für einen Aspekt B, von dem er
abhängt, zusätzliche Pointcut-Beschreibungen und Advices enthalten. Durch die automatische Berücksichtigung des Aspekts B werden auch die zusätzlichen Deklarationen
berücksichtigt. Aus Sicht der Gruppierung gehören diese zum Aspekt B. Andererseits
dienen sie aber der Implementation des Aspekts A. Generell ist diese Vorgehensweise
behutsam einzusetzen. So sollte gewährleistet sein, dass die zusätzlichen Deklarationen
bei alleiniger Anwendung des Aspekts B keinen Einfluss haben und nur bei gleichzeitiger
Anwendung des Aspekts A zum Tragen kommen.
5.4.3 Generische Aspekte
In den Beispielen dieses Kapitels wurden bisher nur Konstanten als Aspekt-Bezeichner
verwendet. Durch die Verwendung von Termen lassen sich generische Aspekte beschreiben. Ein solcher Aspekt enthält dann Argumente, welche in Pointcut-Beschreibungen
und Advices generisch genutzt werden können. Abbildung 5.19 zeigt dies am Beispiel
des Aspekts rename/2, welcher der Umbenennung von Prädikaten dient. Das erste Argument des Aspekts enthält dabei das Symbol und die Arität des umzubenennenden
Prädikats. Das zweite Argument enthält den neuen Namen.
Die generischen Argumente eines Aspekts müssen zur Zeit des Webevorgangs instanziiert sein. Dies kann durch Angabe konkreter Werte beim Aufruf des Webers oder durch
Aspekte, welche von generischen Aspekten abhängen, geschehen. Abbildung 5.20 zeigt
59
5 Statisches Joinpoint-Modell
1
require_aspects(sumlist, rename(list/1, sumlist)).
2
3
4
5
6
7
8
9
pointcut(clause(list(L), Body), Context, sumlist, sum(Sum)).
pointcut(goal(list(L)), Context, sumlist, sum(Sum)).
pointcut(clause(list([]), _), Context, sumlist, init_clause(Sum)):context(Context, parallel, sumlist, sum(Sum)).
pointcut(clause(list([H|_]), _), Context, sumlist, rec_clause(H, LSum, Sum)):context(Context, parallel, sumlist, sum(Sum)),
context(Context, beside(near), sumlist, sum(LSum)).
10
11
12
13
term_advice(sumlist, sum(Sum), extra_arg(Sum, last)).
term_advice(sumlist, init_clause(Sum), unify(Sum, 0)).
port_advice(sumlist, rec_clause(El, LSum, Sum), exitbody):- Sum is El + LSum.
Abbildung 5.20: Vollständiger Aspekt sumlist zur Summierung von Listenelementen.
den endgültigen Aspekt sumlist/0 zur Summierung von Listenelementen, welcher auf
dem generischen Aspekt rename/2 aus Abbildung 5.19 und den Beispielen der vorherigen
Abschnitte aufbaut.
Bei generischen Aspekten ist die im letzten Abschnitt erwähnte Einführung zusätzlicher Pointcut-Beschreibungen und Advices durch abhängige Aspekte besonders nützlich. Generisch beschriebene Pointcuts lassen sich so z.B. individuell ergänzen. Hiervon können wiederum andere generische Pointcut-Beschreibungen und Advices betroffen
sein, so dass das generische Verhalten individuell angepasst werden kann. Beispiele für
dieses Vorgehen finden sich in Abschnitt 6.2, welcher die Implementation von Techniken
als generische Aspekte sowie deren Anwendung thematisiert.
60
6 Anwendungen
Nachdem im vorherigen Kapitel ein statisches Joinpoint-Modell zur Beschreibung von
Aspekten in Prolog vorgestellt wurde, widmet sich dieses Kapitel Beispielen zur Verwendung dieses Modells. Zunächst wird gezeigt, wie sich Spezifikationen mit Hilfe von
Aspekten separat beschreiben und im Sinne eines Design by Contract als Vor- und
Nachbedingungen platzieren lassen. Im Kern des Kapitels werden verschiedene Techniken des Stepwise Enhancements erläutert. Dabei wird gezeigt, dass sich diese durch das
präsentierte Joinpoint-Modell als generische Aspekte beschreiben lassen. Die Darstellungen werden dabei von Beispielen aus der Literatur zum Stepwise Enhancement begleitet.
Den Abschluss des Kapitels bildet ein Abschnitt zur aspektorientierten Programmierung
von Sprachprozessoren. Hier werden spezielle Techniken für Sprachprozessoren beispielhaft vorgestellt.
6.1 Design by Contract
In Abschnitt 4.2 wurde die Prüfung von Vor- und Nachbedingungen als Crosscutting
Concern identifiziert. In diesem Abschnitt wird gezeigt, wie durch separate Spezifikationen von Prädikateigenschaften mittels Vor- und Nachbedingungen Design by Contract
in Prolog aspektorientiert umgesetzt werden kann. Zunächst wird dazu die Verwendung
dynamischer Tests zur Spezifikation von Vor- und Nachbedingungen diskutiert. Anschließend werden beispielhaft verschiedene Eigenschaften eines Sortierprädikats als Vor- und
Nachbedingungen spezifiziert.
6.1.1 Spezifikation durch dynamische Tests
Die Bibliotheken von SWI-Prolog [93] enthalten das Prädikat assume/1, welches zur
Ausführung von zusätzlichen Tests verwendet werden kann. Ein mit assume/1 ausgeführter Test hinterlässt keine Choicepoints. Schlägt der Test fehl, wird eine Fehlermeldung
ausgegeben und der Debugger gestartet. Bei der Kompilation eines Prolog-Programms
können Aufrufe von assume/1 entfernt werden. Damit bietet das Prädikat eine gute Beschreibungsmöglichkeit für Spezifikationen in Form von Vor- und Nachbedingungen. Die
Beschreibungen stellen jedoch typische Crosscutting Concerns dar, welche die eigentliche
Implementation durchziehen. In Kombination mit einem statischen Joinpoint-Modell,
wie es in Kapitel 5 vorgestellt wurde, lassen sich solche Spezifikationen hingegen separat
als Aspekte beschreiben.
Eine Verwendung von assume/1 in den Advices solcher Aspekte scheint zunächst überflüssig, da eine optimierte Version des Programms bereits existiert, wenn die Spezifika-
61
6 Anwendungen
1
pointcut(goal(mergesort(In, Out)), _, spec1, sort(In, Out)).
2
3
4
port_advice(spec1, sort(In, Out), call):- assume(list(In)).
port_advice(spec1, sort(In, Out), exit):- assume(list(Out)).
5
6
7
list([]).
list([X|Xs]):- list(Xs).
8
9
10
port_advice(spec1, sort(In, Out), call):- assume(ground(In)).
port_advice(spec1, sort(In, Out), exit):- assume(ground(Out)).
11
12
13
port_advice(spec1, sort(In, Out), exit):- assume(permutation(In, Out)).
port_advice(spec1, sort(In, Out), exit):- assume(sorted(Out)).
14
15
16
17
sorted([]).
sorted([X]).
sorted([X1, X2|Xs]):- X1 @=< X2, sorted([X2|Xs]).
Abbildung 6.1: Spezifikation des Prädikats mergesort/2.
tionsaspekte nicht eingewoben werden. Dennoch ist die Spezifikation mittels assume/1
sinnvoll. Zunächst verdeutlicht die Verwendung des Prädikats, dass das Advice keine
zusätzliche Implementation, sondern ein Spezifikationsfragment darstellt. Darüber hinaus ist die Vermeidung zusätzlicher Choicepoints und der automatische Start des Debuggers bei Verletzungen von Spezifikationen ein nützliches Feature des Prädikats. Nicht
zuletzt macht die Verwendung von assume/1 eine doppelte Haltung von optimiertem
Programm ohne Spezifikationen und der unoptimierten Fassung überflüssig.
6.1.2 Spezifikation von Sortierprädikaten
Im folgenden soll das Sortierprädikat mergesort/2 mit Hilfe von Vor- und Nachbedingungen spezifiziert werden. Implementationen des Prädikats finden sich in verschiedenen
Prolog-Lehrbüchern [87, 16]. Abbildung 6.1 zeigt eine Modularisierung verschiedener
Spezifikationsfragmente als Aspekt.
Den Ausgangspunkt bildet die Beschreibung des Pointcuts sort/2, welcher alle Aufrufe des Prädikats mergesort/2 erfasst und dessen Argumente referenziert. Advices
am Call - bzw. Exit-Port eines Aufrufs des Prädikats beschreiben die Eigenschaften des
Prädikats in Form von Vor- und Nachbedingungen. Die ersten beiden Fragmente (Zeilen 3, 4) enthalten eine Spezifikation der Typen. Beim Aufruf des Prädikats erwartet
dieses als erstes Argument eine vollständige Liste. Ein Advice am Call -Port spezifiziert dies als Vorbedingung. Eine erfolgreiche Abarbeitung des Prädikats garantiert eine
vollständige Liste im zweiten Argument. Eine entsprechende Nachbedingung wird am
Exit-Port beschrieben.
Ebenfalls über Vor- und Nachbedingungen lassen sich die Modi des Prädikats spezifizieren. Beim Aufruf des Prädikats muss das erste Argument vollständig instanziiert
sein (Zeile 9). Das Prädikat sichert seinerseits die vollständige Instanziierung des zweiten
62
6.2 Techniken
1
2
port_advice(spec2, sort(In, Out), call):- assume(integer_list(In)).
port_advice(spec2, sort(In, Out), exit):- assume(set(Out)).
3
4
5
integer_list([]).
integer_list([X|Xs]):- integer(X), integer_list(Xs).
6
7
8
set([]).
set([X|Xs]):- \+ member(X, Xs), set(Xs).
9
10
11
port_advice(spec2, sort(In, Out), call):- assume(ground(In)).
port_advice(spec2, sort(In, Out), exit):- assume(ground(Out)).
12
13
14
15
port_advice(spec2, sort(In, Out), exit):- assume(subset(In, Out)).
port_advice(spec2, sort(In, Out), exit):- assume(subset(Out, In)).
port_advice(spec2, sort(In, Out), exit):- assume(sorted_set(Out)).
16
17
18
19
sorted_set([]).
sorted_set([X]).
sorted_set([X1, X2|Xs]):- X1 @< X2, sorted_set([X2|Xs]).
Abbildung 6.2: Veränderte Spezifikation eines Suchprädikats.
Arguments bei Passage des Exit-Ports zu (Zeile 10).
Die letzten beiden Fragmente (Zeilen 12, 13) spezifizieren die Sortiersemantik des
Prädikats durch Nachbedingungen. Bei erfolgreicher Abarbeitung garantiert das Prädikat, dass die Ausgabe eine Permutation der Eingabe ist, d.h. dass alle Elemente der
Eingabe auch in der Ausgabe und keine zusätzlichen Elemente enthalten sind. Darüber
hinaus ist die Ausgabe im Sinne der Standard-Ordnung für Terme aufsteigend sortiert.
Neben dem Mergesort-Algorithmus lässt sich die Sortierung einer Liste auf verschiedene Arten implementieren. Die präsentierten Spezifikationsfragmente lassen sich für
andere Implementationsstrategien wiederverwenden. Durch die Integration von Aufrufen der jeweiligen Prädikate in den Pointcut sort/2 können die entsprechenden Vorund Nachbedingungen in diese Prädikate ebenfalls eingewoben werden. Des Weiteren
ist es möglich, durch Änderungen an den Fragmenten alternative Verhaltensweisen zu
spezifizieren. So kann ein Sortierprädikat beispielsweise mehrfache Auftreten eines Elements entfernen. Dies lässt sich mit einer veränderten Spezifikation des Ausgabetyps
beschreiben. Eine weitere mögliche Änderung könnte die Beschränkung auf Listen von
Integer-Werten als Eingabe sein. Abbildung 6.2 zeigt eine derart veränderte Spezifikation.
6.2 Techniken
In Abschnitt 4.3 wurde die Methode des Stepwise Enhancements vorgestellt und herausgearbeitet, dass diese einen Ansatz zur aspektorientierten Prolog-Programmierung
liefert. Dabei wurden Techniken als generische Aspekte klassifiziert. In diesem Abschnitt
wird nun gezeigt, wie sich Techniken unter Verwendung des in Kapitel 5 präsentierten
63
6 Anwendungen
Joinpoint-Modells generisch beschreiben und für konkrete Anwendungen einsetzen lassen. Zunächst wird dazu der Zusammenhang zwischen Techniken und Aspekten noch
einmal verdeutlicht, bevor dann verschiedene Techniken einzeln erläutert werden.
6.2.1 Techniken und Aspekte
Generizität von Techniken
Eine Technik beschreibt auf generische Art, wie sich die Anwendung dieser auf ein Skelett auswirkt. Grundlage sind dabei die Klassifizierung von Klauseln und Zielen, welche
von der Anwendung einer Technik betroffen sind. Eine Technik nutzt die Klassifikation
dann zu einer Beschreibung, an welchen Stellen zusätzliche Prädikate oder Argumente allgemein platziert werden. Verschiedene Anwendungen einer Technik auf eventuell
verschiedene Skelette unterscheiden sich daher lediglich in den Prädikaten, welche sie betreffen, in den hinzuzufügenden Prädikaten oder in der Position, an welcher zusätzliche
Argumente eingefügt werden sollen.
Beschreibung von Techniken
Die Beschreibung der generischen Anteile einer Technik ist ein zentraler Punkt für das
Verständnis der Technik selbst und ihrer Anwendungen. Ein klares Verständnis einer
Technik führt dabei zu einem Verständnis ihrer Anwendungen auf einer höheren Ebene [46]. Dieser Effekt ist mit der Verwendung von Entwurfsmustern in der objektorientierten Programmierung [28] vergleichbar.
Dennoch finden sich zumeist nur informale [85, 46] oder semiformale Beschreibungen [38] von Techniken, welche zumeist anhand von Beispielen erklärt werden. Formale
Ansätze beschränken sich zumeist auf die Darstellung einer Technik in Verbindung mit
einem Skelett [38, 91]. Andere Ansätze gestatten zwar eine separate Formalisierung von
Techniken, schränken aber deren Anwendung auf bestimmte, besonders einfache Skelettarten ein [78]. Formale Beschreibungsmittel ohne solche Limitierungen sind bis heute Gegenstand der Forschung. Diese sind notwendig, um die Anwendung einer Technik
auf ein Skelett unabhängig von diesem formulieren zu können. Des Weiteren bilden sie
die Grundlage für verschiedene formale Verfahren wie z.B. Korrektheitsbeweise. Bisherige Verfahren beziehen sich lediglich auf die Korrektheit konkreter Erweiterungen eines
Skelettes [37], nicht aber auf die Korrektheit einer Technik an sich.
Anwendungen von Techniken als Aspekte
Wie bereits in Abschnitt 4.3 verdeutlicht wurde, kann die Anwendung einer Technik
auf ein Skelett als ein eigenständiges Concern betrachtet werden, dessen Implementation entlang des Skeletts verteilt ist. Eine separate Beschreibung als Aspekt ermöglicht
eine Fokussierung auf das mit der Anwendung der Technik verbundene Concern und
erleichtert somit Verständnis, Erweiterbarkeit, Wiederverwendung und Wartbarkeit dieses Concerns. Diese Trennung der Concerns mag bei vielen typischen Beispielen der
Literatur zum Stepwise Enhancement noch nicht unbedingt notwendig oder gar sinnvoll
64
6.2 Techniken
sein, gewinnt aber bei komplexeren Beispielen wie der Konstruktion eines abstrakten
Syntaxbaums in einem Parser (vgl. Abschnitt 6.3.2) an Bedeutung.
Techniken als generische Aspekte
In der Beschreibung der Anwendung einer Technik als Aspekt treten die generischen
Merkmale der Technik in konkreten Ausprägungen auf. Daraus folgt, dass das Wesen
der Anwendung der Technik mit Merkmalen der Technik selbst vermengt ist. Dies führt
neben einem erschwerten Verständnis der Anwendung zu einer redundanten Beschreibung der generischen Merkmale einer Technik. So enthält der in Kapitel 5 entwickelte
Aspekt sumlist/0 zur separaten Beschreibung der Summierung von Listenelementen
generische Merkmale der Calculate-Technik. Weitere Aspekte, welche die Anwendung
dieser Technik beschreiben, weisen diese Merkmale ebenfalls auf.
Eine Kapselung der Merkmale als generischen Aspekt fördert die Fokussierung auf die
Anwendung der Technik als Concern. Ein solcher generischer Aspekt enthält vor allem
Pointcut-Beschreibungen, welche die Klassifizierung von Zielen und Klauseln durch die
Technik definieren. Diese Pointcut-Beschreibungen sind dabei unabhängig vom Skelett,
auf welches die Technik angewandt werden soll. Durch die Belegung der generischen
Parameter lässt sich die Technik auf ein konkretes Skelett anwenden. Advices, welche an
den generisch beschriebenen Pointcuts platziert werden, ermöglichen dann die Angabe
der hinzuzufügenden Prädikate.
Dieses Vorgehen trennt die generischen Merkmale einer Technik von den Merkmalen
ihrer Anwendung. Die Technik selbst ist durch einen generischen Aspekt formal beschrieben und klar definiert. Verschiedene Anwendungen der Technik lassen sich unter
Verwendung des generischen Aspekts als eigenständige Aspekte implementieren. Diese Aspekte verwenden die Technik als abstraktes Beschreibungsmittel und formulieren
lediglich die für die Anwendung der Technik entscheidenden Details.
Im weiteren Verlauf dieses Kapitels werden verschiedene Techniken zunächst an Beispielen erläutert. Anschließend werden ihre generischen Merkmale herausgearbeitet und
mit Hilfe generischer Aspekte beschrieben. Den Abschluss bilden jeweils beispielhafte
Anwendungen der Technik als Aspekte. Im folgenden Kapitel werden die vorgestellten
Techniken in einem komplexeren Anwendungsfall zur Implementation von Sprachprozessoren genutzt.
6.2.2 Build- und Calculate-Technik
Die Build-Technik [85] ist eine häufig verwendete Technik zur Konstruktion von Datenstrukturen entlang des Kontrollflusses eines Skeletts. An dieser Konstruktion können
mehrere Prädikate beteiligt sein. Jedes dieser Prädikate wird um ein zusätzliches Argument für die zu konstruierende Struktur ergänzt. In Klauseln der beteiligten Prädikate
stellen zusätzliche Ziele die Verbindung zwischen den im Klauselkörper konstruierten
Strukturen und der im Klauselkopf enthaltenen endgültigen Struktur her.
Die beteiligten Klauseln lassen sich in Initialisierungs- und Konstruktionsklauseln
unterscheiden. Initialisierungsklauseln enthalten keine Aufrufe beteiligter Prädikate im
65
6 Anwendungen
1
2
3
part_set([], _).
part_set([X|Xs], Ys):- member(X, Ys), part_set(Xs, Ys).
part_set([X|Xs], Ys):- \+ member(X, Ys), part_set(Xs, Ys).
4
5
6
7
union([], Ys, Us):- Us = Ys.
union([X|Xs], Ys, Us):- member(X, Ys), union(Xs, Ys, Us1), Us = Us1.
union([X|Xs], Ys, Us):- \+ member(X, Ys), union(Xs, Ys, Us1), Us = [X|Us1].
Abbildung 6.3: Anwendung der Build-Technik zur Implementation von Mengenprädikaten.
Klauselkörper, wodurch keine Teilstrukturen zur Konstruktion der Struktur im Klauselkopf zur Verfügung stehen. Das hinzuzufügende Konstruktionsprädikat initialisiert daher die Struktur. Konstruktionsklauseln verwenden hingegen Strukturen aus Aufrufen
beteiligter Prädikate im Klauselkörper zur Konstruktion der Struktur im Klauselkopf.
Sowohl in Initialisierungs- als auch Konstruktionsklauseln können weitere Elemente zur
Konstruktion der Struktur genutzt werden. Auch Ziele beteiligter Prädikate lassen sich
klassifizieren. Konstruktionsziele treten im Körper von Konstruktionsklauseln auf. Initialisierungsziele liegen hingegen außerhalb beteiligter Klauseln. Durch diese Ziele ist
der Zugriff auf die konstruierten Strukturen möglich.
Eine der Build-Technik sehr ähnliche Technik ist die Calculate-Technik [85]. Sie dient
der Berechnung eines Wertes entlang des Kontrollflusses eines Skeletts. Wie bei der
Build-Technik können mehrere Prädikate zur Berechnung beitragen. Die beteiligten
Prädikate werden dazu ebenfalls um ein zusätzliches Argument erweitert. Ein solches
Argument in einem Klauselkopf eines beteiligten Prädikats wird dann durch den Aufruf des Prädikats is/2 unter Verwendung von eventuellen Zwischenergebnissen aus dem
Klauselkörper berechnet.
Beispiele
Abbildung 6.3 zeigt die Anwendung der Build-Technik zur Implementation des Mengenprädikats union/3. Den Ausgangspunkt bildet das Skelett part set/2, welches eine als
Liste repräsentierte Menge im ersten Argument traversiert. Das zweite Argument enthält
ebenfalls eine auf diese Art repräsentierte Menge. Diese dient der Unterscheidung zweier
Fälle der Rekursion. Im ersten Fall ist das aktuelle Element der ersten Menge in der
zweiten Menge enthalten, im zweiten nicht.
Durch die Anwendung der Build-Technik erhält das Mengenprädikat nun ein zusätzliches drittes Argument. Des Weiteren wird am Ende jeder Klausel ein zusätzliches Prädikat angefügt, welches das zusätzliche Argument im Klauselkopf initialisiert bzw. konstruiert. Verschiedenen Mengenprädikate wie union/3, intersect/3 oder subtract/3
unterscheiden sich lediglich in diesen hinzugefügten Prädikaten zur Initialisierung und
Konstruktion. Im Falle von union/3 wird die Struktur mit der Menge aus dem zweiten
Argument initialisiert und dann schrittweise um Elemente der ersten Menge erweitert,
welche nicht bereits in der zweiten Menge enthalten sind. Das Prädikat konstruiert somit
die Vereinigung der beiden Mengen.
66
6.2 Techniken
Pointcut
val(Pred/N, Struct)
Erfasste Joinpoints
Ziele und Klauseln beteiligter Prädikate
Initialisierungsklauseln
init clause(Pred/N, Struct)
init clause(Pred/N, Elem, Struct)
build clause(Pred/N, S1, ..., Sn, Struct)
Konstruktionsklauseln
build clause(Pred/N, Elem, S1, ..., Sn, Struct)
init goal(Pred/N, Struct)
Initialisierungsziele
build goal(Pred/N, Struct)
Konstruktionsziele
Tabelle 6.1: Generische Pointcuts der Build-Technik.
Als Beispiel für die Anwendung der Calculate-Technik wurde bei der Vorstellung des
statischen Joinpoint-Modells in Kapitel 5 die Summierung von Listenelementen ausführlich betrachtet (vgl. Abbildung 5.1).
Implementation als generischer Aspekt
Die Anwendung der Build-Technik auf ein Skelett kann als separater Aspekt beschrieben
werden. Eine solche Beschreibung lässt sich durch einen generischen Aspekt build/2 unterstützen, welcher für die Technik typische Bestandteile abstrahiert. Hauptbestandteil
der Implementation ist die Umsetzung der Klassifikation von Klauseln und Zielen durch
die Technik. Tabelle 6.1 enthält die dazu definierten Pointcuts und die durch sie erfassten
Joinpoints. Die Implementation selbst zeigt Abbildung 6.4.
Um die Unabhängigkeit dieser Implementation von konkreten Skeletten zu ermöglichen, enthält der Aspekt build/2 zwei generische Parameter. Der erste dieser Parameter
legt einen Namen für den Konstruktionsaspekt fest. Dieser Name dient der Unterscheidung unterschiedlicher Konstruktionsaspekte. Der zweite Parameter enthält eine Liste der beteiligten Prädikate. Ein Element dieser Liste beschreibt dabei ein beteiligtes
Prädikat, die Arität des Prädikats sowie die Position, an der das zusätzliche Argument
eingefügt werden soll.
Den Ausgangspunkt bildet der Pointcut val/2, welcher alle Ziele und Klauseln beteiligter Prädikate erfasst. Als Argumente enthält dieser Pointcut das beteiligte Prädikat
inklusive dessen Arität sowie eine ungebundene Variable für die zu konstruierende Struktur. Diese zu konstruierende Struktur wird dann durch ein Term-Advice als zusätzliches
Argument in die beteiligten Prädikate integriert (Zeilen 5, 6). Die Position, an welcher das Argument eingefügt wird, wird dabei dem zweiten generischen Parameter des
Aspekts entnommen.
Darauf aufbauend lässt sich eine gemeinsame Pointcut-Beschreibung für Initialisierungs- und Konstruktionsklauseln formulieren. Diese Klauseln definieren beteiligte Prädikate und müssen daher auch im Pointcut val/2 enthalten sein (Zeile 9). Das zweite Argument dieses Pointcuts referenziert die zu konstruierende Struktur. Um Initialisierungsvon Konstruktionsklauseln unterscheiden zu können, werden zunächst alle im Klauselkörper erzeugten Strukturen gesammelt (Zeilen 10, 11). Ist keines der Prädikate im
67
6 Anwendungen
1
2
3
pointcut(Joinpoint, _, build(_, Preds), val(Pred/N, _Struct)):( Joinpoint = goal(Goal) ; Joinpoint = clause(Goal, _) ),
nonvar(Goal), functor(Goal, Pred, N), memberchk(Pred/N/_, Preds).
4
5
6
term_advice(build(_, Preds), val(Pred/N, Struct), extra_arg(Struct, Pos)):memberchk(Pred/N/Pos, Preds).
7
8
9
10
11
12
13
14
15
16
pointcut(clause(_, _), Context, build(Name, Preds), Pointcut):context(Context, parallel, build(Name, Preds), val(Pred/N, Struct)),
all_context(S, Context, beside(all), build(Name, Preds), val(_, S),
ReverseStructs),
reverse([Struct|ReverseStructs], Structs),
( Structs = [Struct] -> Id = init_clause ; Id = build_clause ),
( context(Context, parallel, Name, elem(Elem)) ->
Pointcut =.. [Id, Pred/N, Elem|Structs] ;
Pointcut =.. [Id, Pred/N|Structs] ).
17
18
19
20
pointcut(goal(_), Context, build(Name, Preds), init_goal(Pred/N, Struct)):context(Context, parallel, build(Name, Preds), val(Pred/N, Struct)),
\+ context(Context, inside(first), build(Name, Preds), val(_, _)).
21
22
23
24
pointcut(goal(_), Context, build(Name, Preds), build_goal(Pred/N, Struct)):context(Context, parallel, build(Name, Preds), val(Pred/N, Struct)),
context(Context, inside(first), build(Name, Preds), val(_, _)).
25
26
27
pointcut(_, Context, Aspect, Pointcut):context(Context, parallel, build(Aspect, _), Pointcut).
Abbildung 6.4: Generische Beschreibung der Build-Technik.
Klauselkörper durch einen Pointcut val/2 erfasst, handelt es sich um eine Initialisierungsklausel. Hingegen enthält der Klauselkörper von Konstruktionsklauseln beteiligte
Prädikate, welche durch einen solchen Pointcut erfasst werden (Zeile 13).
Aus der Klassifizierung der Klausel und den an der Konstruktion beteiligten Strukturen lässt sich dann ein Pointcut konstruieren, welcher die Klausel enthält. Initialisierungsklauseln werden von einem Pointcut init clause, Konstruktionsklauseln von
einem Pointcut build clause erfasst. Die Arität der Pointcuts hängt dabei von der
Anzahl der beteiligten Strukturen ab. Das erste Argument enthält stets das beteiligte
Prädikat. Die folgenden Argumente umfassen die an der Konstruktion beteiligten Strukturen, wobei diese nach ihrem Auftreten im Klauselkörper geordnet sind und die Struktur des Klauselkopfes den Abschluss bildet (Zeilen 12, 16). Da Initialisierungsklauseln
keine beteiligten Strukturen im Klauselkörper enthalten, weisen init clause-Pointcuts
lediglich die Struktur des Klauselkopfes als zweites Argument auf.
Im Beispiel des Prädikats union/3 aus Abbildung 6.3 finden sowohl in Initialisierungsals auch in Konstruktionsklauseln weitere Elemente bei der Konstruktion der Struktur
Verwendung. So wird in der Initialisierungsklausel die als zweites Argument geführte Menge genutzt. In der letzten Konstruktionsklausel wird das aktuelle Element der
ersten Menge zur Konstruktion der Gesamtstruktur herangezogen. Solche zusätzlichen
68
6.2 Techniken
Elemente können durch einen Pointcut elem/1 des Konstruktionsaspekts beschrieben
werden. Ist eine Initialisierungs- oder Konstruktionsklausel Teil eines solchen Pointcuts,
geht das als Argument enthaltene Element in den jeweiligen Pointcut ein. Es wird in
diesem Fall als zweites Argument des Pointcuts geführt (Zeile 15).
Um eine Anwendung des generischen Aspekts zu erleichtern, werden alle durch den
generischen Aspekt beschriebenen Pointcuts auch für Aspekte definiert, welche den generischen Aspekt verwenden (Zeilen 26, 27). Die im weiteren Verlauf des Kapitels vorgestellten generischen Aspekte folgen diesem Vorgehen.
Anwendung
Unter Verwendung des vorgestellten generischen Aspekts lässt sich nun der Konstruktionsaspekt des Prädikats union/3 separat beschreiben. Abbildung 6.5 zeigt eine mögliche Vorgehensweise. Zunächst wird die Abhängigkeit des Aspekts von den generischen
Aspekten build/2 und rename/2 deklariert (Zeilen 1, 2). Da bei der Initialisierung die
als zweites Argument geführte Liste benötigt wird, wird diese über einen Pointcut elem/1
in die Build-Technik eingebunden (Zeile 4). Die Build-Technik stellt dann einen Pointcut init clause/3 bereit, welcher durch den Konstruktionsaspekt mit einem Advice
zur Initialisierung der Vereinigung am Exitbody-Port versehen wird (Zeilen 7, 8).
Die erste Konstruktionsklausel kommt ohne ein zusätzliches Konstruktionselement
aus und wird durch den von der Build-Technik beschriebenen Pointcut build clause/3
erfasst. Durch die Platzierung eines Exitbody-Advice wird die rekursiv konstruierte Vereinigung als Ergebnis verwendet (Zeilen 9, 10). Für die zweite Konstruktionsklausel
wird hingegen ein Pointcut elem/1 deklariert (Zeile 5). Dadurch kann das aktuelle
Mengenelement in der Konstruktion verwendet werden und die Klausel ist Teil eines
Pointcuts build clause/4. Auch hier beschreibt ein Exitbody-Advice die Konstruktion
(Zeilen 11, 12).
Im vorherigen Beispiel wurden die beiden Konstruktionsklauseln anhand eines zusätzlich zur Verfügung gestellten Konstruktionselements unterschieden. Eine alternative Verwendung der Build-Technik zeigt Abbildung 6.6. In dieser Beschreibung wird das Element beiden Klauseln zur Verfügung gestellt (Zeilen 5–8). Die Unterscheidung der beiden Klauseln wird über einen Term als Argument des Pointcuts elem/1 realisiert. Dieser
Term wird dann bei der Platzierung der Advices zur Fallunterscheidung herangezogen
(Zeilen 12–15), was zu einer verbesserten Lesbarkeit des Konstruktionsaspekts führt.
Allgemein lässt sich die Verwendung der Build-Technik in zwei Schritte einteilen.
Im ersten Schritt werden Pointcuts elem/1 deklariert, welche Klauseln enthalten, die
zusätzliche Elemente bei der Initialisierung oder Konstruktion benötigen. Im zweiten
Schritt werden dann an den Exitbody-Ports der durch die Build-Technik bereitgestellten Pointcuts Advices platziert, welche die Konstruktion beschreiben. Die Pointcuts
der Build-Technik enthalten dabei das beteiligte Prädikat, ein eventuell zusätzlich zur
Konstruktion benötigtes Element, Teilstrukturen aus dem Klauselkörper und die im
Klauselkopf zu konstruierende Struktur als Argumente.
Da sich Calculate- und Build-Technik nur in der Art der zusätzlichen Prädikate unterscheiden, lässt sich die vorgestellte Implementation des generischen Aspekts build/2
69
6 Anwendungen
1
2
require_aspects(union, [build(union, [part_list/2/last]),
rename(part_list/2, union)]).
3
4
5
pointcut(clause(part_list([], Ys), _), _, union, elem(Ys)).
pointcut(clause(part_list([X|_], _), ( \+ member(X, _), _)), _, union, elem(X)).
6
7
8
9
10
11
12
port_advice(union, init_clause(part_list/2, Ys, Us), exitbody):Us = Ys.
port_advice(union, build_clause(part_list/2, Us, Us1), exitbody):Us1 = Us.
port_advice(union, build_clause(part_list/2, X, Us, Us1), exitbody):Us1 = [X|Us].
Abbildung 6.5: Aspekt zur Konstruktion der Vereinigung zweier Mengen unter Verwendung der Build-Technik.
1
2
require_aspects(union, [build(union, [part_list/2/last]),
rename(part_list/2, union)]).
3
4
5
6
7
8
pointcut(clause(part_list([], Ys), _), _, union, elem(Ys)).
pointcut(clause(part_list([X|_], Ys), ( member(X, Ys), _)), _, union,
elem(member(X))).
pointcut(clause(part_list([X|_], Ys), ( \+ member(X, Ys), _)), _, union,
elem(nonmember(X))).
9
10
11
12
13
14
15
port_advice(union, init_clause(part_list/2, Ys, Us), exitbody):Us = Ys.
port_advice(union, build_clause(part_list/2, member(_), Us, Us1), exitbody):Us1 = Us.
port_advice(union, build_clause(part_list/2, nonmember(X), Us, Us1), exitbody):Us1 = [X|Us].
Abbildung 6.6: Alternative Beschreibung eines Aspekts zur Konstruktion der Vereinigung zweier Mengen unter Verwendung der Build-Technik.
1
2
require_aspects(sumlist, [build(sumlist, [list/1/last]),
rename(list/1, sumlist)]).
3
4
pointcut(clause(list([X, _]), _), _, sumlist, elem(X)).
5
6
7
8
9
port_advice(sumlist, init_clause(list/1, Sum), exitbody):Sum is 0.
port_advice(sumlist, build_clause(list/1, X, TSum, Sum), exitbody):Sum is TSum + X.
Abbildung 6.7: Beschreibung der Summierung von Listenelementen als Aspekt unter
Verwendung der Calculate-Technik.
70
6.2 Techniken
1
2
list([]).
list([X|Xs]):- list(Xs).
3
4
5
reverse([], A_in, A_out):- A_out = A_in.
reverse([X|Xs], A_in, A_out) :- A1 = [X|A_in], reverse(Xs, A1, A2), A_out = A2.
Abbildung 6.8: Anwendung der Akkumulator-Technik zur Invertierung einer Liste.
auch für Anwendungen der Calculate-Technik nutzen. Dadurch lässt sich der in Abbildung 5.20 gezeigte Aspekt zur Summierung von Listenelementen kompakter beschreiben.
Eine solche Beschreibung als Anwendung der Calculate-Technik zeigt Abbildung 6.7.
6.2.3 Akkumulator-Techniken
Die Verwendung von Akkumulatoren [87] ist eine weitere typische Technik der PrologProgrammierung. Akkumulatoren stellen eine Möglichkeit dar, globale Variablen entlang
eines Skeletts zu propagieren und zu berechnen. Je nachdem, ob eine solche globale Variable der Konstruktion einer Datenstruktur oder der Berechnung eines numerischen Werts
dient, werden Accumulate-Build- und Accumulate-Calculate-Technik unterschieden [85].
In beiden Fällen werden beteiligte Prädikate um zwei zusätzliche Argumente erweitert. Das erste Argument enthält den aktuellen Wert der Variable. Das zweite Argument
nimmt den neuen Wert auf. Die beteiligten Klauseln lassen sich wie bei der Build-Technik
in Initialisierungs- und Konstruktionsklauseln klassifizieren. Initialisierungsklauseln enthalten im Klauselkörper keine an der Akkumulation beteiligten Prädikatsaufrufe und
werden um zusätzliche Prädikate ergänzt, welche die Verbindung zwischen alten und
neuen Akkumulatorwerten herstellen. In den meisten Fällen werden beide unifiziert.
Hingegen enthalten die Klauselkörper der Konstruktionsklauseln Prädikataufrufe, welche an der Akkumulation beteiligt sind. Durch die Akkumulator-Techniken werden an
verschiedenen Stellen dieser Klauseln Prädikate hinzugefügt, welche die Akkumulatoren
verschiedener Prädikatsaufrufe in Verbindung setzen. So kann z.B. ein Konstruktionsschritt zwischen zwei Prädikatsaufrufen, welche an der Akkumulation beteiligt sind,
notwendig sein. Des Weiteren verbindet ein zusätzlicher Prädikatsaufruf am Klauselende den zuletzt berechneten Akkumulator mit dem endgültigen Wert des Akkumulators
im Klauselkopf.
Beispiel
Ein häufiges Einführungsbeispiel für die Verwendung eines Akkumulators ist die effiziente Implementation reverse/3 zur Invertierung einer Liste [87, 16], wie sie Abbildung 6.8
zeigt. Als Skelett dient mit list/1 eine rekursive Definition einer Liste. Die invertierte
Liste wird nun in den beiden zusätzlichen Argumenten akkumuliert.
Die Initialisierungsklausel wird durch die Anwendung der Technik um einen zusätzlichen Prädikatsaufruf ergänzt, welcher den neuen mit dem alten Wert unifiziert. In der
Konstruktionsklausel erfolgt die eigentliche Akkumulation. Das aktuelle Element der
zu invertierenden Liste wird durch einen zusätzlichen Prädikataufruf zunächst vor das
71
6 Anwendungen
Zwischenergebnis der bereits invertierten Elemente gesetzt. Das so entstandene neue
Zwischenergebnis dient dann als Eingabe für den rekursiven Aufruf. Das aktuelle Ergebnis wird schließlich durch einen weiteren zusätzlichen Prädikatsaufruf mit dem Ergebnis
des rekursiven Aufrufs unifiziert.
Wird bei einem Aufruf von reverse/3 der Akkumulator mit der leeren Liste initialisiert, liefert dieses sukzessive Platzieren aktueller Elemente vor das bisherige Ergebnis
die invertierte Liste als Ergebnis der Akkumulation. Dabei ist das Prädikat effizienter
als eine naive Implementation reverse/2, welche auf append/3 zurückgreift, da eine
doppelte Rekursion vermieden wird.
Propagierung des Akkumulators
Ein wichtiges Merkmal der Akkumulator-Technik ist die Propagierung des Akkumulators innerhalb beteiligter Klauseln. Dabei kann vor und nach jeder Verwendung des
Akkumulators dessen Wert verändert werden. Eine solche Propagierung ist auch für andere Anwendungen nützlich. Daher soll an dieser Stelle zunächst ein generischer Aspekt
pass/2 vorgestellt werden. Tabelle 6.2 führt die durch den Aspekt definierten Pointcuts
auf. Abbildung 6.9 zeigt die entsprechenden Pointcut-Beschreibungen.
Den Ausgangspunkt bildet der Pointcut pass/2 (Zeilen 1–3), welcher Ziele und Klauseln beteiligter Prädikate erfasst. Als erstes Argument enthält der Pointcut das beteiligte
Prädikat Das zweite Argument umfasst eine ungebundene Variable, welche durch Advices zur Anpassung und Weitergabe der Information verwendet wird.
Klauseln können in Stopp- und Passklauseln unterschieden werden(Zeilen 5-12). Stoppklauseln werden von einem stop clause/1-Pointcut erfasst. Ihre Körper keine Ziele,
welche an der Informationsweitergabe beteiligt sind. Konstruktionsklauseln weisen solche Ziele auf und werden von einem pass clause/3-Pointcut erfasst. Das zweite Argument dieses Pointcuts beinhaltet die temporäre Variable, welche das letzte beteiligte
Ziel des Klauselkörpers zur Informationsweitergabe nutzt. Im letzten Argument wird die
temporäre Variable geführt, in welcher dem ersten beteiligten Ziel des Klauselkörpers
Informationen zur Verfügung gestellt werden.
Auf dieselbe Art lassen sich Ziele in Initialisierungs- und Passziele unterteilen (Zeilen 14–24). Initialisierungsziele sind Ziele beteiligter Prädikate, welche außerhalb von
Passklauseln liegen und somit nicht zur Informationsweitergabe beitragen. Sie werden
von einem init goal/1-Pointcut erfasst. Der Pointcut pass goal/3 adressiert Passziele, welche Informationen transportieren. Der Pointcut enthält die temporäre Variable,
in welcher ein vorhergegangenes Ziel oder die Klausel Informationen bereitstellen, als
zweites Argument. Das letzte Argument bildet die Variable, welche das Ziel selbst zur
Informationsweitergabe nutzt.
Implementation als generischer Aspekt
Unter Verwendung des generischen Aspekts pass/2 lassen sich die typischen Merkmale
der Akkumulator-Technik als generischer Aspekt accumulate/2 beschreiben und so für
konkrete Aspekte nutzbar machen. Als generische Argumente dienen erneut ein Name
72
6.2 Techniken
Pointcut
pass(Pred/N, Pass)
stop
pass
init
pass
Erfasste Joinpoints
Ziele und Klauseln beteiligter Prädikate
clause(Pred/N)
Stoppklauseln
clause(Pred/N, Passed, Pass) Passklauseln
goal(Pred/N)
Initialisierungsziele
goal(Pred/N, Struct)
Passziele
Tabelle 6.2: Generische Pointcuts zur Propagierung von Informationen.
1
2
3
pointcut(Joinpoint, _, pass(_, Preds), pass(Pred/N, _Pass)):( Joinpoint = goal(Goal) ; Joinpoint = clause(Goal, _) ),
nonvar(Goal), functor(Goal, Pred, N), memberchk(Pred/N/_, Preds).
4
5
6
7
pointcut(clause(_, _), Context, pass(Name, Preds), stop_clause(Pred/N)):context(Context, parallel, pass(Name, Preds), pass(Pred/N, _)),
\+ context(Context, beside(near), pass(Name, Preds), pass(_, _)).
8
9
10
11
12
pointcut(clause(_, _), Context, pass(Name, Preds),
pass_clause(Pred/N, Passed, Pass)):context(Context, parallel, pass(Name, Preds), pass(Pred/N, Pass)),
context(Context, beside(near), pass(Name, Preds), pass(_, Passed)).
13
14
15
16
17
pointcut(goal(_), Context, pass(Name, Preds), init_goal(Pred/N)):context(Context, parallel, pass(Name, Preds), pass(Pred/N, _)),
\+ context(Context, beside(near), pass(Name, Preds), pass(_, _)),
\+ context(Context, inside(near), pass(Name, Preds), pass(_, _)).
18
19
20
21
22
23
24
pointcut(goal(_), Context, pass(Name, Preds),
pass_goal(Pred/N, Passed, Pass)):context(Context, parallel, pass(Name, Preds), pass(Pred/N, Pass)),
( context(Context, beside(near), pass(Name, Preds), pass(_, Passed)) ->
true ;
context(Context, inside(near), pass(Name, Preds), pass(_, Passed)) ).
Abbildung 6.9: Generische Pointcut-Beschreibungen zur Weitergabe von Informationen
zwischen benachbarten Pointcuts mittels zusätzlicher Variablen.
Pointcut
accumulator(Pred/N, In, Out)
init clause(Pred/N, In, Out)
init clause(Pred/N, Elem, In, Out)
build clause(Pred/N, In, Out, Passed, Pass)
build clause(Pred/N, Elem, In, Out, Passed, Pass)
init goal(Pred/N, In, Out)
build goal(Pred/N, In, Out, Passed, Pass)
Erfasste Joinpoints
Ziele und Klauseln beteiligter Prädikate
Initialisierungsklauseln
Konstruktionsklauseln
Initialisierungsziele
Konstruktionsziele
Tabelle 6.3: Generische Pointcuts der Akkumulator-Technik.
73
6 Anwendungen
und eine Liste der beteiligten Prädikate. Tabelle 6.3 und Abbildung 6.10 zeigen die
Pointcuts, welche der Aspekt definiert sowie deren Beschreibung.
Ein generisches Merkmal der Technik ist die Erweiterung beteiligter Prädikate um
einen Akkumulator, welcher durch zwei zusätzliche Argumente repräsentiert wird. Der
generische Pointcut accumulator/3 erfasst hierfür alle Ziele und Klauseln beteiligter
Prädikate. Ein an einem solchen Pointcut platziertes Term-Advice fügt dann die zusätzlichen Argumente ein (Zeilen 3–9).
Die Pointcuts des generischen Aspekts pass/2 können nun zur Klassifizierung von Zielen und Klauseln genutzt werden. Initialisierungsklauseln haben keine an der Akkumulation beteiligten Ziele im Klauselkörper. Sie werden von einem stop clause/1-Pointcut
des pass/2-Aspekts erfasst. Abhängig davon, ob durch den konkreten Aspekt ein zusätzliches Konstruktionselement zur Verfügung gestellt wird, lassen sich init clause/3bzw. init clause/4-Pointcuts der Akkumulator-Technik unterscheiden (Zeilen 11–17).
Die erste Variante führt das beteiligte Prädikat, sowie den Ein- und Ausgabe-Akkumulator
als Argumente. Die zweite Variante enthält als zweites Argument ein zusätzliches bei
der Akkumulation verwendetes Element.
Konstruktionsklauseln werden durch build clause/5 bzw. build clause/6-Pointcuts
erfasst (Zeilen 19–26). Sie führen als Argumente das beteiligte Prädikat, ein optionales
Element, welches bei der Akkumulation verwendet werden kann, den Ein- und AusgabeAkkumulator sowie die beiden letzten Argumente eines pass clause/3-Pointcuts. Das
erste dieser Argumente beinhaltet die vom letzten beteiligten Ziel des Klauselkörpers
eingeführte temporäre Variable. Das zweite Argument stellt eine solche temporäre Variable für das erste beteiligte Ziel des Klauselkörpers bereit. Diese temporären Variablen
lassen sich in Advices dann nutzen, um den Inhalt des Akkumulators zu propagieren
oder zu verändern.
Initialisierungziele befinden sich außerhalb von beteiligten Klauseln und werden somit
von einem init goal/1-Pointcut des pass/2-Aspekts erfasst. Die Pointcut-Beschreibung
init goal/3 ermöglicht die Platzierung von Advices an solchen Zielen (Zeilen 28–31).
Im zweiten Argument stellt die Beschreibung den Eingabe-Akkumulator zur Verfügung,
wodurch dieser initialisiert werden kann. Der im letzten Argument enthaltene AusgabeAkkumulator ermöglicht darüber hinaus den Zugriff auf das Ergebnis der Akkumulation.
Letztlich erfasst die Pointcut-Beschreibung build goal/5 Ziele beteiligter Prädikate
im Körper von Konstruktionsklauseln (Zeilen 33–38). Als Argumente enthält ein solcher
Pointcut das beteiligte Prädikat, das Akkumulator-Paar und die beiden temporären Variablen des korrespondierenden pass goal/3-Pointcuts. Die erste dieser Variablen wird
entweder von der Klausel oder einem vorhergehenden beteiligten Ziel zur Verfügung
gestellt. Die zweite Variable wird hingegen für folgende beteiligte Ziele oder die Klausel bereitgestellt. Auch hier kann durch die Verwendung der Variablen in Advices die
Propagierung oder Veränderung des Akkumulators an einem Port des erfassten Ziels
beschrieben werden.
Anwendung
Mit Hilfe des vorgestellten generischen Aspekts accumulate/2 lässt sich die Akku-
74
6.2 Techniken
1
require_aspects(accumulate(Name, Preds), pass(accu(Name), Preds)).
2
3
4
5
pointcut(Joinpoint, _, accumulate(_, Preds), accumulator(Pred/N, _In, _Out)):( Joinpoint = goal(Goal) ; Joinpoint = clause(Goal, _) ),
nonvar(Goal), functor(Goal, Pred, N), memberchk(Pred/N/_, Preds).
6
7
8
9
term_advice(accumulate(_, Preds), accumulator(Pred/N, A_in, A_out),
extra_args([A_in, A_out], Pos)):memberchk(Pred/N/Pos, Preds).
10
11
12
13
14
15
16
17
pointcut(clause(_, _), Context, accumulate(Name, Preds), Pointcut):context(Context, parallel,
accumulate(Name, Preds), accumulator(Pred/N, In, Out)),
context(Context, parallel, pass(accu(Name), _), stop_clause(Pred/N)),
( context(Context, parallel, Name, elem(Elem)) ->
Pointcut = init_clause(Pred/N, Elem, In, Out) ;
Pointcut = init_clause(Pred/N, In, Out) ).
18
19
20
21
22
23
24
25
26
pointcut(clause(_, _), Context, accumulate(Name, Preds), Pointcut):context(Context, parallel,
accumulate(Name, Preds), accumulator(Pred/N, In, Out)),
context(Context, parallel,
pass(accu(Name), _), pass_clause(Pred/N, Passed, Pass)),
( context(Context, parallel, Name, elem(Elem)) ->
Pointcut = build_clause(Pred/N, Elem, In, Out, Passed, Pass) ;
Pointcut = build_clause(Pred/N, In, Out, Passed, Pass) ).
27
28
29
30
31
pointcut(goal(_), Context, accumulate(Name, Preds), init_goal(Pred/N, In, Out)):context(Context, parallel,
accumulate(Name, Preds), accumulator(Pred/N, In, Out)),
context(Context, parallel, pass(accu(Name), _), init_goal(Pred/N)).
32
33
34
35
36
37
38
pointcut(goal(_), Context, accumulate(Name, Preds),
build_goal(Pred/N, In, Out, Passed, Pass)):context(Context, parallel,
accumulate(Name, Preds), accumulator(Pred/N, In, Out)),
context(Context, parallel,
pass(accu(Name), _), pass_goal(Pred/N, Passed, Pass)).
Abbildung 6.10: Generische Pointcut-Beschreibungen der Akkumulator-Technik zur
Klassifizierung beteiligter Klauseln und Ziele.
75
6 Anwendungen
1
2
require_aspects(reverse_ac, [accumulate(reverse_ac, [list/1/last]),
rename(list/1, reverse)]).
3
4
pointcut(clause(list([X|_]), _), _, reverse_ac, elem(X)).
5
6
7
8
9
10
11
12
13
14
15
16
17
port_advice(reverse_ac, init_clause(list/1, A_in, A_out), enterbody):A_out = A_in.
port_advice(reverse_ac, build_clause(list/1, X, A_in, _, _, Pass), enterbody):Pass = [X|A_in].
port_advice(reverse_ac, build_clause(list/1, _, _, A_out, Passed, _), exitbody):A_out = Passed.
port_advice(reverse_ac, build_goal(list/1, A_in, _, Passed, _), call):A_in = Passed.
port_advice(reverse_ac, build_goal(list/1, _, A_out, _, Pass), exit):Pass = A_out.
port_advice(reverse_ac, init_goal(list/1, A_in, _), call):A_in = [].
Abbildung 6.11: Separate Beschreibung der Invertierung einer Liste durch Akkumulation.
mulation einer invertierten Liste als Anwendung der Akkumulator-Technik separat beschreiben. Abbildung 6.11 zeigt den Aspekt reverse ac/0, welcher eine solche Beschreibung enthält. Im Initialisierungsfall wird das aktuelle mit dem temporären Ergebnis
unifiziert (Zeilen 6, 7). In Konstruktionsklauseln wird das temporäre Ergebnis um das
aktuelle Listenelement erweitert und an den Klauselkörper weitergereicht (Zeilen 8, 9).
Das dort enthaltene Konstruktionsziel nutzt diese Informationen als neues temporäres
Ergebnis (Zeilen 12, 13) und gibt seinerseits das Endergebnis weiter (Zeilen 14, 15). Am
Ende der Konstruktionsklausel wird das Endergebnis dann mit diesem Ergebnis unifiziert (Zeilen 10, 11). Durch ein am Call -Port von Initialisierungszielen platziertes Advice
wird der Akkumulator mit der leeren Liste initialisiert (Zeilen 16, 17).
6.2.4 Differenzstrukturen
Die Verwendung von Differenzstrukturen an Stelle vollständiger Datenstrukturen ist eine
gängige Prolog-Programmiertechnik [87]. Meist ist ihre Anwendung in einer Steigerung
der Effizienz motiviert. Dies ist darin begründet, dass sie zur Repräsentation unvollständiger Datenstrukturen verwendet werden können.
So bieten beispielsweise Differenzlisten die Möglichkeit, unvollständige Listen darzustellen. Eine solche unvollständige Liste Xs\Ys besteht dann aus bereits bekannten
Elementen Xs am Listenanfang und einem unvollständigen Listenende Ys, welches als
Zeiger auf das Ende der Liste betrachtet werden kann. Durch diese Art der Darstellung
ist es möglich, Elemente am Ende einer Liste einzufügen, ohne auf append/3 zurückzugreifen. Allgemein lassen sich Differenzlisten durch Unifikation in konstanter Zeit aneinanderfügen.
76
6.2 Techniken
1
2
list([]).
list([X|Xs]):- list(Xs).
3
4
5
reverse([], Ys\Zs):- Ys = Zs.
reverse([X|Xs], Ys\Zs) :- Zs1 = [X|Zs], reverse(Xs, Ys1\Zs1), Ys = Ys1.
Abbildung 6.12: Verwendung einer Differenzstruktur zur effizienten Invertierung einer
Liste.
Beispiel
Abbildung 6.12 zeigt eine effiziente Implementation zur Invertierung einer Liste, welche
auf der Verwendung von Differenzlisten basiert. Die Implementation stellt eine Erweiterung des Skeletts list/1 dar. Die invertierte Liste wird dabei im Listenschwanz konstruiert. In der Initialisierungsklausel werden diese Elemente aus dem Schwanz der Liste
als Listenkopf übernommen (Zeile 4). Die so erzeugte Differenzliste repräsentiert damit
eine leere Liste. In der Konstruktionsklausel wird das aktuelle Listenelement am Ende
der Differenzliste eingefügt. Der Listenkopf aus dem rekursiven Aufruf wird dann als
Listenkopf der zu konstruierenden Liste verwendet (Zeile 5).
Differenzstrukturen und Akkumulatoren
Vergleicht man die Implementation zur Invertierung einer Liste unter Verwendung von
Differenzlisten aus Abbildung 6.12 mit der unter Verwendung von Akkumulatoren aus
Abbildung 6.8, lässt sich eine große Ähnlichkeit feststellen. Die Programme unterscheiden
sich lediglich syntaktisch. Die Bestandteile der Differenzlisten finden sich als separate Argumente in der Akkumulator-Implementation wieder, wobei ihre Reihenfolge vertauscht
wurde.
Beim Vergleich weiterer Implementationen mit Hilfe von Akkumulatoren oder Differenzstrukturen, lässt sich allgemein feststellen, dass Akkumulatoren Strukturen von
hinten nach vorne (bottom-up) konstruieren, während Differenzstrukturen dieselben
Strukturen von vorne nach hinten (top-down) konstruieren [87]. Dies hat Auswirkungen auf das Skelett, auf welches eine der beiden Technik angewendet werden muss, um
die gewünschte Erweiterung zu erhalten. Umgekehrt kann die Vorgabe eines Skeletts die
Denkweise einer der beiden Techniken unterstützen und so deren Auswahl beeinflussen.
Implementation als generischer Aspekt
Allgemein können Differenzstrukturen als Generalisierung von Akkumulatoren betrachtet werden [87]. Dies wirkt sich auch auf die Implementation als generischen Aspekt
difference/3 aus. Die Pointcut-Beschreibungen der Akkumulator-Technik aus Abbildung 6.10 können daher einfach an eine veränderte Grundlage angepasst werden.
Diese veränderte Grundlage bildet die Pointcut-Beschreibung diff struct/3 aus Abbildung 6.13. Im Gegensatz zur Beschreibung accumulator/3 aus Abbildung 6.10 enthält
dieser Pointcut in den Argumenten zwei und drei nicht die Akkumulatoren, sondern die
77
6 Anwendungen
1
2
3
4
pointcut(Joinpoint, _, difference(_, _, Preds),
diff_struct(Pred/N, _Struct, _Hole)):( Joinpoint = goal(Goal) ; Joinpoint = clause(Goal, _) ),
functor(Goal, Pred, N), memberchk(Pred/N/_, Preds).
5
6
7
8
9
term_advice(difference(_, Symbol, Preds), diff_struct(Pred/N, Struct, Hole),
extra_arg(DiffStruct, Pos)):member(Pred/N/Pos, Preds), functor(DiffStruct, Symbol, 2),
arg(1, DiffStruct, Struct), arg(2, DiffStruct, Hole).
Abbildung 6.13: Generische Beschreibung zur Erweiterung von Prädikaten um eine Differenzstruktur.
Differenzstruktur sowie die Referenz auf den unvollständigen Teil der Struktur (Zeilen 1–4). Anders als bei der Akkumulator-Technik werden betroffene Prädikate nur um
ein zusätzliches Argument erweitert (Zeilen 6–9). Da beide Bestandteile der Differenzstruktur in diesem Argument aufgenommen werden müssen, werden diese durch ein
Funktionssymbol zu einem Term kombiniert. Dieses Symbol wird dem Aspekt als generisches Argument übergeben. Gewöhnlich wird \/2 oder -/2 verwendet.
Anwendung
Abbildung 6.14 zeigt, wie sich die Invertierung einer Liste mittels einer Differenzliste als
Aspekt reverse dl/0 separat beschreiben lässt. Der Aspekt baut auf dem generischen
Aspekt difference/3 auf. Dem Prädikat list/1 wird an letzter Position ein zusätzliches Argument hinzugefügt, welches die invertierte Liste in Form einer Differenzliste
aufnimmt. Listenkopf und -schwanz werden durch den Infix-Operator \/2 getrennt.
Verschiedene Port-Advices werden an den Pointcuts des generischen Aspekts platziert.
Am Exitbody-Port der Initialisierungsklausel wird der Listenkopf mit dem Listenschwanz
unifiziert (Zeilen 6, 7). Beim Eintritt in den Körper einer Konstruktionsklausel wird am
Enterbody-Port das aktuelle Listenelement am Ende der Differenzliste eingefügt und der
Listenschwanz an den Klauselkörper weitergereicht (Zeilen 8, 9). Das rekursive Konstruktionsziel verwendet diese Struktur als Listenschwanz (Zeilen 12, 13) und gibt seinerseits
den Listenkopf weiter (Zeilen 14, 15). Am Exitbody-Port der Konstruktionsklausel wird
dieser dann mit dem Listenkopf des Klauselkopfes unifiziert (Zeilen 10, 11).
Differenzstrukturen und Build-Technik
Der im vorherigen Abschnitt vorgestellte Aspekt reverse dl/0 verdeutlicht einen typischen Nachteil bei der Verwendung von Differenzstrukturen. Sie erhöhen einerseits
die Effizienz der betroffenen Prädikate, erschweren jedoch oftmals deren deklaratives
Verständnis. Die Invertierung einer Liste lässt sich unter Verwendung der Build-Technik
klar verständlich beschreiben. Der Aspekt reverse/0 in Abbildung 6.15 enthält eine
solche Beschreibung in separierter Form. Die invertierte Liste wird mit einer leeren Liste initialisiert (Zeilen 5, 6) und durch Anhängen des aktuellen Listenelements an die
invertierte Restliste konstruiert (Zeilen 7, 8).
78
6.2 Techniken
1
2
require_aspects(reverse_dl, [difference(reverse_dl, \, [list/1/last]),
rename(list/1, reverse_dl)]).
3
4
pointcut(clause(list([X|_]), _), _, reverse_dl, elem(X)).
5
6
7
8
9
10
11
12
13
14
15
port_advice(reverse_dl,
Ys = Zs.
port_advice(reverse_dl,
Pass = [X|Zs].
port_advice(reverse_dl,
Ys = Passed.
port_advice(reverse_dl,
Hole = Passed.
port_advice(reverse_dl,
Pass = Struct.
init_clause(list/1, Ys, Zs), exitbody):build_clause(list/1, X, _, Zs, _, Pass), enterbody):build_clause(list/1, _, Ys, _, Passed, _), exitbody):build_goal(list/1, _, Hole, Passed, _), call):build_goal(list/1, Struct, _, _, Pass), exit):-
Abbildung 6.14: Separate Beschreibung der Invertierung einer Liste mittels einer Differenzstruktur.
1
require_aspects(reverse, [build(reverse, [list/1/last]),rename(list/1, reverse)]).
2
3
pointcut(clause(list([X|_]), _), _, reverse, elem(X)).
4
5
6
7
8
port_advice(reverse, init_clause(list/1, Rs), enterbody):Rs = [].
port_advice(reverse, build_clause(list/1, X, Rs1, Rs), exitbody):append(Rs1, [X], Rs).
Abbildung 6.15: Beschreibung der Invertierung einer Liste durch Anwendung der BuildTechnik.
1
2
require_aspects(reverse_unf, [build(reverse_unf, [list/1/last]),
unfold(append_dl/3), rename(list/1, reverse_dl)]).
3
4
pointcut(clause(list([X|_]), _), _, reverse_b, elem(X)).
5
6
7
8
9
port_advice(reverse_unf, init_clause(list/1, Rs), exitbody):Rs = Xs\Xs.
port_advice(reverse_unf, build_clause(list/1, X, Rs1, Rs), exitbody):append_dl(Rs1, [X|Xs]\Xs, Rs).
Abbildung 6.16: Um Differenzlisten erweiterte Beschreibung der Invertierung einer Liste.
79
6 Anwendungen
1
2
3
pointcut(goal(Goal), _, unfold(Pred/N), unfold(Goal, Head, Body)):nonvar(Goal), functor(Goal, Pred, N),
findall((Goal, Body), clause(Goal, Body), [(Head, Advice)]).
4
5
6
term_advice(unfold(_), unfold(Goal, Head, Body), unify(Goal, Head)).
port_advice(unfold(_), unfold(Goal, Head, Body), around):- Body.
Abbildung 6.17: Generischer Aspekt zur Entfaltung von Prädikaten.
Die Verwendung von append/3 führt bei diesem Ansatz allerdings zu einer doppelten
Rekursion und der damit verbundenen Ineffizienz. Jedoch lässt sich diese Implementation
als Ausgangspunkt für eine effizientere Implementierung mittels Differenzlisten nutzen.
Dazu wird der ursprüngliche Ansatz erweitert, indem Differenzlisten anstelle gewöhnlicher Listen verwendet werden [61]. Abbildung 6.16 zeigt einen Aspekt reverse unf/0,
welcher auf diese Art die Invertierung einer Liste beschreibt, ohne die Deklarativität des
naiven Ansatzes einzubüßen.
Zusätzlich kann das Auftreten von append dl/3 durch einen zusätzlichen Aspekt entfaltet werden. Im Beispiel geschieht dies durch die Verwendung des generischen Aspekts
unfold/1, dessen Implementation Abbildung 6.17 zeigt. Alternativ kann auch in Betracht gezogen werden, den vollständigen Übergang von gewöhnlichen Listen zu Differenzlisten als generischen Aspekt zu beschreiben.
6.2.5 Kontextpropagierung
Die Propagierung von Kontextinformationen ist eine weitere gängige Technik der PrologProgrammierung [85]. Die Kontextinformationen werden dabei entlang eines Skeletts in
einem zusätzlichen Argument propagiert. Weitere auf das Skelett angewandte Techniken
können diese Informationen dann für verschiedene Zwecke nutzen.
Die Klauseln der an der Propagierung beteiligten Prädikate lassen sich dabei in Stoppund Passklauseln unterteilen. Stoppklauseln enthalten keine an der Propagierung beteiligten Prädikate im Klauselkörper, weshalb die Propagierung in diesem Falle gestoppt
wird. Hingegen versorgen Passklauseln beteiligte Prädikate im Klauselkörper mit Kontextinformationen. Dabei werden durch ein zusätzliches Prädikat am Anfang des Klauselkörpers die Kontextinformationen aus dem Klauselkopf mit den zu propagierenden
Kontextinformationen in Verbindung gebracht. Des Weiteren lassen sich Ziele beteiligter Prädikate klassifizieren. Ziele innerhalb des Körpers von Passklauseln stellen Passziele dar. Hingegen treten Initialisierungsziele nicht in Klauseln beteiligter Prädikate
auf. Daher muss für eine korrekte Abarbeitung des Ziels das zusätzliche Kontextelement
initialisiert werden.
Beispiel
Abbildung 6.18 zeigt eine Erweiterung des Skeletts tree/1 zum Programm tree/2.
Das erweiterte Prädikat tree/2 propagiert im ersten Argument die Tiefe des aktuellen
80
6.2 Techniken
1
2
tree(nil).
tree(node(L, X, R)):- tree(L), tree(R).
3
4
5
tree(D, nil).
tree(D, node(L, X, R)):- NewD is D + 1, tree(NewD, L), tree(NewD, R).
Abbildung 6.18: Propagierung der Tiefe eines Knotens als Kontextinformation.
Knotens als Kontextinformation. In der Passklausel wird die aktuelle Tiefe inkrementiert, bevor sie an die rekursiven Aufrufe weitergereicht wird. Die Stoppklausel bleibt
ohne zusätzliche Prädikate im Klauselkörper. Die so entstandene Erweiterung kann nun
als Skelett für weitere Erweiterungen dienen, welche dann auf die propagierte Tiefeninformation zurückgreifen.
Implementation als generischer Aspekt
Die Beschreibung der Propagierung von Kontextinformationen als Aspekt kann durch
einen generischen Aspekt context/2 unterstützt werden, welcher typische Merkmale der
Technik implementiert und somit durch konkrete Aspekte nutzbar macht. Eine individuelle Anpassung auf einen Anwendungsfall erfolgt wie bei der Build-Technik durch einen
Namen und eine Liste der beteiligten Prädikate als Argumente des generischen Aspekts
sowie durch die Platzierung von Advices an den durch den Aspekt beschriebenen Pointcuts. Die durch den generischen Aspekt beschriebenen Pointcuts zeigt Tabelle 6.4.
Ein wesentliches Merkmal der Technik ist die Erweiterung beteiligter Prädikate um
ein Kontextargument. Abbildung 6.19 enthält die dazu nötigen Pointcut-Beschreibungen
und ein Term-Advice zur Platzierung des zusätzlichen Arguments. Ausgangspunkt bildet
die Beschreibung des Pointcuts val/2, welcher alle Klauseln und Ziele von Prädikaten
erfasst, welche an der Propagierung der Kontextinformationen beteiligt sind (Zeilen 1–3).
Alle Vorkommen solcher Prädikate werden dann durch ein Term-Advice um ein Argument erweitert.
Klauseln beteiligter Prädikate werden zusätzlich zu einem val/2-Pointcut von einem
Pointcut clause/3 erfasst. Dieser übernimmt beide Argumente des val/2-Pointcuts. Im
ersten Argument enthalten beide Pointcuts den Namen und die Arität des beteiligten
Prädikats. Das zweite Argument führt das dem Prädikat hinzuzufügende Kontextargument. Darüber hinaus weisen clause/3-Pointcuts ein weiteres Argument auf. Dieses
enthält die an die Prädikate im Klauselkörper zu propagierende Kontextinformation. In
der Pointcut-Beschreibung von val/2 wird geprüft, ob der aktuelle Joinpoint innerhalb
einer solchen Klausel liegt. Ist dies der Fall, ist der aktuelle Joinpoint ein Ziel innerhalb
des Körpers einer beteiligten Klausel. Somit enthält das letzte Argument des Pointcuts
clause/3 das zusätzliche Kontextargument für dieses Ziel. Anderenfalls ist der aktuelle Joinpoint entweder selbst eine Klausel oder aber ein Ziel außerhalb einer beteiligten
Klausel und muss um ein ungebundenes Argument erweitert werden (Zeilen 4, 5).
Auf der Basis der beiden Pointcut-Beschreibungen val/2 und clause/3 lassen sich
nun Pointcut-Beschreibungen zur Klassifizierung beteiligter Klauseln und Ziele erstellen.
81
6 Anwendungen
Pointcut
val(Pred/N, Context)
Erfasste Joinpoints
Ziele und Klauseln beteiligter Prädikate
clause(Pred/N, Context, NewContext)
Klauseln beteiligter
Prädikate
stop clause(Pred/N, Context)
Stoppklauseln
pass clause(Pred/N, Context, NewContext) Passklauseln
init goal(Pred/N, Context)
Initialisierungsziele
pass goal(Pred/N, Context)
Passziele
Tabelle 6.4: Generische Pointcuts einer Technik zur Kontextpropagierung.
1
2
3
4
5
pointcut(Joinpoint, Context, context(Name, Preds), val(Pred/N, C)):( Joinpoint = goal(Goal) ; Joinpoint = clause(Goal, _) ),
nonvar(Goal), functor(Goal, Pred, N), memberchk(Pred/N/_, Preds),
ignore( context(Context, inside(near),
context(Name, Preds), clause(_, _, C)) ).
6
7
8
pointcut(clause(_, _), Context, context(Name, Preds), clause(Pred/N, C, _NC)):context(Context, parallel, context(Name, Preds), val(Pred/N, C)).
9
10
11
term_advice(context(_, Preds), val(Pred/N, C), extra_arg(C, Pos)):memberchk(Pred/N/Pos, Preds).
12
13
14
15
pointcut(clause(_, _), Context, context(Name, Preds), stop_clause(Pred/N, C)):context(Context, parallel, context(Name, Preds), clause(Pred/N, C, _)),
\+ context(Context, beside(near), context(Name, Preds), val(_, _)).
16
17
18
19
20
21
pointcut(clause(_, _), Context, context(Name, Preds),
pass_clause(Pred/N, C, NewC)):context(Context, parallel,
context(Name, Preds), clause(Pred/N, C, NewC)),
context(Context, beside(near), context(Name, Preds), val(_, _)).
22
23
24
25
26
pointcut(goal(_), Context, context(Name, Preds), init_goal(Pred/N, C)):context(Context, parallel, context(Name, Preds), val(Pred/N, C)),
\+ context(Context, inside(near),
context(Name, Preds), clause(_, _, _)).
27
28
29
30
pointcut(goal(_), Context, context(Name, Preds), pass_goal(Pred/N, C)):context(Context, parallel, context(Name, Preds), val(Pred/N, C)),
context(Context, inside(near), context(Name, Preds), clause(_, _, _)).
Abbildung 6.19: Generische Pointcut-Beschreibungen zur Klassifizierung von Klauseln
und Zielen, welche Kontextinformationen propagieren.
82
6.2 Techniken
1
require_aspects(depth, context(depth, [tree/1/first])).
2
3
4
5
6
port_advice(depth, pass_clause(tree/1, D, NewD), enterbody):NewD is D + 1.
port_advice(depth, init_goal(tree/1, D), call):D is 0.
Abbildung 6.20: Separate Beschreibung der Propagierung der Tiefe eines Knotens als
Aspekt.
Stoppklauseln werden durch einen Pointcut stop clause/2 erfasst, Passklauseln durch
pass clause/3. Beide Pointcuts erfassen beteiligte Klauseln und lassen sich anhand des
nebengeordneten Kontextes unterscheiden. In Stoppklauseln sind in diesem Kontext keine beteiligten Prädikate und somit keine durch val/2-Pointcuts erfasste Ziele enthalten
(Zeile 15). In Passklauseln ist gerade dies der Fall (Zeile 21).
Die Pointcut-Beschreibungen init goal/2 und pass goal/2 klassifizieren auf ähnliche Art beteiligte Ziele. Dabei erfasst init goal/2 beteiligte Ziele außerhalb beteiligter
Klauseln. Solche Initialisierungsziele müssen die zu propagierenden Kontextinformationen zur Verfügung stellen. Dies kann durch individuelle Advices geschehen, welche am
Call -Port dieser Ziele platziert werden. pass goal/2-Pointcuts erfassen im Gegensatz
hierzu beteiligte Ziele innerhalb beteiligter Klauseln. An ihnen werden für gewöhnlich
keine Advices platziert. Sie können aber bei der Beschreibung weiterer Pointcuts von
Nutzen sein.
Anwendung
Die Propagierung der Tiefe eines Baumes als Kontextinformation durch das Prädikat tree/2 aus Abbildung 6.18 lässt sich als separater Aspekt beschreiben. Dieser in
Abbildung 6.20 gezeigte Aspekt baut auf dem zuvor vorgestellten generischen Aspekt
context/2 auf und platziert an den durch diesen beschriebenen Pointcuts Advices. Ein
am Enterbody-Port der Passklausel platziertes Advice sorgt für die Inkrementierung der
zu propagierenden Kontextinformation (Zeilen 3, 4). Ein weiteres Advice wird am Call Port von Initialisierungszielen platziert. Dieses stellt sicher, dass die Tiefeninformation
bei Aufrufen des erweiterten Prädikats korrekt initialisiert wird (Zeilen 5, 6).
Kontextinformationen und Build-Technik
Bei der Anwendung der Build-Technik werden die Pointcuts des generischen Aspekts
build/2 genutzt, um aus Strukturen aus dem Klauselkörper eine Struktur im Klauselkopf zu konstruieren. Dieser Prozess lässt sich unter Verwendung derselben Pointcuts
auch invers betreiben. Dazu werden die Strukturen im Klauselkörper aus der Struktur
im Klauselkopf gebildet. Dieser Prozess kann sowohl eine Destruktion der Struktur des
Klauselkopfes sein als auch eine Konstruktion neuer Strukturen.
Auf diese Weise lassen sich unterschiedliche Kontextinformationen an verschiedene
Prädikate im Klauselkörper propagieren. So propagiert der Aspekt path/0 aus Abbil-
83
6 Anwendungen
1
require_aspects(path, build(path, [tree/1/first])).
2
3
4
5
6
7
port_advice(path, build_clause(tree/1, Left, Right, P), enterbody):append(P, [left], Left),
append(P, [right], Right).
port_advice(path, init_goal(tree/1, Path), call):Path = [].
Abbildung 6.21: Separate Beschreibung der Propagierung eines Baumpfades.
dung 6.21 einen Pfad von der Wurzel des Baumes zum aktuellen Knoten als Kontextinformation. Für die rekursiven Aufrufe wird hierzu der aktuelle Pfad jeweils erweitert (Zeilen 3–5). Bei Aufrufen des Prädikats außerhalb der zu erweiternden Klauseln
wird der Pfad mit einer leeren Liste initialisiert (Zeilen 6, 7).
6.3 Sprachprozessoren
In diesem Abschnitt wird eine Unterstützung der Entwicklung von Prototypen für Sprachprozessoren durch generische Aspekte vorgestellt. Als Beispiele werden auf aspektorientierte Weise ein Parser und ein Pretty-Printer für die Sprache Pico entwickelt. Bei dieser
handelt es sich um eine kleine imperative Programmiersprache, welche vor allem Testund Demonstrationszwecken für die ASF+SDF Meta-Environment [11, 48] dient.
6.3.1 Implementation von Sprachprozessoren in Prolog
Prolog ist seit seiner Entwicklung eine beliebte Implementationssprache für Sprachprozessoren wie Compiler [92, 13, 18, 94] oder Interpreter [12, 83]. Darüber hinaus lassen sich
verschiedene Formalismen zur Beschreibung von Sprachprozessoren in Prolog umsetzen.
Hierzu gehören reguläre Ausdrücke [69], kontextfreie Grammatiken [73], Attributgrammatiken [21, 22, 77, 71], denotationale Semantikbeschreibungen [13] und algebraische
Spezifikationen [24].
Des Weiteren wird Prolog durch seine Ausdrucksstärke und metalogischen Möglichkeiten häufig zur Entwicklung von Prototypen verwendet. Dies legt die Verwendung Prologs
zur Entwicklung von Prototypen für Sprachprozessoren auf Basis solcher Beschreibungen nahe [12, 75, 10, 57]. Systeme wie LDL [76] und dessen Nachfolger Laptob [58]
unterstützen diesen Prozess.
6.3.2 Parser
Logische Grammatiken
Als Ausgangspunkt für die Entwicklung eines Parsers dient eine logische Grammatik.
In Anlehnung an die Notation in Laptob [58] werden Grammatikregeln als Klauseln
eines Prolog-Programms repräsentiert. Nichtterminale und Morphemklassen werden als
Prädikate kodiert. Nichtterminale werden durch gewöhnliche Zeichenketten notiert.
84
6.3 Sprachprozessoren
1
program:- "begin", decls, statements, "end".
2
3
decls:- "declare", idtypes, ";".
4
5
6
idtypes.
idtypes:- idtype, rem_idtypes.
7
8
idtype:- id, ":", type.
9
10
11
rem_idtypes.
rem_idtypes:- ",", sep_idtypes.
12
13
14
statements.
statements:- statement, rem_statements.
15
16
17
18
statement:- id, ":=", exp.
statement:- "if", exp, "then", statements, "else", statements, "fi".
statement:- "while", exp, "do", statements, "od".
19
20
21
rem_statements.
rem_statements:- ";", sep_statements.
22
23
24
exp:- simple_exp.
exp:- simple_exp, bop, exp.
25
26
27
28
simple_exp:- id.
simple_exp:- nat_con.
simple_exp:- string_con.
29
30
31
32
bop:- "+".
bop:- "-".
bop:- "||".
33
34
35
type:- "natural".
type:- "string".
Abbildung 6.22: Logische Grammatik der Programmiersprache Pico als PrologProgramm.
Eine Beschreibung der Pico-Grammatik im Syntaxdefinitionsformalismus SDF [33]
findet sich in der Grammar Base des XT-Projektes für Programm-Transformationen [1]
Programm-Transformationen. Abbildung 6.22 zeigt eine um Linksrekursionen und EBNFKonstrukte bereinigte Variante als logische Grammatik. Dieses bildet die Grundlage für
die im Folgenden vorgestellten Sprachprozessoren.
Scanner-Anbindung
Um die logische Grammatik aus Abbildung 6.22 zum Parsen von Pico-Programmen
verwenden zu können, muss ein Eingabestrom durch einen Scanner in Token zerlegt
werden und diese anstelle der Auftreten von Morphemklassen und Terminalsymbolen
85
6 Anwendungen
1
require_aspects(token(Name), Name).
2
3
4
5
6
pointcut(goal(Goal), Context, token(Name), token(Token)):context(Context, parallel, Name, morphem), functor(Token, Goal, 1).
pointcut(goal(List), _, token(_), token(kw(KW))):is_list(List), string_to_list(KW, List).
Abbildung 6.23: Generische Pointcut-Beschreibung für Token einer logischen Grammatik.
1
2
3
pointcut(goal(id), _, pico, morphem).
pointcut(goal(nat_con), _, pico, morphem).
pointcut(goal(string_con), _, pico, morphem).
Abbildung 6.24: Pointcut-Beschreibung für Morphemklassen einer logischen Grammatik.
konsumiert werden. Laptob [58] bietet verschiedene Scanner-Implementationen, welche
beispielsweise auf der Simulation endlicher Automaten [69] oder auf der Anbindung von
flex [30] über die C-Schnittstelle von Prolog basieren. Beide Varianten haben gemeinsam,
dass sie als Schnittstelle ein Prädikat get token/1 bieten, welches das aktuelle Token als
Term liefert und volles Backtracking unterstützt. Ähnliche Lösungen sind auch außerhalb
Laptobs realisierbar.
Für die Anbindung des Scanners werden zunächst Pointcut-Beschreibungen für Positionen benötigt, an denen Token konsumiert werden sollen. Abbildung 6.23 zeigt einen
Aspekt token/1, welcher lediglich aus einer entsprechenden generischen Pointcut-Beschreibung token/1 besteht. Als Argument referenziert diese das zu konsumierende
Token. Die Generizität der Beschreibung ist in der Unkenntnis der Morphemklassen
verschiedener Grammatiken begründet. Während Terminale durch ihre Repräsentation
als Liste von Zeichen in jeder Grammatik erkannt werden können, nutzt die generische
Beschreibung Pointcuts morphem/0 der jeweiligen Grammatik zur Identifizierung von
Prädikaten, welche Morphemklassen repräsentieren. Abbildung 6.24 enthält die entsprechenden Deklarationen für die Pico-Grammatik.
Ein einfacher generischer Aspekt parser/1 kann nun zur Implementation einer Technik der Anbindung des Scanners definiert werden. Dieser Aspekt übernimmt sämtliche
Pointcuts des Aspekts token/1 und platziert an diesen ein Around -Advice, welches ein
Token vom Scanner anfordert. Den vollständigen Aspekt präsentiert Abbildung 6.25.
1
require_aspects(parser(Name), token(Name)).
2
3
4
pointcut(goal(_), Context, parser(Name), Pointcut):context(Context, parallel, token(Name), Pointcut).
5
6
port_advice(parser(_), token(Token), around):- get_token(Token).
Abbildung 6.25: Technik zur Scanner-Anbindung.
86
6.3 Sprachprozessoren
Konstruktion eines abstrakten Syntaxbaums
Für die meisten Anwendungen ist es notwendig, die Struktur eines eingelesenen Programms zu kennen. Als Repräsentationsform solcher Strukturen haben sich abstrakte
Syntaxbäume bewährt [3]. Die Konstruktion eines abstrakten Syntaxbaums kann als
Technik zur Entwicklung von Parsern betrachtet werden. Diese Technik ähnelt dabei in
weiten Teilen der Build-Technik, wie sie in Abschnitt 6.2.2 vorgestellt wurde.
Abbildung 6.26 zeigt eine Implementation der Technik als generischen Aspekt ast/1,
welcher auf der Implementation der Build-Technik aufbaut. An der Konstruktion des
Baums sind zunächst alle Nichtterminale und somit alle Klauseln einer logischen Grammatik beteiligt (Zeilen 1–3). Zusätzlich müssen jedoch Token, welche Morpheme repräsentieren, bei der Konstruktion berücksichtigt werden. Daher werden zusätzliche
val/1-Pointcuts für die Build-Technik definiert (Zeilen 5–7).
Die Technik lässt sich nun zur Konstruktion von abstrakten Syntaxbäumen für PicoProgramme verwenden. Abbildung 6.27 zeigt einen Ausschnitt aus entsprechenden Pointcut-Beschreibungen und Advices. Der Ausschnitt beschränkt sich auf die Konstruktion
von Teilbäumen für Anweisungsfolgen und Anweisungen. Teilbäume für Anweisungsfolgen werden mit einer leeren Liste initialisiert (Zeilen 4, 5). Im rekursiven Fall wird eine
Liste aus dem Teilbaum der Anweisung und der Restliste der Anweisungsfolge aus dem
rekursiven Aufruf konstruiert (Zeilen 6–8).
Da Zuweisung und while-Anweisung die gleiche Anzahl von Teilstrukturen aufweisen,
lassen sie sich die Pointcuts der Build-Technik, welche die jeweilige Klausel erfassen, nicht
ohne weiteres unterscheiden. Zusätzliche elem/1-Pointcuts lösen das Problem durch eine Unterscheidung anhand des ersten Prädikataufrufs im Klauselkörper (Zeilen 10–12).
Das Argument eines elem/1-Pointcuts tritt dann in den Pointcuts der Build-Technik erneut auf, worüber die Fälle unterschieden und die entsprechenden Teilbäume konstruiert
werden können (Zeilen 14–19).
Semantische Analyse
Analog zur Implementation der Konstruktion von abstrakten Syntaxbäumen lassen sich
die in Abschnitt 6.2 vorgestellten Techniken zur separaten Beschreibung weiterer Aspekte eines Parsers verwenden. So lässt sich die Konstruktion einer Symboltabelle mittels
der Build- oder der Akkumulator-Technik beschreiben. Eine derart konstruierte Tabelle
kann dann als Kontext propagiert und zur Typüberprüfung (Build-Technik) verwendet
werden.
Auf diese Art entwickelte Aspekte sind in hohem Grade kombinier- und wiederverwendbar. So lässt sich beispielsweise durch Einweben des Aspekts parser/1 aus Abbildung 6.25 und eines Aspekts zur Typüberprüfung in die Pico-Grammatik ein einfacher
Parser generieren, welcher Programme auf syntaktische Korrektheit und Wohlgetyptheit
überprüft. Andererseits lässt sich aus der Pico-Grammatik auch ein Programm ableiten,
welches abstrakte Syntaxbäume auf Wohlgetyptheit untersucht. Dazu wird der generische Aspekt adt/1 aus Abbildung 6.28, der Aspekt ast/1 aus den Abbildungen 6.26
und 6.27 und ein Aspekt zur Typüberprüfung eingewoben.
87
6 Anwendungen
1
2
3
require_aspects(ast(Name), [token(Name), Name, build(ast(Name), Preds)]):nonterminals(Name, Nonterminals),
findall(Nonterminal/0/last, member(Nonterminal, NTs), Preds).
4
5
6
7
pointcut(goal(Pred), Context, build(ast(Name), _), val(Pred/0, Token)):context(Context, parallel, Name, morphem),
context(Context, parallel, token(Name), token(Token)).
Abbildung 6.26: Ableitung einer Technik zur Verwendung abstrakter Syntaxbäume aus
der Build-Technik.
1
2
nonterminals(pico, [program, decls, idtypes, rem_idtypes, idtype, statements,
rem_statements, statement, exp, simple_exp, bop, type]).
3
4
5
6
7
8
term_advice(ast(pico), init_clause(Pred, Statements), unify(Statements, [])):memberchk(Pred, [statements/0, rem_statements/0]).
term_advice(ast(pico), build_clause(Pred, Statement, RemStatements, Statements),
unify(Statements, [Statement|RemStatements])):memberchk(Pred, [statements/0, rem_statements/0]).
9
10
11
12
pointcut(clause(statement, (id, _)), _, ast(pico), elem(assign)).
pointcut(clause(statement, ("if", _)), _, ast(pico), elem(if)).
pointcut(clause(statement, ("while", _)), _, ast(pico), elem(while)).
13
14
15
16
17
18
19
term_advice(ast(pico), build_clause(statement/0, assign, Id, Exp, St),
unify(St, assign(Id, Exp))).
term_advice(ast(pico), build_clause(statement/0, if, Exp, IfSts, ElseSts, St),
unify(St, if(Exp, IfSts, ElseSts))).
term_advice(ast(pico), build_clause(statement/0, while, Exp, WhileSts, St),
unify(St, while(Exp, WhileSts))).
20
21
22
23
term_advice(ast(pico), build_clause(exp/0, SExp, Exp), unify(Exp, SExp)).
term_advice(ast(pico), build_clause(exp/0, Exp1, Bop, Exp2, Exp),
unify(Exp, bop(Bop, Exp1, Exp2))).
24
25
term_advice(ast(pico), build_clause(simple_exp/0, SExp, Exp), unify(Exp, SExp)).
26
27
28
29
pointcut(clause(bop, "+"), _, ast(pico), elem(plus)).
pointcut(clause(bop, "-"), _, ast(pico), elem(minus)).
pointcut(clause(bop, "||"), _, ast(pico), elem(concat)).
30
31
term_advice(ast(pico), init_clause(bop/0, Bop1, Bop2), unify(Bop1, Bop2)).
Abbildung 6.27: Anwendung der ast/1-Technik zur Konstruktion von abstrakten Syntaxbäumen für Pico-Programme (Ausschnitt).
88
6.3 Sprachprozessoren
1
require_aspects(parser(Name), token(Name)).
2
3
4
pointcut(goal(_), Context, parser(Name), Pointcut):context(Context, parallel, token(Name), Pointcut).
5
6
port_advice(parser(_), token(Token), around).
Abbildung 6.28: Technik zur Entfernung von Tokenbezügen aus einer logischen Grammatik.
Der Aspekt adt/1 löscht Positionen von Token in einer logischen Grammatik. Durch
Anwendung des Aspekts auf eine logische Grammatik entsteht eine Beschreibung eines Datentyps für abstrakte Syntaxbäume der durch die Grammatik beschriebenen
Sprache. Eine zusätzliche Anwendung eines Aspekts zur Konstruktion eines Baums
liefert ein Programm, welches solche abstrakten Syntaxbäume vollständig traversiert.
Dieses Programm kann durch weitere Aspekte für verschiedene Anwendungsfälle erweitert werden. Neben der angesprochenen Typüberprüfung sind Programmanalysen und
-transformationen weitere typische Anwendungen. Zur Implementation entsprechender
Aspekte kann dabei wiederum auf die in Abschnitt 6.2 beschriebenen Techniken zurückgegriffen werden.
6.3.3 Unparser
Als zweites Beispiel zur aspektorientierten Entwicklung von Sprachprozessoren wird in
diesem Unterabschnitt ein Unparser für Pico-Programme erarbeitet. Ein solcher Unparser traversiert einen abstrakten Syntaxbaum und gibt die konkrete Syntax des durch
den Baum repräsentierten Baums aus.
Eine einfache Ausgabe des Programms lässt sich durch die Anwendung des Aspekts
ast/1 und des generischen Aspekts unparser/1 in die Pico-Grammatik realisieren. Der
Aspekt unparser/1 in Abbildung 6.29 stellt ein Gegenstück zu parser/1 aus Abbildung 6.25 dar. Er ersetzt Auftreten von Token durch Aufrufe des Prädikats put token/1,
welches für die Ausgabe des jeweiligen Token und Trennzeichen zwischen diesen verantwortlich ist.
Die Verwendung von put token/1 zur Trennung von Token im Ausgabestrom wird
Aufgaben wie Einrückungen, Blockbildung und Zeilenumbrüche nur ungenügend ge1
require_aspects(unparser(Name), token(Name)).
2
3
4
pointcut(goal(_), Context, unparser(Name), Pointcut):context(Context, parallel, token(Name), Pointcut).
5
6
7
port_advice(unparser(_), token(Token), around):put_token(Token).
Abbildung 6.29: Technik zur Ausgabe von Token.
89
6 Anwendungen
1
2
indents(pico, [decls, idtypes, rem_idtypes, statements, rem_statements,
statement]).
3
4
5
6
pointcut(clause(decls, _), _, pico, block).
pointcut(clause(idtypes, _), _, pico, block).
pointcut(clause(statements, _), _, pico, block).
7
8
9
10
11
pointcut(goal(statements), _, pico, nl).
pointcut(goal(","), _, pico, nl).
pointcut(goal(";"), _, pico, nl).
pointcut(goal("end"), _, pico, nl).
12
13
14
15
pointcut(goal(bop), _, pico, space_before).
pointcut(goal("then"), _, pico, space_before).
pointcut(goal("do"), _, pico, space_before).
16
17
18
19
20
21
pointcut(goal(bop), _, pico, space_after).
pointcut(goal(":"), _, pico, space_after).
pointcut(goal(":="), _, pico, space_after).
pointcut(goal("if"), _, pico, space_after).
pointcut(goal("while"), _, pico, space_after).
Abbildung 6.30: Pointcut-Beschreibungen zum Pretty-Printing von Pico-Programmen.
recht, da diese oft vom Auftreten eines Tokens abhängig sind. Eine Alternative stellt die
Anwendung einer generischen Pretty-Print-Technik dar. Abbildung 6.30 zeigt individuelle Pointcut-Beschreibungen für die Pico-Grammatik, auf welche die generische Technik
dann zurückgreifen kann.
Das Prädikat indents/2 spezifiziert Nichtterminale, deren Ausgaben eingerückt werden. Der Pointcut block/0 umfasst Klauseln, deren Ausgaben in einem Block gemeinsam
eingerückt werden. Die Pointcuts nl/0, space before/0 und space after/0 erfassen
Ziele, nach deren Abarbeitung ein Zeilenumbruch erfolgt bzw. vor oder nach deren Abarbeitung ein Leerzeichen ausgegeben wird.
Abbildung 6.31 enthält eine Implementation der generischen Pretty-Print-Technik
pp/2. Das erste Argument der Technik dient der Unterscheidung verschiedener Grammatiken. Das zweite Argument spezifiziert die Einrückungstiefe eines Blocks. Die Implementation basiert auf der Anwendung der Technik zur Kontextpropagierung aus Abschnitt 6.2.5.
Die aktuelle Einrückungstiefe wird für von Einrückungen betroffene Prädikate als
Kontext geführt (Zeilen 1–3). Dabei wird die Tiefe mit 0 initialisiert (Zeile 12). Die an
der Propagierung beteiligten Klauseln werden unterschieden, ob deren Ausgabe in einem neuen Block eingerückt (Zeilen 5–7) oder der umgebene Block weiter genutzt wird
(Zeilen 8–10). Im ersten Fall wird ein Advice am Enterbody-Port der Klausel platziert,
welches die Einrücktiefe erhöht, einen Zeilenumbruch einfügt und die folgenden Ausgaben mit der neuen Einrücktiefe einrückt (Zeilen 13, 14). Im zweiten Fall bleibt die
Einrücktiefe unverändert (Zeile 15).
Bei der Einfügung von einfachen Zeilenumbrüchen muss beachtet werden, ob die ak-
90
6.3 Sprachprozessoren
1
2
3
require_aspects(pp(Name, _), [unparser(Name), Name, context(indent, Preds)]):indents(Name, Indents),
findall(Indent/0/last, member(Indent, Indents), Preds).
4
5
6
7
8
9
10
pointcut(clause(_, _), Context, pp(Name, _), block(I, NewI)):context(Context, parallel, context(indent, _), pass_clause(_, I, NewI)),
context(Context, parallel, Name, block).
pointcut(clause(_, _), Context, pp(Name, _), no_block(I, NewI)):context(Context, parallel, context(indent, _), pass_clause(_, I, NewI)),
\+ context(Context, parallel, Name, block).
11
12
13
14
15
term_advice(context(indent, _), init_goal(_, I), unify(I, 0)).
port_advice(pp(_, Indent), block(I, NewI), enterbody):NewI is I + Indent, nl, tab(NewI).
term_advice(pp(_, _), no_block(I, NewI), unify(I, NewI)).
16
17
18
19
20
21
22
23
24
25
26
pointcut(goal(_), Context, pp(Name, _), nl_indent(I)):context(Context, parallel, Name, nl),
context(Context, inside(near), context(indent, _), pass_clause(_, I)).
pointcut(pp(Name, _), goal(_), Context, nl_no_indent):context(Context, parallel, Name, nl),
\+ context(Context, inside(near), context(indent, _), pass_clause(_, _)).
pointcut(pp(Name, _), goal(_), Context, space_before):context(Context, parallel, Name, space_before).
pointcut(pp(Name, _), goal(_), Context, space_after):context(Context, parallel, Name, space_after).
27
28
29
30
31
port_advice(pp(_,
port_advice(pp(_,
port_advice(pp(_,
port_advice(pp(_,
_),
_),
_),
_),
nl_indent(I), exit):- nl, tab(I).
nl_no_indent, exit):- nl.
space_before, call):- tab(1).
space_after, exit):- tab(1).
Abbildung 6.31: Pretty-Printing-Technik.
tuelle Klausel Einrückungen vornimmt (Zeilen 17–22). Ist dies der Fall, enthält das am
Exit-Port eines Ziels platzierte Advice zusätzlich zum Zeilenumbruch ein Prädikatsaufruf, welcher die Einrückung realisiert (Zeile 28). Ansonsten besteht das Advice lediglich
aus einem Zeilenumbruch (Zeile 29). Die Pointcuts für Leerzeichen übernimmt der generische Aspekt (Zeilen 23–26) und platziert am Call - bzw. Exit-Port der betroffenen
Ziele ein Advice zur Ausgabe des Leerzeichens (Zeilen 30, 31).
91
7 Abschließende Betrachtungen
7.1 Ergebnisse der Arbeit
Die vorliegende Arbeit hat gezeigt, dass auch in der Programmierung mit Prolog Concerns auftreten, welche die Implementation anderer Concerns crosscutten und sich nicht
separat modularisieren lassen. Mit den Port-Annotationen und der Methode des Stepwise
Enhancements wurden Ansätze vorgestellt, welche die Modularisierung solcher Crosscutting Concerns ermöglichen und somit die wesentliche Zielstellung der Aspektorientierung verfolgen.
Des Weiteren wurde gezeigt, dass sich auf Basis eines erweiterten Port-Modells objektorientierte Ideen zur Beschreibung von Aspekten anhand von Pointcuts und Advices
auf Prolog übertragen lassen. Als Ergebnis dieser Überlegungen wurde ein statisches
Joinpoint-Modell vorgestellt. Als erster Anwendungsfall wurde die separate Beschreibung von Spezifikationen anhand von Vor- und Nachbedingungen vorgestellt.
Im nächsten Schritt wurde das Modell verwendet, um Techniken, welche vom Stepwise
Enhancement bekannt waren, formal zu beschreiben. Damit wurde eine offene Problematik dieser Methodik gelöst und der enge Zusammenhang zwischen Stepwise Enhancement
und aspektorientierter Programmierung in Prolog nochmals unterstrichen. Ferner wurde die modulare Beschreibung von Sprachprozessoren als weiterer Anwendungsfall des
Modells hervorgehoben. Auch hier war die Implementation von Techniken als generische
Aspekte ein zentraler Bestandteil der Betrachtungen.
7.2 Weiterführende Arbeiten
Ausgehend von den in dieser Arbeit präsentierten Ansätzen lassen sich diese weiterverfolgen, präzisieren, sowie weitere Fragestellungen der Aspektorientierung in Prolog,
in der logischen Programmierung und im deklarativen Paradigma untersuchen. Dieser
Abschnitt zeigt mögliche Entwicklungsrichtungen auf.
7.2.1 Konzeptuelle Weiterentwicklung
Das in Kapitel 5 vorgestellte Joinpoint-Modell liefert einen ersten Anhaltspunkt zur
Beschreibung logischer Aspekte. Dennoch lässt dieses Fragen wie die Behandlung von
Metaprädikaten und deren Argumenten oder die Berücksichtigung von Alternativen im
Kontext eines Joinpoints offen.
Eine vollständige Formalisierung des Modells kann diese Fragen unter Umständen
klären. Aus einer formalen Beschreibung des Modells ergeben sich daraufhin weitere
92
7.2 Weiterführende Arbeiten
interessante Ansatzpunkte. Hier ist vor allem der Zusammenhang zu Abbildungen zwischen logischen Programmen [47, 59] als zentraler Punkt hervorzuheben. Gelingt es,
hier einen Bezug herzustellen, lassen sich Aussagen über die Semantikerhaltung von
Aspekten und die Korrektheit verwobener Programme treffen, wie dies für das Stepwise
Enhancement bereits möglich ist [37].
Des Weiteren lässt sich das vorgestellte Modell in verschiedene Richtungen weiterentwickeln. Zunächst kann dabei an eine Erweiterung des statischen Modells gedacht
werden. Typische Bestandteile eines Aspekts wie die Definition zusätzlicher Prädikate
oder zusätzlicher Klauseln für bereits definierte Prädikate werden bisher unzureichend
berücksichtigt. Darüber hinaus kann das Modell auch Ansätze für die Entwicklung eines
dynamischen Joinpoint-Modells ähnlich dem der Port-Annotationen liefern.
Die Methode des Stepwise Enhancement ist nicht nur auf Prolog beschränkt, sondern allgemein zur Entwicklung von Programmen in verschiedenen Ausprägungen der
logischen Programmierung geeignet [46]. Es ist zu untersuchen, ob das vorgestellte Modell aspektorientierte logische Programmierung in gleicher allgemeiner Weise unterstützt
und wie Erweiterungen des Modells für andere logische Ansätze aussehen können. Der
Zusammenhang zu anderen deklarativen Ansätzen [54] ein weiterer Forschungspunkt.
Die Beschreibung von Aspekten als Metaprogramme [54] wurde durch die Arbeit bereits als alternatives Konzept identifiziert. Eine konkrete Anwendung dieses Konzepts
auf Prolog sowie weitere alternative Konzepte zur aspektorientierten Programmierung in
Prolog sind denkbar. Vergleichende Untersuchungen der verschiedenen Konzepte können
Aufschlüsse über deren Stärken und Schwächen liefern. Teil dieser Überlegungen sollte es auch sein, inwiefern bereits existierende Methoden zur Entwicklung von PrologProgrammen aspektorientierte Ansätze beinhalten oder zu aspektorientierten Methoden
erweitert werden können. Letztlich ist auch die Übertragung weiterer aspektorientierter Ansätze aus der objektorientierten Programmierung weiterzuverfolgen. Besonders
herausfordernd ist hierbei ein Transfer der Method-Call Interception zu einer Predicate
Port Interception. Ansätze hierzu kann ein dynamisches Joinpoint-Modell sowie dessen
Implementation als Meta-Interpreter liefern.
7.2.2 Tool-Support
Bei der Entwicklung von AspectJ wurde eine Erweiterung existierender Java-Tools für
die neuen aspektorientierten Sprachkonstrukte und Konzepte angestrebt [44]. Hauptziel
war es dabei, die Benutzerakzeptanz und die Praktikabilität der Sprache zu fördern.
Diese Überlegungen lassen sich auch auf aspektorientierte Erweiterungen Prologs übertragen. Als entscheidende Tools stehen dabei Editoren und Debugger im Mittelpunkt
der Betrachtungen.
Editoren
SWI-Prolog [93] bietet einen anspruchsvollen graphischen Editor, welcher Abhängigkeiten zwischen Prädikaten und Modulen visualisiert. Eine Erweiterung dieses Editors
93
7 Abschließende Betrachtungen
oder die Entwicklung neuer Editoren kann aspektorientierte Programmierung in Prolog
entscheidend unterstützen.
Wünschenswert ist hierbei die Integration des Webers in den Editor, so dass Komponenten, Aspekte und verwobenes Programm innerhalb einer Umgebung bereitgestellt
werden. Die Struktur des Gesamtprogramms lässt sich dabei analog zu der Program
Enhancement Structure [38] eines mittels der Methode Stepwise Enhancement entwickelten Programms visualisieren.
Eine Visualisierung von Aspektabhängigkeiten ist ähnlich zu der von Modulabhängigkeiten möglich. In Kombination mit einer Visualisierung von Pointcut-Beschreibungen
und -Abhängigkeiten kann die Entwicklung von Aspekten entscheidend unterstützt werden. Es gilt, Modelle zur Visualisierung zu entwickeln und in die Dokumentation von
Aspekten einfließen zu lassen.
Darüber hinaus lässt sich die Programmierung von Aspekten durch Informationen
über Pointcuts, welche einen Prädikataufruf oder eine Klausel erfassen, erleichtern. Der
Anwender kann so beispielsweise per Maus abfragen, von welchen Pointcuts ein bestimmtes Ziel erfasst wird und an einem für ihn relevanten Pointcut ein Advice platzieren. Eine
ähnliche Unterstützung für Techniken des Stepwise Enhancements existiert als Prototyp [52]. Ein Nachteil hierbei ist, dass die Unterstützung der Techniken fest im Editor
implementiert ist und nicht von einer Beschreibung dieser abhängt. Daher ist für die
Unterstützung aspektorientierter Entwicklung von Prolog-Programmen zu untersuchen,
ob ein Editor generische Aspekte anhand deren Pointcut-Beschreibungen und Advices
generisch unterstützen kann.
Debugger
SWI-Prolog [93] enthält ein graphisches Frontend zum Debuggen von Programmen,
was den Debugging-Prozess entscheidend erleichtert. Dieses beinhaltet die Möglichkeit,
Prädikatdefinitionen einzusehen, den Stack zu inspizieren sowie die aktuelle Belegung
von Variablen zu verfolgen.
Für das Debuggen von verwobenen Programmen muss ein solcher Debugger entsprechend erweitert werden. So sollte die Möglichkeit bestehen, Advices vom ursprünglichen
Code der Komponente zu unterscheiden. Erklärungskomponenten, warum ein Advice an
einem bestimmten Joinpoint platziert wurde, können dabei das Verständnis entscheidend
fördern.
Weiterhin ist die Entwicklung eines interaktiven Webers für das Debugging von Pointcut-Beschreibungen hilfreich. Ein solcher interaktiver Weber kann Aufschluss darüber
geben, warum ein Joinpoint von einem Pointcut erfasst oder ausgelassen wird. Dies liefert neben der bereits angesprochenen Visualisierung einen wichtigen Fortschritt für die
Fehlersuche in Pointcut-Beschreibungen.
7.2.3 Anwendungen
Im Rahmen dieser Arbeit wurden einige Anwendungsfälle aspektorientierter PrologProgrammierung skizziert. Überdies wurden aspektorientierte Implementationen kom-
94
7.2 Weiterführende Arbeiten
plexerer Anwendungen wie Sprachprozessoren vorgestellt.
Anforderungen von Anwendungen an aspektorientierte Konzepte
Die Erstellung weiterer Anwendungen ist zur endgültigen Beurteilung des aspektorientierten Ansatzes im Allgemeinen und des vorgestellten Modells im Speziellen zwingend
notwendig. Insbesondere größere Anwendungen können zeigen, ob das Vorgehen die erhofften Vorteile bringt und ob verschiedene aspektorientierte Konzepte praktikabel sind.
Die aspektorientierte Implementation solcher Anwendungen kann zudem wertvolle Hinweise für Anforderungen an ein aspektorientiertes Konzept liefern. Neue Anforderungen
können so in bestehende Konzepte integriert werden. Ebenso lassen sich überflüssige
Bestandteile reduzieren.
So ist beispielsweise für das vorgestellte Joinpoint-Modell unklar, ob die Platzierung
von Advices an rückwärtsgerichteten Ports praktisch relevant ist. Die in dieser Arbeit
präsentierten Beispiele nutzen lediglich Advices an vorwärtsgerichteten Ports, around und Term-Advices. Dies kann in einer Prägung durch objektorientierte Ansätze, welchen
das Konzept des Backtrackings nicht kennen, begründet sein. Eventuell kann die Platzierung von Advices an diesen Ports für spezielle Aufgaben wie beispielsweise Tracing oder
Debugging nützlich sein. Genauso ist es aber auch möglich, dass rückwärtsgerichtete
Ports in einer aspektorientierten Programmentwicklung keine Rolle spielen.
Design by Contract
In Abschnitt 6.1 wurde an einem kleinen Beispiel gezeigt, wie sich mit Hilfe von Advices
Spezifikationsfragmente in Form von Vor- und Nachbedingungen separat beschreiben
lassen. Hier ist in weiteren Untersuchungen zu prüfen, inwiefern sich solche Spezifikationen zur statischen Typ- und Modi-Prüfung von Prolog-Programmen [58] bzw. von
Zusicherungen im Allgemeinen [68] verwenden lassen. Eventuell kann eine solche statische Überprüfung durch generische Pointcuts, welche fehlerhafte Joinpoints erfassen,
beschrieben werden. Weitere statische Semantiktests sind ebenfalls denkbar.
Sprachprozessoren
Während der Erstellung dieser Arbeit wurden neben den in Abschnitt 6.3 vorgestellten Sprachprozessoren für die Sprache Pico weitere solcher Prozessoren implementiert.
Die Entwicklung für komplexere Beispielsprachen und -anwendungen ist ein zusätzliches Forschungsthema. Dabei gilt es zu untersuchen, inwiefern sich bestimmte Aspekte
zu Techniken verallgemeinern und als generische Aspekte implementieren lassen. Als
Ergebnis dieser Arbeiten ist ein Baukastensystem für das Prototyping von Sprachprozessoren denkbar.
Ein weiterer Anwendungsfall liegt in der aspektorientierten Entwicklung von PrologMeta-Interpretern vor. Implementationen solcher Interpreter mittels der Methode des
Stepwise Enhancements [86, 53] können dabei als Ausgangspunkt dienen und entsprechende Implementationen in eine aspektorientierte Beschreibung überführt werden. Ers-
95
7 Abschließende Betrachtungen
te Untersuchungen zur Implementation eines dynamischen Webers lieferten vielversprechende Ergebnisse.
In der bisherigen Betrachtung von Sprachprozessoren dienten diese der Verarbeitung
von künstlichen Sprachen. Prolog ist jedoch seit seiner Entstehung eine beliebte Entwicklungssprache für die Verarbeitung natürlicher Sprachen [74, 29, 19]. Auch hier finden standardisierte Techniken Verwendung. Hier ist zu prüfen, inwiefern solche Techniken als generische Aspekte beschreibbar und wiederverwendbar sind. Darüber hinaus
muss geprüft werden, ob eine aspektorientierte Beschreibung für bestimmte Phänomene
natürlicher Sprachen, welche sich bisher nur schwer erfassen lassen, geeignet erscheint.
Eine generelle Verbesserung der Situation ist allerdings aufgrund der Unentscheidbarkeit
des Wortproblems für kontextsensitive Sprachen [34] nicht zu erwarten.
Wissensrepräsentation
Ein weiteres typisches Anwendungsfeld für Prolog ist die Modellierung und Repräsentation von Wissen [15, 32]. Eine zukünftige Fragestellung ist hier, inwiefern Crosscutting eine
Modularisierung von Wissen behindert und ob eine aspektorientierte Wissensbeschreibung möglich und sinnvoll ist. Ein denkbarer Anwendungsfall wäre die Beeinflussung von
Wissen durch Aspekte, beispielsweise die Modellierung des Einflusses einer Krankheit
auf die Modellierung von Körperfunktionen als Aspekt.
Einer besonderen Bedeutung kommt dabei die Möglichkeit der Repräsentation nichtmonotoner Logiken mit Hilfe von Aspekten zu. Für gewöhnlich erweitern neue Klauseln
in Prolog die Menge der ableitbaren Grundinstanzen eines Programms oder erhalten
diese. Lediglich Klauseln, welche einen Cut enthalten und Prädikate mit extralogischen
Seiteneffekten durchbrechen diese Monotonie. Aspekte hingegen erreichen durch rein
logische Advices eine Einschränkung des Wissens und können somit zur Beschreibung
nichtmonotoner Sachverhalte verwendet werden. Inwiefern dies zur Modellierung von
Wissen nützlich sein kann, gilt es zu untersuchen.
96
Literaturverzeichnis
[1] Grammar Base. http://www.program-transformation.org/Sdf/GrammarBase/.
[2] Adelsberger, Heimo H. und Gustaf Neumann: Goal Oriented Simulation
modelling using Prolog. In: Lavery, R. G. (Herausgeber): Proceedings of the Conference on Modeling and Simulation on Microcomputers, Seiten 42–47, San Diego,
CA, USA, 1985.
[3] Aho, Alfred V., Ravi Sethi und Jeffrey D. Ullman: Compilers: Principles,
Techniques, and Tools. Addison-Wesley, 1986.
[4] AI Applications Institute, University of Edinburgh: Edinburgh Prolog
Tools, 1988.
[5] Aksit, Mehmet, Robert E. Filman, Siobhan Clarke und Tzilla Elrad
(Herausgeber): Aspect-Oriented Software Development. Addison-Wesley, Oktober
2004.
[6] AspectJ Team: AspectJ home page. http://www.eclipse.org/aspectj/.
[7] Balzert, Helmut: Software-Entwicklung, Band 1 der Reihe Lehrbuch der
Software-Technik. Spektrum Akademischer Verlag, Heidelberg, 2. Auflage, 2001.
[8] Barklund, Jonas: What is a Meta-Variable in Prolog? In: Abramson, Harvey D. und M.H. Rogers (Herausgeber): Meta-Programming in Logic Programming, Seiten 383–398. MIT Press, 1989.
[9] Bossi, Annalisa (Herausgeber): Proceedings of the 9th International Workshop
on Logic Based Program Synthesis and Transformation (LOPSTR’99), Band 1817
der Reihe Lecture Notes in Computer Science, Venedig, Italien, September 1999.
Springer-Verlag.
[10] Bowen, Jonathan P., He Jifeng und Paritosh K. Pandya: An Approach to
Verifiable Compiling Specification and Prototyping. In: Deransart, Pierre und
Jan Maluszyński (Herausgeber): Proceedings of the 2nd International Workshop
on Programming Language Implementation and Logic Programming (PLILP’90),
Band 456 der Reihe Lecture Notes in Computer Science, Seiten 45–59, Linköping,
Schweden, August 1990. Springer-Verlag.
[11] Brand, Marc van den, Arie van Deursen, Jan Heering, Hayco de Jong,
Merijn de Jonge, Tobias Kuipers, Paul Klint, Leon Moonen, Pieter A.
97
Literaturverzeichnis
Olivier, Jeroen Scheerder, Jurgen J. Vinju, Eelco Visser und Joost
Visser: The ASF+SDF Meta-Environment: a Component-Based Language Development Environment. In: Wilhelm, Reinhard (Herausgeber): Proceedings of the
10th International Conference on Compiler Construction (CC’01), Band 2027 der
Reihe Lecture Notes in Computer Science, Seiten 365–370, Genua, Italien, April
2001. Springer-Verlag.
[12] Bryant, Barrett R. und Aiqin Pan: Rapid Prototyping of Programming Language Semantics Using Prolog. In: Proceedings of the 13th Annual International
Computer Software and Applications Conference (COMPSAC’99), Seiten 439–446,
Orlando, Florida, September 1989. IEEE Computer Society Press.
[13] Bryant, Barrett R. und Aiqin Pan: Denotational Semantics-Directed Compilation Using Prolog. In: Proceedings of the Symposium on Applied Computing
(SAC’90), Seiten 122–127, Fayetteville, AR, USA, April 1990.
[14] Byrd, Lawrence: Understanding the control flow of Prolog programs. In: Tarnlund, Sten-Ake (Herausgeber): Proceedings of the Logic Programming Workshop,
Debrecen, Ungarn, Juli 1980.
[15] Clark, Keith L. und Frank G. McCabe: PROLOG: A Language for Implementing Expert Systems. In: Hayes, Jean E., Donald Michie und Yo-Han
Pao (Herausgeber): Machine Intelligence, Band 10, Seiten 455–470. Ellis Horwood,
1982.
[16] Clocksin, William F. und Christopher S. Mellish: Programming in Prolog.
Springer-Verlag, 4. Auflage, 1994.
[17] Coady, Yvonne, Gregor Kiczales, Mike Feeley und Greg Smolyn: Using
AspectC to Improve the Modularity of Path-Specific Customization in Operating System Code. In: Proceedings of the Joint European Software Engineering Conference
(ESEC) and 9th ACM SIGSOFT International Symposium on the Foundations of
Software Engineering (FSE-9), Seiten 88–98, Wien, Österreich, September 2001.
ACM Press.
[18] Cohen, Jacques und Timothy J. Hickey: Parsing and compiling using Prolog.
ACM Transactions on Programming Languages and Systems, 9(2):125–163, April
1987.
[19] Covington, Michael A.: Natural Language Processing for Prolog Programmers.
Prentice-Hall, 1994.
[20] Deransart, Pierre, AbdelAli Ed-Dbali und Laurent Cernvoni: Prolog:
The Standard. Springer-Verlag, 1996.
[21] Deransart, Pierre und Jan Maluszyński: Relating Logic Programs and Attribute Grammars. Theory and Practice of Logic Programming, 2(2):119–155, Juli
1985.
98
Literaturverzeichnis
[22] Deransart, Pierre und Jan Maluszyński: A grammatical view of logic programming. MIT Press, 1993.
[23] Dijkstra, Edsger W.: On the role of scientific thought. In: Selected Writings on
Computing: A Personal Perspective, Seiten 60–66. Springer-Verlag, 1982.
[24] Drosten, Klaus: Translating Algebraic Specifications to Prolog Programs: A
Comparative Study. In: Grabowski, Jan, Pierre Lescanne und Wolfgang
Wechler (Herausgeber): Proceedings of the International Workshop on Algebraic
and Logic Programming, Band 343 der Reihe Lecture Notes in Computer Science,
Seiten 137–146, Gaußig, DDR, 1988. Springer-Verlag.
[25] Filman, Robert E. (Herausgeber): Proceedings of the 5th international conference on Aspect-oriented software development (AOSD’06), Bonn, Deutschland,
März 2006.
[26] Filman, Robert E. und Daniel P. Friedman: Aspect-Oriented Programming
is Quantification and Obliviousness. In: Aksit, Mehmet et al. [5], Kapitel 2.
[27] Finlay, A. Y. und Peter Hammond: Expert systems in dermatology: the computer potential. The example of facial tumour diagnosis. Dermatologica, 173(2):79–84,
1996.
[28] Gamma, Erich, Richard Helm, Ralph Johnson und John M. Vlissides:
Design Patterns: Elements of Reusable Object-Oriented Software. Addison-Wesley,
1995.
[29] Gazdar, Gerald und Christopher S. Mellish: Natural Language Processing
in Prolog: An Introduction to Computational Linguistics. Addison-Wesley, 1989.
[30] GNU Project: Flex — a scanner generator. Free Software Foundation, 1998.
http://www.gnu.org/software/flex/manual/.
[31] Gudmundson, Stephan und Gregor Kiczales: Addressing Practical Software Development Issues in AspectJ with a Pointcut Interface. In: Frohner,
Ákos (Herausgeber): Workshop Reader of the 15th European Conference on Objectoriented Programming (ECOOP’01), Band 2323 der Reihe Lecture Notes in Computer Science, Budapest, Ungarn, Juni 2001. Springer-Verlag.
[32] Hammond, Peter: Micro-Prolog for Expert Systems. In: Clark, Keith L. und
Frank G. McCabe (Herausgeber): Micro-Prolog: Programming in Logic, Kapitel 11. Prentice Hall, 1984.
[33] Heering, Jan, Paul Hendriks, Paul Klint und Jan Rekers: The syntax
definition formalism SDF — Reference manual. SIGPLAN Notices, 24(11):43–75,
1989.
99
Literaturverzeichnis
[34] Hopcroft, J. und Jeffrey D. Ullman: Introduction to Automata Theory, Languages and Computation. Addison Wesley, 1980.
[35] Intelligent Systems Laboratory: SICStus Prolog User’s Manual. Swedish
Institute of Computer Science, Kista, Schweden, März 2006.
[36] International Organization for Standardization: ISO/IEC 13211-1:
PROLOG – Part 1: General core, 1995.
[37] Jain, Ashish, Marc Kirschenbaum und Leon S. Sterling: Constructing Provably Correct Logic Programs. Technischer Bericht CES-94-04, Department of Computer Engineering and Science, Case Western Reserve University, März 1994.
[38] Jain, Ashish und Leon S. Sterling: A Methodology for Program Construction
by Stepwise Structural Enhancement. Technischer Bericht CES-94-10, Department
of Computer Engineering and Science, Case Western Reserve University, Juni 1994.
[39] Kalleberg, Karl Trygve und Eelco Visser: Combining Aspect-Oriented and
Strategic Programming. In: Cirstea, Horatiu und Narciso Martı́-Oliet (Herausgeber): Proceedings of the 6th International Workshop on Rule-Based Programming (RULE’05), Electronic Notes in Theoretical Computer Science, Nara, Japan,
April 2005. Elsevier Science.
[40] Karaorman, Murat, Urs Hölzle und John Bruno: jContractor: A Reflective
Java Library to Support Design by Contract. In: Cointe, Pierre (Herausgeber):
Proceedings of the 2nd International Conference on Meta-Level Architectures and
Reflection (Reflection’99), Band 1616 der Reihe Lecture Notes in Computer Science,
Seiten 175–196, SaintMalo, Frankreich, Juli 1999. Springer-Verlag.
[41] Katz, Shmuel: Diagnosis of Harmful Aspects Using Regression Verification. In:
Clifton, Curtis, Ralf Lämmel und Gary T Leavens (Herausgeber): Proceedings of the Workshop on Foundations Of Aspect-Oriented Languages (FOAL’04),
Seiten 1–6, Lancaster, UK, März 2004.
[42] Kiczales, Gregor: Is ‘advice’ adequate? AOSD-Mailingliste, November 2004.
[43] Kiczales, Gregor: It’s the Crosscutting. Software Development, Februar 2004.
[44] Kiczales, Gregor, Eric Hilsdale, Jim Hugunin, Mik Kersten, Jeffrey
Palm und William G. Griswold: An Overview of AspectJ. In: Knudsen,
Jørgen Lindskov (Herausgeber): Proceedings of the 15th European Conference on
Object-Oriented Programming (ECOOP’01), Band 2072 der Reihe Lecture Notes in
Computer Science, Seiten 327–353, Budapest, Ungarn, Juni 2001. Springer-Verlag.
[45] Kiczales, Gregor, John Lamping, Andruag Mendhekar, Chris Maeda,
Cristina Lopes, Jean-Marc Loingtier und John Irwin: Aspect-oriented
Programming. In: Akşit, Mehmet und Satoshi Matsuoka (Herausgeber):
100
Literaturverzeichnis
Proceedings of the 11th European Conference on Object-Oriented Programming
(ECOOP’97), Band 1241 der Reihe Lecture Notes in Computer Science, Seiten
220–242, Jyvaskyla, Finnland, Juni 1997. Springer-Verlag.
[46] Kirschenbaum, Marc, Spiro Michaylov und Leon S. Sterling: Skeletons
and Techniques as a Normative Approach to Program Development in Logic-Based
Languages. In: Proceedings of the Australian Computer Science Communications(ACSC’96), Band 18, Seiten 516–524, 1996.
[47] Kirschenbaum, Marc, Leon S. Sterling und Ashish Jain: Relating Logic
Programs Via Program Maps. Annals in Mathematics and Artificial Intelligence,
8(III–IV):229–246, 1993.
[48] Klint, Paul: A meta-environment for generating programming environments.
ACM Transactions on Software Engineering and Methodology, 2(2):176–201, 1993.
[49] Klint, Paul, Tijs van der Storm und Jurgen J. Vinju: Term Rewriting
Meets Aspect-Oriented Programming. In: Middeldorp, Aart, Vincent van
Oostrom, Femke van Raamsdonk und Roel C. de Vrijer (Herausgeber):
Processes, Terms and Cycles: Steps on the Road to Infinity, Essays Dedicated to Jan
Willem Klop, on the Occasion of His 60th Birthday, Band 3838 der Reihe Lecture
Notes in Computer Science, Seiten 88–105. Springer-Verlag, 2005.
[50] Kulaš, Marija: Annotations for Prolog – A Concept and Runtime Handling. In:
Bossi, Annalisa [9], Seiten 234–254.
[51] Kulaš, Marija: Debugging Prolog Using Annotations. In: Ducassé, Mireille, Anthony J. Kusalik und German Puebla (Herausgeber): Proceedings of
the 10th Workshop on Logic Programming Environments (WLPE’99), Band 30 der
Reihe Electronical Notes in Theoretical Computer Science, Las Cruses, NM, USA,
November 1999. Elsevier.
[52] Lakhotia, Arun: A Workbench for Developing Logic Programs By Stepwise
Enhancement. Doktorarbeit, Case Western Reserve University, Department of Computer Engineering and Science, August 1989.
[53] Lakhotia, Arun, Leon S. Sterling und Dimitar Bojantchev: Development
of a Prolog Tracer by Stepwise Enhancement. In: Proceedings of the 3rd International Conference on the Practical Application of Prolog (PAP’95), Seiten 371–393,
Paris, Frankreich, April 1995. ACM Press.
[54] Lämmel, Ralf: Declarative Aspect-Oriented Programming. In: Danvy, Olivier
(Herausgeber): Proceedings of the ACM SIGPLAN Workshop on Partial Evaluation and Semantics-Based Program Manipulation (PEPM’99), Nummer NS-99-1 in
BRICS NOTES Series, Seiten 131–146, San Antonio, TX, USA, Januar 1999.
101
Literaturverzeichnis
[55] Lämmel, Ralf: A Semantical Approach to Method-Call Interception. In: Kiczales, Gregor (Herausgeber): Proceedings of the 1st International Conference on
Aspect-Oriented Software Development (AOSD’02), Seiten 41–55, Twente, Niederlande, April 2002. ACM Press.
[56] Lämmel, Ralf und Kris De Schutter: What does aspect-oriented programming
mean to Cobol? In: Mezini, Mira und Peri Tarr [63], Seiten 99–110.
[57] Lämmel, Ralf und Günter Riedewald: Provable Correctness of Prototype Interpreters in LDL. In: Fritzson, Peter (Herausgeber): Proceedings of the 5th
International Conference on Compiler Construction (CC’94), Band 786 der Reihe
Lecture Notes in Computer Science, Seiten 218–232, Edinburgh, UK, 1994. SpringerVerlag.
[58] Lämmel, Ralf und Günter Riedewald: Prological Language Processing. In:
Brand, Mark van den und Didier Parigot (Herausgeber): Proceedings of
the 1st Workshop on Language Descriptions, Tools and Applications (LDTA’01),
Band 44 der Reihe Electronical Notes in Theoretical Computer Science, Genua,
Italien, April 2001. Elsevier Science.
[59] Lämmel, Ralf, Günter Riedewald und Wolfgang Lohmann: Projections
of Programs Revisited. In: Bossi, Annalisa [9].
[60] Lämmel, Ralf und Christian Stenzel: Semantics-Directed Implementation of
Method-Call Interception. IEE Proceedings – Software, 151(2):109–128, April 2004.
[61] Marriott, Kim und Harald Søndergaard: Difference-List Transformation
for Prolog. New Generation Computing, 11(2):125–157, Oktober 1993.
[62] Meyer, Bertrand: Eiffel: the language. Prentice-Hall, New York, 2. Auflage,
1992.
[63] Mezini, Mira und Peri Tarr (Herausgeber): Proceedings of the 4th international
conference on Aspect-oriented software development (AOSD’05), Chicago, IL, USA,
März 2005.
[64] Naish, Lee: Types and intended meaning. In: Pfenning, Frank (Herausgeber):
Types in Logic Programming, Kapitel 6, Seiten 189–216. MIT Press, 1992.
[65] Naish, Lee: Higher order logic programming in Prolog. In: Chakravarty, Manuel, Yike Guo und Tetsuo Ida (Herausgeber): JICSLP’96 Post Conference
Workshop on Multi-Paradigm Logic Programming, Bonn, Juni 1996.
[66] Naish, Lee und Leon S. Sterling: A Higher Order Reconstruction of Stepwise Enhancement. In: Fuchs, Norbert E. (Herausgeber): Proceedings of the
7th International Workshop on Logic Based Program Synthesis and Transformation
(LOPSTR’97), Band 1463 der Reihe Lecture Notes in Computer Science, Seiten
245–263, Leuven, Belgien, Juli 1997. Springer-Verlag.
102
Literaturverzeichnis
[67] Naish, Lee und Leon S. Sterling: Stepwise Enhancement and Higher-Order
Programming in Prolog. The Journal of Functional and Logic Programming,
2000(4), März 2000.
[68] Neumann, H.: Automatisierung des Testens von Zusicherungen für PrologProgramme. Diplomarbeit, FernUniversität Hagen, 1998.
[69] Noord, Gertjan van: FSA Utilities: A Toolbox to Manipulate Finite-State Automata. In: Raymond, Darrell, Derick Wood und Sheng Yu (Herausgeber): Automata Implementation, Band 1260 der Reihe Lecture Notes in Computer
Science. Springer-Verlag, 1997.
[70] O’Keefe, Richard A.: advice.pl – Interlisp-like advice package. In: DEC-10
Prolog Library [4].
[71] Paakki, Jukka: A Logic-Based Modification of Attribute Grammars for Practical
Compiler Writing. In: Warren, David H. D. und Péter Szeredi (Herausgeber):
Proceedings of the 7th International Conference on Logic Programming (ICLP’99),
Seiten 203–217, Jerusalem, Israel, Juni 1990. MIT Press.
[72] Parnas, David L.: On the Criteria To Be Used in Decomposing Systems into
Modules. Seiten 411–427, 2002.
[73] Pereira, Fernando C. N. und David H. D. Warren: Definite clause grammars for language analysis—a survey of the formalism and a comparison with augmented transition networks. Artificial Intelligence, 13(3):231–278, Mai 1980.
[74] Pereira, Fernando C.N. und Stuart M. Shieber: Prolog and NaturalLanguage Analysis. Nummer 10 in Lecture Notes. Center for the Study of Language
and Information, Stanford, CA, USA, 1987.
[75] Riedewald, Günter: Prototyping by Using an Attribute Grammar as a Logic
Program. In: Alblas, Henk und Borivoj Melichar (Herausgeber): Proceedings of the International Summer School on Attribute Grammars, Applications
and Systems (SAGA’91), Band 545 der Reihe Lecture Notes in Computer Science,
Seiten 401–437, Prag, Tschechoslowakei, Juni 1991. Springer-Verlag.
[76] Riedewald, Günter: The LDL - Language Development Laboratory. In: Kastens, Uwe und Peter Pfahler (Herausgeber): Proceedings of the 4th International Conference on Compiler Construction (CC’92), Band 641 der Reihe Lecture
Notes in Computer Science, Seiten 88–94, Paderborn, Deutschland, Oktober 1992.
Springer-Verlag.
[77] Riedewald, Günter und Uwe Lämmel: Using an Attribute Grammar as a Logic Program. In: Deransart, Pierre, Bernard Lorho und Jan Maluszyński
(Herausgeber): Proceedings of the 1st International Workshop on Programming Language Implementation and Logic Programming (PLILP’88), Band 348 der Reihe
103
Literaturverzeichnis
Lecture Notes in Computer Science, Seiten 161–179, Orléans, Frankreich, Mai 1988.
Springer-Verlag.
[78] Robertson, David: A Simple Prolog Techniques Editor for Novice Users. In:
Proceedings of the 3 rd Annual Conference on Logic Programming, Seiten 78–85,
Edinburgh, April 1991.
[79] Robertson, David, Alan Bundy, Robert Muetzelfeldt, Mandy Haggith und Michael Uschold: Eco-Logic: Logic-Based Approaches to Ecological
Modelling. MIT Press, 1991.
[80] Schnabel, Kai Philipp: Entwicklung und Evaluation eines Expertensystems zur
Prognoseabschätzung bei Kindern mit Hirnstammgliomen. Doktorarbeit, Medizinische Fakultät der Humboldt-Universität zu Berlin, 1996.
[81] Siemens AG Österreich, Wien: IF/Prolog V5.3 Reference Manual, 1999.
[82] Skotiniotis, Therapon und David H. Lorenz: Cona: aspects for contracts and
contracts for aspects. In: Vlissides, John M. und Douglas C. Schmidt (Herausgeber): Companion to the 19th Annual ACM SIGPLAN Conference on ObjectOriented Programming, Systems, Languages, and Applications (OOPSLA’04), Seiten 196–197, Vancouver, BC, Kanada, Oktober 2004. ACM Press.
[83] Slonneger, Ken und Barry L. Kurtz: Formal Syntax and Semantics of Programming Languages: A Laboratory Based Approach. Addison-Wesley, 1994.
[84] Sterling, Leon S., Ashish Jain und Marc Kirschenbaum: Composition Based on Skeletons and Techniques. In: ILPS’93 Post Conference Workshop on Methodologies for Composing Logic Programs, Vancouver, Kanada, Oktober 1993.
[85] Sterling, Leon S. und Marc Kirschenbaum: Applying Techniques to Skeletons. In: Jacquet, Jean-Marie (Herausgeber): Constructing Logic Programs,
Kapitel 6, Seiten 127–140. John Wiley & Sons Ltd, 1993.
[86] Sterling, Leon S. und Arun Lakhotia: Composing Prolog Meta-Interpreters.
In: Kowalski, Robert A. und Kenneth A. Bowen (Herausgeber): Proceedings
of the 5th International Conference on Logic Programming, Seiten 386–403, Seattle,
WA, USA, August 1988. MIT Press.
[87] Sterling, Leon S. und Ehud Shapiro: The Art of Prolog. MIT Press, 2. Auflage, 1994.
[88] Tarr, Peri, Osher Harold Harrison William und Stanley M. Jr. Sutton: N Degrees of Separation: Multi-Dimensional Separation of Concerns. In: Proceedings of the 21st International Conference on Software Engineering (ICSE’99),
Seiten 107–119, Los Angeles, CA, USA, Mai 199.
104
Literaturverzeichnis
[89] Teitelman, Warren: Pilot: A Step towards Man-Computer Symbiosis. Doktorarbeit, Massachusetts Institute of Technology, Cambridge, MA, USA, September
1966.
[90] Teitelman, Warren: InterLISP Reference Manual. Xerox Paolo Alto Research
Center, Paolo Alto, CA, USA, 1974.
[91] Vasconcelos, Wamberto W.: Designing Prolog Programming Techniques. In:
Deville, Yves (Herausgeber): Proceedings of the 3rd International Workshop on
Logic Program Sythesis and Transformation (LOPSTR’93), Workshops in Computing, Seiten 85–99, Louvain-La-Neuve, Belgien, Juli 1993. Springer-Verlag.
[92] Warren, David H. D.: Logic Programming and Compiler Writing. Software —
Practice and Experience, 10(2):97–125, Februar 1980.
[93] Wielemaker, Jan: SWI Prolog 5.6 Reference Manual. Department of Social
Science Informatics, University of Amsterdam, Amsterdam, März 2006.
[94] Wilhelm, Reinhard und Dieter Maurer: Compiler Design. Addison-Wesley,
1995.
105
A Implementation
A.1 Pointcut-Beschreibungen
A.1.1 Kontext
1
2
3
4
5
6
7
8
9
10
11
12
description/context.pl
context(Context, inside(Distance), Aspect, Pointcut):arg(1, Context, Up),
in_context(Up, Distance, Aspect, Pointcut).
context(Context, beside(Distance), Aspect, Pointcut):arg(2, Context, Left),
in_context(Left, Distance, Aspect, Pointcut).
context(Context, parallel, Aspect, Pointcut):arg(3, Context, Same),
soft_member(pc(Aspect, Pointcut), Same).
context(Context, static, Aspect, Pointcut):arg(4, Context, StaticPointcuts),
soft_member(pc(Aspect, Pointcut), StaticPointcuts).
13
14
15
16
17
18
19
20
21
22
23
24
25
in_context([First|_], first, Aspect, Pointcut):soft_member(pc(Aspect, Pointcut), First).
in_context(Context, almost_first, Aspect, Pointcut):in_context(Context, first, Aspect, Pointcut).
in_context([Transparent|Context], almost_first, Aspect, Pointcut):memberchk(Transparent, [cut, true, fail]),
in_context(Context, almost_first, Aspect, Pointcut).
in_context(Context, near, Aspect, Pointcut):in_context(Context, all, Aspect, Pointcut), !.
in_context(Context, all, Aspect, Pointcut):member(Pointcuts, Context),
soft_member(pc(Aspect, Pointcut), Pointcuts).
26
27
28
29
30
all_context(Template, Context, Position, Aspect, Pointcut, Bag):all_solutions(Context, Template,
context(Context, Position, Aspect, Pointcut),
Bag).
106
A.1 Pointcut-Beschreibungen
A.1.2 Fixpunkt-Algorithmus
1
2
description/pointcuts.pl
clause_pointcuts(Aspects, Clause, BodyPCs, PCs):fixpoint(clause_pointcuts1(Aspects, Clause), _, (BodyPCs, PCs)).
3
4
5
6
7
clause_pointcuts1(Aspects, (Head:-Body), (OldBodyPCs, OldPCs), (BodyPCs, PCs)):( nonvar(OldBodyPCs) -> reverse(OldBodyPCs, Left) ; Left = [] ),
fixpoint(pointcuts(Aspects, clause(Head, Body), [], Left), OldPCs, PCs),
goal_pointcuts(Aspects, Body, [PCs], [], _, OldBodyPCs-[], BodyPCs-[]).
8
9
10
goal_pointcuts(Aspects, Goal, Up, Pointcuts):goal_pointcuts(Aspects, Goal, Up, [], _, _, Pointcuts-[]).
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
goal_pointcuts(_, !, _, Left, [cut|Left],
[cut|Hole1]-Hole1, [cut|Hole2]-Hole2):- !.
goal_pointcuts(_, true, _, Left, [true|Left],
[true|Hole1]-Hole1, [true|Hole2]-Hole2) :- !.
goal_pointcuts(_, fail, _, Left, [fail|Left],
[fail|Hole1]-Hole1, [fail|Hole2]-Hole2) :- !.
goal_pointcuts(Aspects, CompoundedGoal, Up, Left, NewLeft,
OldPCs-Hole1, PCs-Hole2) :nonvar(CompoundedGoal),
CompoundedGoal =.. [Functor, Goal1, Goal2],
memberchk(Functor, [’,’, ’->’, ’*->’]), !,
goal_pointcuts(Aspects, Goal1, Up, Left, TempLeft, OldPCs-Hole3, PCs-Hole4),
goal_pointcuts(Aspects, Goal2, Up, TempLeft, NewLeft,
Hole3-Hole1, Hole4-Hole2).
goal_pointcuts(Aspects, (Goal1 ; Goal2), Up, Left, [alt(PCs1, PCs2)|Left],
[alt(OldPCs1, OldPCs2)|Hole1]-Hole1,
[alt(PCs1, PCs2)|Hole2]-Hole2) :- !,
goal_pointcuts(Aspects, Goal1, Up, Left, _, OldPCs1-[], PCs1-[]),
goal_pointcuts(Aspects, Goal2, Up, Left, _, OldPCs2-[], PCs2-[]).
goal_pointcuts(Aspects, Goal, Up, Left, [PCs|Left],
[OldPCs|Hole1]-Hole1, [PCs|Hole2]-Hole2):fixpoint(pointcuts(Aspects, goal(Goal), Up, Left), OldPCs, PCs).
34
35
36
37
38
39
40
41
42
pointcuts(Aspects, Joinpoint, Up, Left, OldPCs, Pointcuts):( var(OldPCs) -> OldPCs = [] ; true ),
Context1 = context(Up, Left, OldPCs),
sublist(pointcut1(Aspects, Joinpoint, Context1), OldPCs, FilteredPCs),
Context2 = context(Up, Left, FilteredPCs),
all_solutions((Joinpoint, Context2), Pointcut,
pointcut2(Aspects, Joinpoint, Context2, Pointcut), NewPCs),
append(FilteredPCs, NewPCs, Pointcuts).
43
44
45
46
47
48
49
50
pointcut1(Aspects, Joinpoint, Context, pc(Aspect, Pointcut)):member(Aspect, Aspects),
soft_call(pointcut(Joinpoint, Context, Aspect, Pointcut),
(Aspect, Joinpoint, Context)).
pointcut2(Aspects, Joinpoint, Context, pc(Aspect, Pointcut)):pointcut1(Aspects, Joinpoint, Context, pc(Aspect, Pointcut)),
\+ context(Context, parallel, Aspect, Pointcut).
107
A Implementation
A.2 Advices
A.2.1 Port-Advices
1
2
3
4
5
6
7
description/port advices.pl
port_advices(Aspects, PCs, Port, Advices, VarNameAdvices):all_solutions(PCs, Advice, port_adv1(Aspects, PCs, Port, Advice), Bag),
Bag = [_|_],
maplist(arg(1), Bag, Advices1),
maplist(arg(2), Bag, VarNameAdvices1),
combine_goals(Advices1, Advices),
flatten(VarNameAdvices1, VarNameAdvices).
8
9
10
11
12
13
14
15
16
port_adv1(Aspects, Pointcuts, Port, (Advice, VarNameAdvices)):member(Aspect, Aspects),
soft_call(’$clause’(port_advice(Aspect, PC1, Port), Advice, Ref, Vars),
Aspect),
member(pc(Aspect, PC2), Pointcuts),
soft_call(PC1 = PC2, PC2),
prolog_clause:clause_info(Ref, _, _, VarNameTerm),
maplist(var_name(VarNameTerm), Vars, VarNameAdvices).
17
18
19
combine_goals([], true).
combine_goals([Goal|Goals], (Goal, CompGoal)):- combine_goals(Goals, CompGoal).
A.2.2 Term-Advices
1
2
3
4
5
6
7
description/term advices.pl
term_advices(Aspects, Pointcuts, Advices, VarNameAdvices):all_solutions(Pointcuts, Advice, term_adv1(Aspects, Pointcuts, Advice),
Bag),
Bag = [_|_],
maplist(arg(1), Bag, Advices),
maplist(arg(2), Bag, VarNameAdvices1),
flatten(VarNameAdvices1, VarNameAdvices).
8
9
10
11
12
13
14
15
16
term_adv1(Aspects, Pointcuts, (Advice, VarNameAdvices)):member(Aspect, Aspects),
soft_call(’$clause’(term_advice(Aspect, PC1, Advice), Body, Ref, Vars),
Aspect),
member(pc(Aspect, PC2), Pointcuts),
soft_call((PC1 = PC2, Body), PC2),
prolog_clause:clause_info(Ref, _, _, VarNameTerm),
maplist(var_name(VarNameTerm), Vars, VarNameAdvices).
108
A.3 Aspekte
A.3 Aspekte
1
description/require.pl
required_aspects(Aspects, AllAspects):- required_aspects(Aspects, [], AllAspects).
2
3
4
5
6
7
8
9
required_aspects([], Aspects, Aspects).
required_aspects([Aspect|Aspects], Temp1, Temp4):required_aspects(Aspects, Temp1, Temp2),
all_solutions(Aspect, Req, require_aspects(Aspect, Req), Reqs),
flatten(Reqs, FlatReqs),
required_aspects(FlatReqs, Temp2, Temp3),
( memberchk(Aspect, Temp3) -> Temp4 = Temp3 ; Temp4 = [Aspect|Temp3] ).
A.4 Weber
A.4.1 Dateien
1
2
3
4
weave.pl
weave_file(Aspects, FileIn, FileOut):read_program(FileIn, Prog, VarNames),
weave(Aspects, Prog, Woven, VarNames, WovenVarNames),
write_program(FileOut, Woven, WovenVarNames).
5
6
7
8
weave(Aspects, Prog, Woven, VarNamesIn, VarNamesOut):required_aspects(Aspects, AllAspects),
weave_program(AllAspects, Prog, Woven, VarNamesIn, VarNamesOut).
A.4.2 Programme
1
2
3
4
5
6
7
weaver/program.pl
weave_program(_, [], [], [], []).
weave_program(Aspects, [Clause|Clauses], [NewClause|NewClauses],
[VarNames1|RemVarNames1], [VarNames2|RemVarNames2]):weave_clause(Aspects, Clause, NewClause, VarNames3-[]),
append(VarNames1, VarNames3, VarNames2),
weave_program(Aspects, Clauses, NewClauses,
RemVarNames1, RemVarNames2).
109
A Implementation
A.4.3 Klauseln
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
weaver/clause.pl
weave_clause(Aspects, Head1 :- Body1, NewClause, VarNames1-VarNames10):- !,
clause_pointcuts(Aspects, Head1 :- Body1, BodyPCs, Pointcuts),
Up = [Pointcuts],
weave_goal(Aspects, BodyPCs-[], Up, Body1, Body2, VarNames9-VarNames10),
weave_term(Aspects, Pointcuts, Head1, Head2, VarNames8-VarNames9),
weave_port(Aspects, Pointcuts, around, Up, Body2, Body3,
VarNames1-VarNames2),
weave_port(Aspects, Pointcuts, failbody, Up, Body3, Body4,
VarNames2-VarNames3),
weave_port(Aspects, Pointcuts, enterbody, Up, Body4, Body5,
VarNames3-VarNames4),
weave_port(Aspects, Pointcuts, redobody, Up, Body5, Body6,
VarNames4-VarNames5),
weave_port(Aspects, Pointcuts, exitbody, Up, Body6, Body7,
VarNames5-VarNames6),
weave_port(Aspects, Pointcuts, trymatch, Up, Match1, Match2,
VarNames6-VarNames7),
weave_port(Aspects, Pointcuts, failmatch, Up, Match2, Match3,
VarNames7-VarNames8),
( var(Match3) -> Body8 = Body7, Head3 = Head2 ;
match_goal(Head2, Head3, Match1), Body8 = (Match3 -> Body7) ),
simplify_clause(Head3:-Body8, NewClause).
23
24
25
26
27
weave_clause(Aspects, (:- Body), (:- NewBody), VarNames):- !,
goal_pointcuts(Aspects, Body, [], Pointcuts),
weave_goal(Aspects, Pointcuts-[], [], Body, TempBody, VarNames),
simplify_goal(TempBody, NewBody).
28
29
30
weave_clause(Aspects, Head, NewClause, VarNames):weave_clause(Aspects, Head :- true, NewClause, VarNames).
110
A.4 Weber
A.4.4 Ziele
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
weaver/goal.pl
weave_goal(_, [cut|Hole]-Hole, _, !, !, VarNames-VarNames):- !.
weave_goal(_, [true|Hole]-Hole, _, true, true, VarNames-VarNames):- !.
weave_goal(_, [fail|Hole]-Hole, _, fail, fail, VarNames-VarNames):- !.
weave_goal(Aspects, Pointcuts-Hole, Up, CompoundedGoal, NewGoal,
VarNames1-VarNames3):nonvar(CompoundedGoal),
CompoundedGoal =.. [Functor, Goal1, Goal2],
memberchk(Functor, [’,’, ’->’, ’*->’]), !,
weave_goal(Aspects, Pointcuts-Hole1, Up, Goal1, Goal3,
VarNames1-VarNames2),
weave_goal(Aspects, Hole1-Hole, Up, Goal2, Goal4,
VarNames2-VarNames3),
NewGoal =.. [Functor, Goal3, Goal4].
weave_goal(Aspects, [alt(PCs1, PCs2)|Hole]-Hole, Up,
(Goal1 ; Goal2), (Goal3 ; Goal4), VarNames1-VarNames3):- !,
weave_goal(Aspects, PCs1-[], Up, Goal1, Goal3,
VarNames1-VarNames2),
weave_goal(Aspects, PCs2-[], Up, Goal2, Goal4,
VarNames2-VarNames3).
weave_goal(Aspects, [Pointcuts|Hole]-Hole, Up, Goal1, Goal7, VarNames1-VarNames7):NewUp = [Pointcuts|Up],
weave_term(Aspects, Pointcuts, Goal1, Goal2, VarNames6-VarNames7),
weave_port(Aspects, Pointcuts, around, NewUp, Goal2, Goal3,
VarNames1-VarNames2),
weave_port(Aspects, Pointcuts, fail, NewUp, Goal3, Goal4,
VarNames2-VarNames3),
weave_port(Aspects, Pointcuts, call, NewUp, Goal4, Goal5,
VarNames3-VarNames4),
weave_port(Aspects, Pointcuts, redo, NewUp, Goal5, Goal6,
VarNames4-VarNames5),
weave_port(Aspects, Pointcuts, exit, NewUp, Goal6, Goal7,
VarNames5-VarNames6).
111
A Implementation
A.4.5 Port-Advices
1
2
3
4
5
6
7
weaver/port advices.pl
weave_port(Aspects, Pointcuts, Port, Up, Goal1, Goal2, VarNames1-VarNames3):port_advices(Aspects, Pointcuts, Port, Advices, VarNames) ->
append(VarNames, VarNames2, VarNames1),
weave_port_advices(Aspects, Up, Port, Advices, Goal1, Goal2,
VarNames2-VarNames3) ;
Goal1 = Goal2,
VarNames3 = VarNames1.
8
9
10
11
12
weave_port_advices(Aspects, Up, Port, Advices, Goal, Woven, VarNames):goal_pointcuts(Aspects, Advices, Up, Pointcuts),
weave_goal(Aspects, Pointcuts-[], Up, Advices, NewAdvices, VarNames),
weave_port_advices(Port, NewAdvices, Goal, Woven).
13
14
15
16
17
18
19
20
21
22
23
24
weave_port_advices(call, Advices, Goal, (Advices, Goal)).
weave_port_advices(fail, Advices, Goal, ((true ; Advices), Goal)).
weave_port_advices(around, Advices, _, Advices).
weave_port_advices(exit, Advices, Goal, (Goal, Advices)).
weave_port_advices(redo, Advices, Goal, (Goal, (true ; Advices))).
weave_port_advices(trymatch, Advices, Goal, (Advices, Goal)).
weave_port_advices(failmatch, Advices, Goal, ((true ; Advices), Goal)).
weave_port_advices(enterbody, Advices, Goal, (Advices, Goal)).
weave_port_advices(failbody, Advices, Goal, ((true ; Advices), Goal)).
weave_port_advices(exitbody, Advices, Goal, (Goal, Advices)).
weave_port_advices(redobody, Advices, Goal, (Goal, (true ; Advices))).
112
A.4 Weber
A.4.6 Term-Advices
1
2
3
4
5
6
weaver/term advices.pl
weave_term(Aspects, Pointcuts, Goal1, Goal2, VarNames1-VarNames2):term_advices(Aspects, Pointcuts, Advices, VarNames) ->
weave_term_advices(Advices, Goal1, Goal2),
append(VarNames, VarNames2, VarNames1) ;
Goal1 = Goal2,
VarNames2 = VarNames1.
7
8
9
10
11
weave_term_advices([], Term, Term).
weave_term_advices([Advice|Advices], Term, NewTerm):weave_term_advice(Advice, Term, TempTerm),
weave_term_advices(Advices, TempTerm, NewTerm).
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
weave_term_advice(rename(NewFunctor), Term, NewTerm):Term =.. [_|Args], NewTerm =.. [NewFunctor|Args].
weave_term_advice(extra_arg(Arg, Pos), Term, NewTerm):weave_term_advice(extra_args([Arg], Pos), Term, NewTerm).
weave_term_advice(extra_args(Args, first), Term, NewTerm):- !,
weave_term_advice(extra_args(Args, 0), Term, NewTerm).
weave_term_advice(extra_args(Args, last), Term, NewTerm):- !,
functor(Term, _, N),
weave_term_advice(extra_args(Args, N), Term, NewTerm).
weave_term_advice(extra_args(ExtraArgs, N), Term, NewTerm):Term =.. [Functor|Args],
insert_elems(N, Args, ExtraArgs, NewArgs),
NewTerm =.. [Functor|NewArgs].
weave_term_advice(select(Positions), Term, NewTerm):Term =.. [Functor|Args],
select_elems(Positions, Args, NewArgs),
NewTerm =.. [Functor|NewArgs].
weave_term_advice(unify(X, Y), Term, Term):X = Y.
32
33
34
35
36
insert_elems(0, Elems, NewElems, InsertedElems):- !,
append(NewElems, Elems, InsertedElems).
insert_elems(N, [Elem|Elems], NewElems, [Elem|InsertedElems]):succ(M, N), insert_elems(M, Elems, NewElems, InsertedElems).
37
38
39
40
41
select_elems([], _, []).
select_elems([Pos|Positions], Elems, [Elem|SelectedElems]):( Pos = last -> last(Elems, Elem) ; nth1(Pos, Elems, Elem) ),
select_elems(Positions, Elems, SelectedElems).
113
A Implementation
A.4.7 Vereinfachungen
1
2
3
4
weaver/simplify.pl
simplify_clause(Head :- Body, SimpleClause):simplify_goal(Body, SimpleBody),
( SimpleBody = true -> SimpleClause = Head ;
SimpleClause = (Head :- SimpleBody) ).
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
simplify_goal(((Goal1, Goal2), Goal3), SimpleGoal):- !,
simplify_goal((Goal1, Goal2, Goal3), SimpleGoal).
simplify_goal(((Goal1; Goal2); Goal3), SimpleGoal):- !,
simplify_goal((Goal1; Goal2; Goal3), SimpleGoal).
simplify_goal((Goal1, Goal2), SimpleGoal):- !,
simplify_goal(Goal1, SimpleGoal1),
simplify_goal(Goal2, SimpleGoal2),
( SimpleGoal1 = true -> SimpleGoal = SimpleGoal2 ;
SimpleGoal2 = true -> SimpleGoal = SimpleGoal1 ;
SimpleGoal = (SimpleGoal1, SimpleGoal2) ).
simplify_goal((Goal1 ; Goal2), (SimpleGoal1 ; SimpleGoal2)):- !,
simplify_goal(Goal1, SimpleGoal1),
simplify_goal(Goal2, SimpleGoal2).
simplify_goal((Goal1 -> Goal2), (SimpleGoal1 -> SimpleGoal2)):- !,
simplify_goal(Goal1, SimpleGoal1),
simplify_goal(Goal2, SimpleGoal2).
simplify_goal((Goal1 *-> Goal2), (SimpleGoal1 *-> SimpleGoal2)):- !,
simplify_goal(Goal1, SimpleGoal1),
simplify_goal(Goal2, SimpleGoal2).
simplify_goal(Goal, Goal).
A.5 Hilfsprädikate
A.5.1 Subsumption
1
2
3
4
helper/soft.pl
soft_call(Goal, Invariant):copy_term(Invariant, InvariantCopy),
call(Goal),
variant(Goal, Invariant, InvariantCopy).
5
6
7
8
9
variant(_, Invariant, InvariantCopy):Invariant =@= InvariantCopy, !.
variant(Goal, ChangedInvariant, InvariantCopy):throw(invariant_exception(Goal, ChangedInvariant, InvariantCopy)).
10
11
12
13
14
soft_member(Elem, Elems):member(ProtectedElem, Elems),
catch(soft_call(Elem = ProtectedElem, ProtectedElem),
invariant_exception(_, _, _), fail).
114
A.5 Hilfsprädikate
A.5.2 Mehrere Lösungen
1
2
3
helper/all.pl
fixpoint(Goal, Init, Final):call(Goal, Init, Temp),
( Temp =@= Init -> Final = Temp ; fixpoint(Goal, Temp, Final) ).
4
5
6
7
all_solutions(Pattern, Template, Goal, Bag):findall(Pattern:Template, Goal, PatternBag),
maplist(match(Pattern), PatternBag, Bag).
8
9
match(Pattern, Pattern:X, X).
A.5.3 Ein- und Ausgabe
1
2
3
4
5
6
7
8
9
helper/io.pl
read_program(File, Prog, VarNames):open(File, read, Stream),
read_clauses(Stream, Prog, VarNames),
close(Stream).
read_clauses(Stream, Clauses, VarNames):read_term(Stream, Clause, [variable_names(ClauseVarNames)]),
( Clause = end_of_file -> Clauses = [], VarNames = [] ;
Clauses = [Clause|RemClauses], VarNames = [ClauseVarNames|RemVarNames],
read_clauses(Stream, RemClauses, RemVarNames) ).
10
11
12
13
14
15
16
17
18
19
write_program(File, Prog, VarNames):copy_term((Prog, VarNames), (CopyProg, CopyVarNames)),
open(File, write, Stream),
write_clauses(Stream, CopyProg, CopyVarNames),
close(Stream).
write_clauses(_, [], []).
write_clauses(Stream, [Clause|Clauses], [VarNames|RemVarNames]):bind_names(VarNames), portray_clause(Stream, Clause),
write_clauses(Stream, Clauses, RemVarNames).
A.5.4 Variablennamen
1
helper/vars.pl
var_name(VarNameTerm, N = Var, Name = Var):- succ(N, M), arg(M, VarNameTerm, Name).
2
3
bind_names(Names):- bind_names(Names, [], GenSym), maplist(reset_gensym, GenSym).
4
5
6
7
8
9
bind_names([], _, []).
bind_names([Name = Var|VarNames], Done, GenSym):( var(Var) -> bind_name(Name, Var, Done, NewDone, GenSym1) ;
NewDone = Done, GenSym1 = [] ),
bind_names(VarNames, NewDone, GenSym2), append(GenSym1, GenSym2, GenSym).
10
11
12
13
bind_name(Name, ’$VAR’(UniqueName), Done, [UniqueName|Done], GenSym):memberchk(Name, Done) -> gensym(Name, UniqueName), GenSym = [Name] ;
UniqueName = Name, GenSym = [].
115
Selbstständigkeitserklärung
Ich erkläre, dass ich die vorliegende Arbeit selbstständig und nur unter Vorlage der
angegebenen Literatur und Hilfsmittel angefertigt habe.
Rostock, den 13. April 2006
Guido Wachsmuth
Thesen
1. Aspektorientierte Programmierung erhöht die Verständlichkeit, Wartbarkeit, Erweiterbarkeit und Wiederverwendbarkeit von Implementationen.
2. Crosscutting Concerns treten in der Programmierung mit Prolog auf und lassen
sich durch aspektorientierte Ansätze separat implementieren.
3. Es existieren Ansätze zur separaten Implementation von Crosscutting Concerns
in Prolog, welche inhärent aspektorientierte Merkmale aufweisen. Hierzu gehören
Port-Annotationen, die Methode des Stepwise Enhancements und die Programmierung höherer Ordnung.
4. Aspektorientierte Ansätze aus der objektorientierten Programmierung lassen sich
auf Prolog übertragen.
5. Das vorgestellte statische Joinpoint-Modell erlaubt die Implementation von Crosscutting Concerns als Aspekte in Prolog.
6. Prolog kann als Beschreibungssprache für Aspekte eingesetzt werden.
7. Ein statischer Weber, welcher auf Transformationsregeln basiert, kann in Prolog
umgesetzt werden.
8. Die vorgestellte Beschreibungssprache bietet die Möglichkeit, die Prüfung von Spezifikationsfragmenten als Vor- und Nachbedingungen in Aspekten zu modularisieren.
9. Die Anwendung einer Technik auf ein Skelett stellt ein Crosscutting Concern dar
und kann unter Verwendung der vorgestellten Sprache als Aspekt implementiert
werden.
10. Techniken lassen sich als generische Aspekte formalisieren.
11. Aspektorientierte Prolog-Programmierung unterstützt mit Hilfe generischer Aspekte die Entwicklung von Prototypen für Sprachprozessoren.