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.