Westfälische Wilhelms-Universität Münster Ausarbeitung Algebraische Spezifikation mit OBJ im Rahmen des Seminars Formale Spezifikation Andre Christ Themensteller: Prof. Dr. Herbert Kuchen Betreuer: Prof. Dr. Herbert Kuchen Institut für Wirtschaftsinformatik Praktische Informatik in der Wirtschaft Inhaltsverzeichnis 1 Motivation 2 Grundlagen algebraischer Spezifikation 2.1 Abstrakte Datentypen . . . . . . . . . 2.2 Syntax abstrakter Datentypen . . . . . 2.3 Order-Sorted Algebra . . . . . . . . . 2.4 Denotationale Semantik . . . . . . . . 2.5 Operationale Semantik . . . . . . . . 2.5.1 Termersetzungssysteme . . . 2.5.2 Gleichungsbasiertes Schließen 3 4 Erweiterte Eigenschaften von OBJ 3.1 Modularisierung . . . . . . . . 3.2 Fehlerbehandlung . . . . . . . 3.3 Generizität . . . . . . . . . . . 3.4 Funktionales Prototyping . . . 3.5 Theorem Proving . . . . . . . 1 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2 2 3 4 7 10 10 13 . . . . . 14 14 15 16 18 19 Zusammenfassung und Ausblick 22 A Software A.1 Übersicht . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . A.2 Installation von OBJ3 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . A.3 Installation von BOBJ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23 23 23 24 B Funktionales Prototyping B.1 Benchmark-System . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . B.2 Quelltext Benchmark-Module . . . . . . . . . . . . . . . . . . . . . . . . B.3 Quelltext Fibonacci-Funktion . . . . . . . . . . . . . . . . . . . . . . . . . 25 25 25 25 i Kapitel 1: Motivation 1 Motivation In den vergangenen Jahren ist die Diffusion von Software in nahezu alle Bereiche des täglichen Lebens fortgeschritten. Angefangen beim klassischen Gebiet der Büro- und Unternehmenssoftware über die Steuerung von Anlagen und Maschinen bis hin zum Einsatz in der Medizin finden sich Programme, von denen der Mensch abhängig geworden ist. Diese Abhängigkeit ist eng an die fehlerfreie Funktionsweise, die Korrektheit, gekoppelt. Die zunehmend höhere Komplexität der Problemstellung und der resultierenden Software, die häufige Änderung während des Einsatzes und seit jüngerer Zeit die stärker in den Vordergrund rückende Wiederverwendung erfordern grundsätzlich neue Entwicklungsmethoden. In einem konventionellen Entwicklungsprozess ohne Einsatz formaler Spezifikation folgt der Realisierung eine intensive Testphase, die bei Fehlern Änderungen der Software mit sich bringt. Je später Fehler in diesem sich wiederholenden Zyklus entdeckt werden, umso kostenintensiver wird deren Korrektur. Weiterhin ist es wichtig festzuhalten, dass Testen nur die Existenz von Fehlern aufdeckt, nicht aber fehlerfreie Software garantiert. Als systematischere Herangehensweise zur Qualitätssicherung haben sich daher im Software Engineering Methoden entwickelt, die dazu dienen, die gewünschte Funktionalität von Programmen vor der Implementierung festzulegen. Während formale Methoden mathematische Kalküle der Algebra, Logik oder Mengenlehre benutzen, entziehen sich informale Methoden aufgrund ihrer textlichen oder grafischen Form axiomatischer oder logischer Verifizierbarkeit. Der Software-Entwicklungsprozess beim Einsatz formaler Spezifikation ist dadurch gekennzeichnet, immer detailliertere Beschreibungen der zu erstellenden Software zu konstruieren. Ziel dieses iterativen Prozesses ist ein ausführbares Programm mitsamt seiner Dokumentation. Inhalt der Beschreibungen ist eine Spezifikation der zukünftigen Software. Diese abstrahiert von der Problemstellung und definiert somit, was getan werden muss, ohne eine vollständige Beschreibung zu liefern, wie dies erreicht wird. Der schrittweise Übergang einer Spezifikation in eine detailliertere Spezifikation wird unter den Begriff der Verfeinerung zusammengefasst. In der Literatur werden modellorientierte Spezifikationen (auch als zustandsorientierte Spezifikationen bezeichnet) und eigenschaftsorientierte Spezifikationen (auch axiomatische oder algebraische Spezifikationen) unterschieden [LEW96, S. 13 ff.]. Vertreter modellorientierter Spezifikation sind beispielsweise VDM und Z. In dieser Arbeit wird die algebraische Spezifikation mit OBJ betrachtet. Kapitel 2 führt unter Berücksichtigung der Syntax von OBJ zunächst die mathematische Basis der algebraischen Spezifikation ein. In Kapitel 3 werden Aspekte von OBJ beleuchtet, die den praktischen Einsatz der Spezifikation mit OBJ unterstützen. Nach einem zusammenfassenden Beispiel schließt die Arbeit mit einer Bewertung von OBJ. 1 Kapitel 2: Grundlagen algebraischer Spezifikation 2 Grundlagen algebraischer Spezifikation 2.1 Abstrakte Datentypen Die Aufteilung der Gesamtheit aller möglichen Datenelemente führt zu Klassen gemeinsamer Eigenschaften, den sogenannten Datentypen. Als definierende Eigenschaft eines Datentyps werden die auf einer Wertemenge erklärten Operationen akzeptiert [EGL89, S. 2]. Bei der Analyse der Datentypen liegt der Fokus auf der Betrachtung von abgeschlossenen Familien aufeinander bezogener Datentypen. Das bedeutet, dass alle Datentypen des Argument- oder Wertebereichs Mitglieder der Familie sind [EGL89, S. 4]. Formal besteht eine solche Familie aufeinander bezogener Datentypen D1 , ..., Dn aus den Wertemengen W = W1 , ..., Wn und Operationen mit Argument- und Wertebereich aus W . In der Mathematik ist eine derartige Struktur als Algebra bekannt. Die Wertemengen nehmen hierbei den Platz der Trägermengen ein, die Operationen entsprechen den Verknüpfungen. U. a. ist es aus Gründen der Strukturierung sinnvoll, mehrere Datentypen, d. h. n > 1, zu erlauben. Die Algebra ist in diesem Fall heterogen (engl. many-sorted algebra). Bei der Spezifikation von anwendungsbezogenen Datentypen steht nicht die vollständige Beschreibung, sondern im Sinne der Abstraktion vielmehr die wesentlichen Eigenschaften des Datentyps im Vordergrund. Daher spricht man in dieser Phase der Softwareentwicklung von einem abstrakten Datentyp (ADT). Der Prozess der Verfeinerung engt diese Freiheiten im Zuge des Entwicklungsprozesses bis hin zur Implementation wieder ein [EGL89, S. 5]. Anhand des ADT Stack natürlicher Zahlen, vgl. Listing 1, werden im Folgenden Begriffe der algebraischen Spezifikation unter Benutzung der Syntax von OBJ eingeführt. Als Operationen des Stacks sollen push, top und pop sowie die Konstante empty definiert werden. obj STACK-OF-NAT is protecting NAT . sorts Stack . op empty : -> Stack . *** Signatur op push : Nat Stack -> Stack . op top : Stack -> Nat . op pop : Stack -> Stack . var X : Nat . var S : Stack . *** Axiome eq top(push(X, S)) = X . eq pop(push(X, S)) = S . endo Listing 1: ADT Stack natürlicher Zahlen. 2 Kapitel 2: Grundlagen algebraischer Spezifikation Das Schlüsselwort sorts listet diejenigen Datentypen auf, die in die Beschreibung eingehen. Die Operationen und Sorten des OBJ-Moduls NAT, welches die natürlichen Zahlen spezifiziert, werden durch is protecting importiert. Diese Bestandteile von Modularisierung und hierarchischer Strukturierung werden aus Gründen der Übersichtlichkeit erst im späteren Verlauf dieser Arbeit beschrieben. Das Schlüsselwort op ist jeweils dem Namen der verfügbaren Operationen vorangestellt. Nach dem Doppelpunkt hinter dem Namen folgen Argumentliste und Rückgabewert, die durch einen Pfeil voneinander abgetrennt sind. Dieser erste Teil der Spezifikation legt mittels Signatur die Syntax des abstrakten Datentyps fest. Im zweiten Teil werden unter Benutzung von Variablen, angeführt durch var, mittels Gleichungen nach eq die semantischen Eigenschaften beschrieben. Durch die Angabe von Axiomen beschränkt sich der Spezifizierer auf die gewünschte Funktionalität, er abstrahiert von einer konkreten Umsetzung und deren Details [HL89, S. 8 f.]. Signatur und Gleichungen machen zusammen eine Spezifikation aus. Eine Spezifikation definiert einen abstrakten Datentyp, sie kann von einer Algebra im Sinne einer Interpretation erfüllt werden. Die algebraische Spezifikation basiert auf einem mathematischen Grundgerüst. Es erlaubt unter anderem, eine eindeutige Beziehung zwischen Signatur und Algebra herzustellen. Wichtigstes Ziel dieses und des folgenden Kapitels ist es, das Fundament für gleichungsbasiertes Schließen, einer Technik zur Programmverifikation, zu legen. 2.2 Syntax abstrakter Datentypen Definition 2.1 Eine Signatur Σ besteht aus einer Menge S von Sorten und einer S ∗ × S-indizierten Mengenfamilie {Σs̄,s |s̄ ∈ S ∗ , s ∈ S} von Operationen [GM96, S. 12]. Dies bedeutet, dass zu jeder Liste von Argumentsorten s̄ = s1 , ..., sn und jeder Ergebnissorte s in Σ eine Menge Σs̄,s von Operationen existiert, wobei die leere Menge erlaubt ist. Eine Operation σ ∈ Σs̄,s wird auch geschrieben als σ : s1 × ... × sn → s. Die Anzahl der Argumente n gibt die Stelligkeit (engl. arity) der Operation an. Bei einer leeren Argumentliste, d. h. n = 0, wird die Operation als Konstante der Sorte s bezeichnet. Die Operation push des ADT Stack ist hiernach zweistellig: push : N at × Stack → Stack. Dahingehen ist empty eine Konstante vom Typ Stack: empty :→ Stack. Standardmäßig haben Operationen in OBJ die Prefix-Form mit Klammern und kommagetrennten Argumenten (z. B. push des ADT Stack). OBJ erlaubt darüber hinaus Prefix-, 3 Kapitel 2: Grundlagen algebraischer Spezifikation Infix- und Postfix-Form. Möglich wird diese sogenannte Mixfix-Form durch Unterstriche als Platzhalter für Variablen. In Listing 2 wird der ADT Natürliche Zahlen in Peano-Notation definiert. Diese Notation geht auf den Mathematiker Peano zurück, der gezeigt hat, dass sich die Menge der natürlichen Zahlen mit einem konstanten Startelement, hier der 0 und einer Nachfolger-Operation s beschreiben lässt. Die Zahl zwei entspricht in dieser Notation beispielsweise s(s(0)). Für die Additions- und Multiplikations-Operation wird im Beispiel die Infix-Notation verwendet, für die Nachfolger-Operation die standardmäßige Form. obj NAT is sort Nat . op 0 : -> Nat . op s : Nat -> Nat [prec 15] . op _+_ : Nat Nat -> Nat [prec 40] . op _*_ : Nat Nat -> Nat [prec 30] . [...] endo Listing 2: ADT Natürliche Zahlen in Peano-Notation (Ausschnitt). Um die Notwendigkeit von Klammerung in Ausdrücken einzuschränken, kann einer Operation per Attribut [prec X] ein Präzendenz-Wert zugeordnet werden, der die Bindungsstärke angibt. Dieser liegt zwischen 0 und 128, wobei 0 die höchste und 128 die schwächste Bindung angibt. Eine unäre (einstellige) Prefix-Operation hat standardmäßig den Wert 15, eine binäre (zweistellige) den Wert 41. Der Ausdruck s(0)+ s(0)* s(s(0)) wird unter Benutzung der Präzendenz-Werte im Beispiel gemäß der üblichen Punkt-vor-Strich-Regel ganz ohne Klammerung - richtig interpretiert. 2.3 Order-Sorted Algebra Definition 2.2 Sei Σ eine Signatur, dann besteht jede Σ-Algebra aus einer Trägermenge As für jede Sorte s ∈ S, einer Funktion aσ : As1 × ... × Asn → As für jede Operation σ ∈ Σs̄,s mit s̄ 6= [] und einer Konstanten aσ ∈ As für jedes σ ∈ Σ[],s [GM96, S. 16]. Die Definition legt eine Algebra als Interpretation oder Modell einer bestimmten Signatur fest. Sorten werden durch Mengen interpretiert, Operationen durch Funktionen oder Konstanten mit Argumenten und Rückgabewerten der entsprechenden Sorten. Gemäß OrderSorted Algebra (OSA), einer Erweiterung von Many-Sorted Algebra, können Sorten partiell geordnet sein (reflexive, transitive und antisymmetrische Relation). Auf die Trägermengen bezogen bedeutet partiell geordnet, dass eine Menge die Untermenge in einer anderen Menge 4 Kapitel 2: Grundlagen algebraischer Spezifikation ist. Laut Zahlentheorie der Mathematik gilt beispielsweise, dass die Menge der natürlichen Zahlen Nat eine Untermenge der ganzen Zahlen Int ist. In einem OBJ-Modul wird dies durch subsort Nat < Int ausgedrückt. Auf der semantischen Ebene der Algebra lautet die Relation der entsprechenden Trägermengen AN at ⊆ AInt . In der Sortenhierarchie ist Mehrfachvererbung möglich. Das bedeutet, dass eine Sorte mehrere direkt übergeordnete Sorten haben kann. Als Vorteil einer solchen Sortenhierarchie wird im Verlauf dieser Arbeit die Fehlerbehandlung mit OBJ näher beleuchtet [GWM+ 00, S. 5]. Wie in anderen Programmiersprachen (z. B. C++ oder Java) sind in OBJ polymorphe Operationen möglich. Der Begriff Polymorphie wird im Kontext von Operationen allgemein so verstanden, dass es für ein Operationssymbol mehrere Bedeutungen geben kann. Stehen die Argument- und Ergebnissorten der Operationen nicht in Relation zueinander, so sprechen [GM92] von Ad-hoc-Polymorphismus“. Das Beispiel eines polymorphen Operators + für ” die Addition natürlicher Zahlen und die Konkatenation von Strings, vgl. Listing 3, illustriert dies. op _+_ : Nat Nat -> Nat . op _+_ : String String -> String . Listing 3: Ad-hoc-Polymorphismus am Beispiel der Additions-Operation. Sind Argument- und Ergebnissorten der Operationen mit gleichem Operations-Symbol zudem hierarchisch angeordnet, z. B. Nat < Int, so heißt diese Art der Überladung nach [GM92] Subsort-Polymorphismus“. In Listing 4 ist dieser Polymorphismus am Beispiel ” des Operators + angewendet. Gemäß der Zahlenhierarchie sind die natürlichen Zahlen Nat als Untermenge der ganzen Zahlen Int definiert. Die Additions-Operation benutzt sowohl für natürliche als auch ganze Zahlen das Symbol +. Aufgrund der Hierarchie der Sorten ist Op 2 eine Spezialisierung von Op 1 derart, dass das Ergebnis der Summe zweier natürlicher Zahlen wieder eine natürliche Zahl ist. Die gemischte Summe ist hingegen eine ganze Zahl. subsort Nat < Int . op _+_ : Int Int -> Int . *** Op 1 op _+_ : Nat Nat -> Nat . *** Op 2 Listing 4: Subsort-Polymorphismus am Beispiel der Additions-Operation. Auf der Ebene der Algebra ist diejenige Funktion, die die Operation mit der bzgl. der Sortenhierarchie kleineren Argumentsorte interpretiert, einschränkend gegenüber der Funktion, die die Operation mit der größeren Argumentsorte interpretiert. Dies bedeutet, dass die Addition natürlicher Zahlen (Op 2) zum gleichen Ergebnis führen muss, wie die Integer-Addition (Op 1) der gleichen natürlichen Zahlen [GWM+ 00, S. 8]. Zwischen einer Signatur und einer Algebra ist keine eindeutige Eins-zu-Eins Verbindung möglich. Mehrere Signaturen können eine Algebra beschreiben, eine Signatur kann durch 5 Kapitel 2: Grundlagen algebraischer Spezifikation mehrere Algebren interpretiert werden. Für den ADT der natürlichen Zahlen mit der Signatur ΣN AT lautet eine ΣN AT -Algebra A wie folgt: Die Trägermenge AN at = {0, 1, 2, ...} enthält als Datenobjekte die natürlichen Zahlen, die Konstante ist als a0 = 0 definiert und der Operator s als as (n) = n+1. Als Algebra für die natürlichen Zahlen wird diese Interpretation zweifelsohne als die nächstliegende anerkannt werden. Sie wird daher auch StandardInterpretation genannt [GM96, S. 26]. Jedoch ist sie nicht die einzige: BN at = {0, 1}, b0 = 0 und bs (n) = 1 − n ist ebenfalls eine gültige Algebra. Für eine bestimmte Klasse von Modellen lässt sich hingegen keine Standard-Interpretation ausmachen. In OBJ wird dieser Unterscheidung Rechnung getragen: Ein OBJ-Modul wird durch die Schlüsselwörter obj und endo eingeschlossen, wenn eine Standard-Interpretation beabsichtig ist. Es handelt sich dann um ein Objekt. Soll andernfalls eine Menge von Algebren beschrieben werden, so heißt das Modul Theorie und wird durch die Schlüsselwörter th und endth eingeschlossen. Eine Denotation ist eine Abbildung δ : Σ → A vom syntaktischen Bereich der Signatur Σ in den semantischen Bereich der Algebra A. Darauf aufbauend lassen sich mithilfe der folgenden Definition die Beziehungen zwischen zwei Algebren beschreiben. Definition 2.3 A und B seien Algebren zur gleichen Signatur Σ mit den Denotationen δA : Σ → A und δB : Σ → B. Ein Homomorphismus h : A → B ist eine Menge von Abbildungen h : δA (s) → δB (s). Für jede Sorte s existiert eine Abbildung h, die den Datenobjekten der Algebra A die Objekte der Algebra B zuordnet, so dass gilt [HL89, S. 20]: 1. Für jede nullstellige Operation σi :→ s: h(δA (σi )) = δB (σi ). 2. Für jede mehrstellige Operation σi : s1 ×...×sn → s mit n > 0 und den passend getypten Objekten t1 , ..., tn die zu den Mengen δA (s1 ), ..., δA (sn ) gehören: h(δA (σi )(t1 , ..., tn )) = δB (σi )(h1 (t1 ), ..., hn (tn )). Ein Homomorphismus von A nach B wird nach dieser Definition so verstanden, dass jeder Operation von A die entsprechende Operation von B zugeordnet wird. Gilt jede Abbildung h auch bijektiv, so handelt es sich um einen Isomorphismus zwischen den beiden Algebren [HL89, S. 20]. Abgesehen von der Benennung der Objekte sind die Algebren dann identisch. Das kommutative Diagramm in Abbildung 1 illustriert die Beziehung der Algebren anhand der mehrstelligen Operation σi . Kommutativ bedeutet in diesem Zusammenhang, dass das Resultat h(t) gleich bleibt, unabhängig davon, ob die Operation σi oder die Abbildung h zuerst angewendet wird [EGL89, S. 23]. 6 Kapitel 2: Grundlagen algebraischer Spezifikation A B h1 ,...,hn (t1 , ..., tn ) −−−−→ (h1 (t1 ), ..., hn (tn )) δ (σ ) δ (σ ) yB i yA i t h −−−→ h(t) Abbildung 1: Kommutatives Diagramm der Algebren A und B. 2.4 Denotationale Semantik Um der Spezifikation eine semantische Bedeutung zu geben, reicht die Betrachtung der einzelnen Operationen nicht aus. Erst die Kombination zu Termen und das Aufstellen der Gleichungen erlaubt es letztendlich, die Eigenschaften zu beschreiben. Jedem syntaktischen Objekt wird ein mathematisches, semantisches Objekt zugeordnet. Dies wird auch Denotation genannt, weshalb insgesamt von der denotationalen Semantik gessprochen wird. Die Menge der Gleichungen einer Spezifikation werden auch Axiome genannt. Um gültige Terme aufzustellen, bedarf es einer Termsprache: Definition 2.4 Sei Σ eine Signatur mit einer Menge von Sorten S und X eine Menge getypter Variablen, dann ist eine Termsprache TΣ wie folgt definiert [HL89, S. 28]: 1. Jede Variable xi vom zugehörigen Typ si ist ein Σ-Term vom Typ si . 2. Jede Konstante σi :→ s ist ein Σ-Term vom Typ s. 3. Sei σi : s1 × ... × sn → s mit n > 0 und t1 , ..., tn Terme der Sorten s1 , ..., sn , dann ist σ(t1 , ..., tn ) ein Σ-Term vom Typ s. 4. Jedes weitere Element der Termsprache kann in einer endlichen Anzahl von Schritten so abgeleitet werden. Die Termsprache die durch den ADT Stack definiert wird, enthält unter anderem folgende Terme: {empty, push(X, empty), top(push(X, empty)), ...}. Nachdem eine OBJ-Datei geladen wurde, kann am OBJ-Prompt1 mittels parse überprüft werden, ob es sich um einen gültigen Term handelt. Eine OBJ-Datei wird durch den Befehl in Name geladen. Der Name entspricht dem Dateinamen ohne dessen Endung. Der parse Ausdruck arbeitet immer auf dem zuletzt geladenen Modul, welches durch open Modulname, z. B. open STACK-OF-NUMBER, geändert werden kann. Weiterhin lässt sich 1 Als OBJ-Implementationen kommen in dieser Arbeit OBJ3 und BOBJ zum Einsatz, vgl. Anhang. 7 Kapitel 2: Grundlagen algebraischer Spezifikation über die Ausgabe sicherstellen, das der Parser die Bindungsreihenfolge (Auswertung des Präzedenz-Wertes, vgl. Kapitel 2.2) gemäß Intention interpretiert. Die Ausgabe erfolgt bei BOBJ in einer Baumstruktur, vgl. Abbildung 2. top : NeStack -> Nat push : Nat Stack -> NeStack var X : Nat empty : -> Stack Abbildung 2: Ausgabe als Baumstruktur von parse top(push(X, empty)). Eine Spezifikation (Σ, E) ist eine Signatur Σ, die um eine Menge von Gleichungen E erweitert wurde. In OBJ gibt es zwei Gleichungstypen für die grundsätzlich gilt, dass alle verwendeten Variablen im Modul deklariert werden müssen. Eine Gleichung hat die Form eq t1 = t2 mit t1 , t2 als gültigen Termen der gleichen Sorte aus der Termsprache TΣ . In bestimmten Fällen kann es hilfreich sein, dass eine Gleichung nur unter einer gegebenen Bedingung als wahr gilt. Dieser zweite Typ bildet die bedingten Gleichungen der Form cq t1 = t2 if t3 mit t1 , t2 , t3 ∈ TΣ . Es gelten hierbei dieselben Regeln wie bei einfachen Gleichungen mit der Ergänzung, dass der dritte Term von der Sorte Bool stammt.2 Die Gleichung t1 = t2 muss nur dann gelten, wenn t3 wahr ist [GWM+ 00, S. 10]. Auf der Ebene der Algebra kann nun bestimmt werden, ob diese eine Spezifikation (Σ, E) erfüllt. Folgendes Beispiel verdeutlicht, dass dies offensichtlich von der Wahl der Algebra abhängt. Sei Σ eine Algebra mit der binären Infix-Operation +, As = {a, b} die Trägermenge einer zugehörigen Algebra und a+ eine Verknüpfung zur Konkatenation von Listen. Mit X, Y als Variablen der Sorte Liste lautet das kommutative Gesetz wie folgt: X +Y = Y +X. Dieses wird von dieser Algebra jedoch nicht erfüllt, denn für z. B. X = ab und Y = ba gilt X + Y = abba 6= baab = Y + X [HL89, S. 25]. Um zu zeigen, dass eine Gleichung erfüllt ist, wird mithilfe des Begriffs der Substitution festgelegt, wie mit Variablen in Termen zu verfahren ist. Definition 2.5 Sei TΣ die Menge der Terme über eine Signatur Σ und X die Menge getypter Variablen, dann ist eine Substitution wie folgt definiert: θ : X → TΣ , wobei x ∈ X und θ(X) denselben Typ haben. Eine Grundsubstitution liegt vor, wenn eine Substitution zu einem variablenfreien Term führt [HL89, S. 28]. Abbildung 3 veranschaulicht anhand des Terms push(x, y) aus dem ADT Stack zwei 2 Die Sorte Bool gehört zu den vordefinierten Sorten in OBJ, die standardmäßig zur Verfügung stehen. Bool wird darüber hinaus automatisch in alle OBJ-Module importiert. 8 Kapitel 2: Grundlagen algebraischer Spezifikation Substitutionen bis hin zu einem variablenfreien Term. push(x, y) x → s(0) y push(s(0), y) y → push(0, y empty) push(s(0), push(0, empty)) Abbildung 3: Substitution des Terms push(x, y) des ADT Stack. Auf der bisher beschriebenen mathematischen Grundlage kann nun ausgedrückt werden, wann eine Algebra A mit einer Denotation δA : Σ → A die Spezifikation (Σ, E) erfüllt. A erfüllt eine Gleichung e ∈ E : t1 = t2 falls δA (t1 ) = δA (t2 ) für alle Grundsubstitutionen gilt. A erfüllt eine bedingte Gleichung e ∈ E : t1 = t2 if t3 falls δA (t1 ) = δA (t2 ) für alle Grundsubstitutionen gilt, für den Fall, in dem δA (t3 ) = true ist. A erfüllt die Spezifikation, falls alle Gleichungen derart erfüllt werden [GM96, S. 25 f.]. Der Begriff der Erfülltheit ist also auf der Ebene der Objekte der Algebra definiert. Linke und rechte Seite der variablenfreien Terme sind genau dann gleich, wenn diese jeweils das gleiche Objekt denotieren. Am Beispiel der Signatur der natürlichen Zahlen wurde in Kapitel 2.3 bereits informal eingeführt, dass es mehrere Algebren zu einer Spezifikation geben kann. Diese Eigenschaft ist auch unter dem Begriff der Varietät bekannt. In OBJ wird der Ansatz der initialen Algebra verfolgt, um die Standard-Interpretation zu identifizieren. Die zugrunde liegende Idee ist, dass zwei variablenfreie Terme verschiedene Objekte denotieren, falls auf der Basis der Axiome nicht gezeigt werden kann, dass sie gleich sind. Sie ist wie folgt definiert: Definition 2.6 Sei Σ eine Signatur und E eine Menge von Σ-Gleichungen dann ist A eine initiale ΣAlgebra, wenn folgende Eigenschaften gelten [GM96, S. 36]: 1. no junk: Jedes Objekt von A kann durch einen Σ-Term repräsentiert werden. 2. no confusion: Für jede Gleichung variablenfreier Terme kann mithilfe der Axiome gezeigt werden, dass A sie erfüllt. Die Definition der Isomorphie, vgl. Definition 2.3, ermöglicht es, von der“ initialen Alge” bra zu sprechen, weil verschiedene Benennungen der Objekte auf Ebene der Algebra keine Rolle spielen: Sei I eine initiale Algebra, dann gehören alle Algebren der Varietät zu dieser Klasse der initialen Algebra, wenn sie isomorph zu I sind. Hier findet sich das Prinzip der Abstraktion: Die Implementierung ist unerheblich, solange die Resultate übereinstimmen. 9 Kapitel 2: Grundlagen algebraischer Spezifikation 2.5 2.5.1 Operationale Semantik Termersetzungssysteme Bei der algebraischen Spezifikation werden Programme durch Terme repräsentiert. Über das im vorangegangenen Kapitel vorgestellte Modell hinaus stellen Termersetzungssysteme einen Algorithmus bereit, der das Ergebnis eines Terms berechnet. Gemäß ihres ausführbaren Charakters ist diese Eigenschaft unter dem Namen operationale Semantik bekannt [GM96, S. 36]. Eine Anfrage an das Termersetzungssystem wird mit dem Befehl reduce ausgeführt (wie parse bezieht sich die Anfrage auf das aktuell geöffnete Modul). Eine Anfrage bzgl. des ADT Stack, bei der nacheinander die Peano-Zahlen null und eins auf dem Stack abgelegt werden, lautet reduce top(push(s(0), push(0, empty))). Das System liefert mit Nat: s(0) die korrekte Antwort. Aufbau und Funktionsweise des Termersetzungssystems sind Gegenstand dieses Kapitels. Ein Termersetzungssystem (engl. Term Rewriting System) besteht aus einer endlichen Menge an Termersetzungsregeln t1 ⇒ t2 . Damit eine Gleichung t1 = t2 eine Termersetzungsregel im Sinne von OBJ ist, müssen folgende Regeln erfüllt sein [GWM+ 00, S. 9]: 1. Die Terme beider Seiten haben die gleiche oder eine gemeinsame übergeordnete Sorte. 2. Die Sorte des linken Terms (engl. Left Hand Side“ (LHS)) ist größer oder gleich der ” Sorte des rechten Terms (engl. Right Hand Side“ (RHS)). ” 3. Alle Variablen, die auf der rechten Seite eines Terms benutzt werden, sind auch auf der linken Seite des Terms vorhanden. 4. Der linke Term ist keine Variable. Diese Art der Spezifikation, bei denen Termersetzungssysteme zum Einsatz kommen, wird auch konstruktive Spezifikation genannt. Sie hat den Vorteil, dass sie meist direkt ausgeführt werden kann und somit Rapid Prototyping unterstützt [HL89, S. 120]. Für eine konstruktive Spezifikation ist die Aufteilung der Operatoren in zwei Gruppen charakteristisch. Die Gruppe der Konstruktoren generiert Objekte des abstrakten Datentyps, wohingegen die verbleibenden Operationen, auch Selektoren, das funktionale Verhalten der Objekte beschreiben [HL89, S. 123]. Im Beispiel des ADT Stack sind empty und push Konstruktoren sowie pop und top Selektoren. In OBJ ist es im Gegensatz zu anderen Spezifikationssprachen, wie z. B. Maude, nicht nötig, die Konstruktoren durch ein Attribut zu 10 Kapitel 2: Grundlagen algebraischer Spezifikation identifizieren. Beim Aufstellen der Axiome ist die Unterscheidung der Operationen jedoch sinnvoll. Es ist ein typisches Muster zum Aufstellen der Axiome, für jedes Paar aus Konstruktor und zugehörigem Selektor eine Gleichung zu bilden. Der linke Term der Gleichung besteht bei dieser Vorgehensweise aus einem Selektor, der einen Konstruktor als Argument hat. Die Form der Gleichungen, insbesondere der Gebrauch von Variablen, unterliegt gewissen Beschränkungen. Die oben aufgeführten Regeln für das Aufstellen der Gleichungen sind deshalb auch als Konstruktivitätsbedingungen bekannt. Beim ADT Stack wurden nur Gleichungen aus Paaren der Selektoren top und pop mit dem Konstruktor push gebildet. Die ungültige Anwendung von top und pop auf den leeren Stack, empty, wird im Rahmen der Fehlerbehandlung mit OBJ in Kapitel 3.2 behandelt. Die Funktionsweise der Termersetzung soll der Einfachheit halber am Beispiel des ADT Natürliche Zahlen, vgl. Listing 5, durchgeführt werden. Das entsprechende OBJ-Modul wurde dazu um Gleichungen ergänzt (vgl. ursprüngliches Listing 2 in Kapitel 2.2). Sie erfüllen die Konstruktivitätsbedingungen und sind daher gültige Termersetzungsregeln. obj NAT is sort Nat . op 0 : -> Nat . op s : Nat -> Nat [prec 15] . op _+_ : Nat Nat -> Nat [assoc comm prec 40] . op _*_ : Nat Nat -> Nat [assoc comm prec 30] . vars X Y : Nat . eq 0 + X = X . eq s(X) + Y = s(X + Y) . eq 0 * X = 0 . eq s(X) * Y = (X * Y) + Y . endo *** 1 *** 2 *** 3 *** 4 Listing 5: ADT Natürliche Zahlen in Peano-Notation. Die Prozedur lautet wie folgt: Für eine Signatur Σ sei t0 ein zu reduzierender Term aus der Termsprache t0 ∈ TΣ gegeben. Gemäß Definition 2.5 sei θ eine Substitution, um Bindungen zwischen Variablen und Termen herzustellen. Eine Termersetzungsregel ei : tl ⇒ tr kann auf den Teilterm t00 von t0 angewendet werden, falls die Variablen der linken Seite der Termersetzungsregel so substituiert werden können, dass θ(tl ) = t00 gilt. t00 wird dann durch die substituierte Rechte Seite θ(tr ) der Regel in t0 ersetzt. Die Menge der Teilterme schließt auch t0 selbst mit ein. Es wird dann von einer direkten Übereinstimmung gesprochen [GWM+ 00, S. 12]. Insbesondere für Übereinstimmungen (engl. matches) von Teiltermen ist es notwendig, dass die Substitutionen im Kontext des zu reduzierenden Terms stattfinden. Denn es ist 11 Kapitel 2: Grundlagen algebraischer Spezifikation möglich, dass eine Ersetzungsregel in einer mehrschrittigen Regelanwendung mehrfach mit verschiedenen Variablenbindungen vorkommt [GM96, S. 31]. Eine Anwendung einer Regel ei auf zwei Terme wird im Folgenden durch das Symbol t0 ⇒ei t1 ausgedrückt. Wird durch die Abfolge von Termersetzungen ein Term erreicht, auf den keine Termersetzungsregel angewendet werden kann, dann ist der Term in Normalform. Dieser Term ist das Ergebnis der Berechnung eines Ausdrucks [GM96, S. 32]. Die Termersetzung am Beispiel:3 0 + s(0) * 0 ⇒e1 s(0) * 0 ⇒e4 0 * 0 + 0 ⇒e3 0 + 0 ⇒e1 0 Wenn durch eine Variablensubstitution eine Übereinstimmung mit der linken Seite einer bedingten Termersetzungsregel ei : tl ⇒ tr if tc gefunden wurde, verläuft die Prozedur ein wenig anders. Es werden die Variablen der Bedingung tc mit derselben Substitution gebunden und überprüft, ob der Term entsprechend der Bool-Sorte wahr wird. Nur dann findet die Ersetzung durch die rechte Seite statt. Die Bindung der Variablen innerhalb des Kontextes ist auch hier zwingend notwendig. Die Auswertung der Bedingung kann nämlich zu weiteren Ersetzungsschritten führen, wobei es z. B. möglich ist, dass die ursprüngliche bedingte Ersetzungsregel erneut angewendet wird [GWM+ 00, S. 13]. Die Termersetzung läuft in der Regel nicht so trivial wie in obigem Beispiel ab. Es sind bestimmte Eigenschaften wünschenswert. Ein Termersetzungssystem erfüllt die ChurchRosser Eigenschaft, falls die Reihenfolge der Termersetzungen keine Rolle spielt. Wird ein Term t0 auf zwei verschiedene Weisen zu den Termen t1 und t2 umgeschrieben, dann gibt es einen Term t3 , zu dem t1 und t2 umgeschrieben werden können. Das System terminiert, wenn es keine endlose Abfolge von Termersetzungen gibt, wie z. B. t0 ⇒e1 t1 ⇒e2 ... . Wenn ein Termersetzungssystem Church-Rosser ist und terminiert, dann heißt es kanonisch [GM96, S. 37]. Diese Eigenschaft ist jedoch unentscheidbar. Mittels Algorithmus (Knuth-Bendix) lässt sich nur für ein terminierendes System zeigen, dass es zu einem eindeutigen Ergebnis führt. Laut den Autoren von OBJ stellt dieses Entscheidungsproblem jedoch kein Problem dar, da das resultierende Termersetzungssystem nahezu immer kanonisch ist. Der Grund dafür ist, dass die Gleichungen meist intuitiv primitiven Rekursionen folgen [GWM+ 00, S. 17]. Bei binären Operationen können die Eigenschaften der Assoziativität, Identität und Kommutativität direkt als Attribute in der Signatur gesetzt werden, und nicht als Regeln in den 3 Die Unterstriche verdeutlichen, ob es sich um die Übereinstimmung eines Teilterms oder des ganzen Terms handelt. 12 Kapitel 2: Grundlagen algebraischer Spezifikation Axiomen. Sie betreffen dann nicht nur die Syntax (Parsen von Ausrücken), sondern auch die Semantik (Reihenfolge der Auswertung). Für die Assoziativität hat das Attribut assoc in op _+_ : Nat Nat -> Nat [assoc] die gleiche Bedeutung wie eq (M + N)+ P = M + (N + P). Mit dem Attribut id: 0 kann ein Term angegeben werden, hier 0, für den die Identität der Operation gilt. Die Identität würde für der Addition der Gleichung eq M + 0 = M entsprechen. Das Attribut comm ersetzt die Bedeutung der Gleichung eq M + N = N + M. Letztere würde zu einer nicht terminierenden Abfolge von Ersetzungen führen a + b ⇒ b + a ⇒ a + b ⇒ ... . Es ist daher zwingend notwendig, die Attribute den Gleichungen vorzuziehen, da sie nicht als normale Termersetzungsregeln ausgewertet werden [GM96, S. 33]. 2.5.2 Gleichungsbasiertes Schließen Während beim Termersetzungsverfahren die Ersetzungsregeln nur von links nach rechts, oder auch vorwärts“, angewendet werden, sind beim gleichungsbasierten Schließen bei” de Richtungen möglich. Unter Zuhilfenahme von Schlussregeln (Reflexivität, Symmetrie, Transitivität, Substituierbarkeit und Kongruenz) können aus der Menge der Gleichungen E neue Axiome abgeleitet werden. Weil die Anwendung der Regeln von rechts nach links nicht automatisiert ablaufen kann, stellt OBJ Befehle zur manuellen Anwendung bereit (apply, start) [GWM+ 00, S. 50]. Da in dieser Arbeit der Fokus auf Termersetzungssystemen als operationale Semantik liegt, wird auf eine detaillierte Betrachtung verzichtet. Es ist aber anzumerken, dass das gleichungsbasierte Schließen einen Vorteil bzgl. des mathematischen Modells bietet: Jede Gleichung, die bzgl. aller gültigen Modelle wahr ist, kann auch hergeleitet werden. Termersetzungssysteme erfüllen diese Vollständigkeit aufgrund ihrer gerichteten Regeln nur selten [GM96, S. 32]. 13 Kapitel 3: Erweiterte Eigenschaften von OBJ 3 Erweiterte Eigenschaften von OBJ 3.1 Modularisierung Modularisierung bezeichnet die Zerlegung von Problemen in Teilprobleme bis diese eine angemessene Größe zur Lösung des Problems darstellen. Aus diesem Devide and Conquer“” Ansatz ergeben sich Vorteile für den gesamten Entwicklungsprozess: Erst die Zerlegung in Teilprobleme macht eine verteilte Bearbeitung durch mehrere Personen möglich. Durch Reduktion der Problemgröße lässt sich dieses darüber hinaus einfacher erfassen. In OBJ wird dieser Ansatz durch eine Modulstruktur unterstützt, in der eine Art Vererbung der Definitionen möglich ist [LEW96, S. 9]. Bei der Definition eines neuen OBJ-Moduls ist es möglich, bereits definierte Module zu importieren. Dies wurde bereits beim ADT Stack natürlicher Zahlen benutzt, in welches der ADT Natürliche Zahlen durch das Schlüsselwort protecting importiert wurde. Importe verhalten sich transitiv in folgendem Sinn: Wenn ein Modul M ein Modul M 0 importiert, welches wiederum M 00 importiert, dann wird M 00 auch in M importiert. Es können gleichzeitig mehrere Module importiert werden [GWM+ 00, S. 26]. Es existieren im wesentlichen drei verschiedene Arten, wie die Definitionen aus M 0 in das andere Modul M übernommen werden. Diese unterscheiden sich bezüglich der Einhaltung von Eigenschaften der initialen Algebra beim Import. No junk: Es werden keine neuen Objekte für die Sorten von M 0 durch Konstruktoren in M hinzugefügt. No confusion: Die Bedeutung von Operatoren aus M 0 wird nicht durch neue Gleichungen verändert, d. h. in M wird keine Gleichheit für ungleiche Objekte in M 0 eingeführt [GM96, S. 41]. Die möglichen Import-Arten in OBJ sind: 1. protecting: Garantiert no junk und no confusion durch Import. Die grundsätzliche Idee ist, die Bedeutung des importierten Moduls nicht zu verändern. 2. extending: Garantiert no confusion durch Import. Es ist möglich, den Sorten von M 0 neue Objekte hinzuzufügen. 3. including: Keine Garantie. OBJ verfügt über eine Reihe vordefinierter Module von Standardtypen (u. a. BOOL, NAT, INT und FLOAT), die sich bei der Spezifikation ohne Einschränkungen importieren lassen. Weiterhin besteht die Möglichkeit, ein bereits definiertes Modul (einschließlich der vordefinierten Module) zu überschreiben, indem ein neues Modul mit dem gleichen Namen definiert wird. 14 Kapitel 3: Erweiterte Eigenschaften von OBJ 3.2 Fehlerbehandlung Bei der Definition von abstrakten Datentypen kommt es vor, dass Operationen für bestimmte Wertebereiche nicht definiert sind. Ein typisches Beispiel beim ADT Stack ist die Anwendung der Operationen top und pop auf den leeren Stack. In Pseudo-Spezifikationssprachen findet sich dann häufig unter den Axiomen die Gleichung top(empty)=undefined, die Interpretation dieses Ausdrucks ist jedoch unklar. Aufgrund der partiell geordneten Sortenhierachie (Order-Sorted Algebra) bietet OBJ mit Retracts ein Konzept, dass den Fehlerfall besser in die Spezifikation integriert [GWM+ 00, S. 15]. Der ADT Stack natürlicher Zahlen wird so modifiziert, dass NeStack eine Untersorte von Stack ist, vgl. Listing 6.4 NeStack hat die Bedeutung eines Stacks mit mindestens einem Element. Zu beachten ist, das top und pop nun als Argumentsorte jeweils NeStack haben, und push die Rückgabesorte NeStack, also den nichtleeren Stack, hat. obj STACK-OF-NAT is protecting NAT . sorts Stack NeStack . subsort NeStack < Stack . op empty : -> Stack . op push : Nat Stack -> NeStack . op top : NeStack -> Nat . op pop : NeStack -> Stack . var X : Nat . var S : Stack . eq top(push(X, S)) = X . eq pop(push(X, S)) = S . endo Listing 6: ADT Stack natürlicher Zahlen mit Retracts. Für die zwei Sorten NeStack und Stack fügt OBJ automatisch eine Retract-Operation mit einer Gleichung ein. Retract und Gleichung sind in Listing 7 expliziert. Die Gleichung wird nur dann wahr, wenn das Argument ein Objekt der Untersorte NeStack ist. op r:Stack>NeStack : Stack -> NeStack . var X : NeStack . eq r:Stack>NeStack(X) = X . Listing 7: Retract-Operation für den ADT Stack. Falls die Argumentsorte von top oder pop nicht direkt als NeStack vorliegt, sondern von einer übergeordneten Sorte ist, ersetzt OBJ das Argument bereits beim Parsen durch ein 4 Modifikationen des ADT Stack zur vorangegangenen Definition, vgl. Listing 1, werden durch Unterstreichungen hervorgehoben. 15 Kapitel 3: Erweiterte Eigenschaften von OBJ Retract, vgl. Listing 8. Das ursprüngliche Argument wird zum Argument des Retracts. Da empty die Gleichung des Retracts nicht erfüllt, bleibt der Ausdruck als Ergebnis der Reduktion stehen und signalisiert dadurch einen Fehler. reduce top(empty) . result: top(r:Stack>NeStack(empty)) Listing 8: Automatisches Einfügen der Retract-Operation. Im folgenden Beispiel, vgl. Listing 9, wird ebenfalls ein Retract gebildet, da die Rückgabe von pop von der Sorte Stack ist. Während der Termersetzung wird das Argument des Retracts zu einem Term von Typ NeStack umgeschrieben, so dass der Retract wahr wird und im Ergebnis entfällt. reduce top(pop(push(0, push(0, empty)))) . => top(r:Stack>NeStack(pop(push(0, push(0, empty))))) => top(r:Stack>NeStack(push(0, empty))) => top(push(0, empty)) result: 0 Listing 9: Auflösen der Retract-Operation. An potentiellen Fehlerstellen fügt OBJ dank der Sortenhierarchie automatisch Retracts ein. Falls diese sich im Verlauf der Termersetzung nicht auflösen, zeigen sie im Ergebnis an, an welcher Stelle ein Fehler aufgetreten ist. 3.3 Generizität Aus Gründen der Abstraktion und Wiederverwendung ist es wünschenswert, Spezifikationen zu parametrisieren. Beispielsweise ist das Verhalten eines Stacks nicht davon abhängig, von welchem Typ die Objekte sind. Anstatt mehrere Spezifikationen für verschiedene Sorten, z. B. NAT oder INT, zu erstellen, wird ein generischer Stack definiert. Dieser Ansatz ist unter dem Begriff Generizität bekannt.5 Dadurch reduzieren sich Spezifikationsaufwand und Fehleranfälligkeit. Bei der Spezifikation eines konkreten Systems werden oft mehrere generische Module jeweils mit OBJ-Objekten als konkretem Parameter instanziert [GWM+ 00, S. 33]. Ein Modul wird bei der Definition wie folgt parametrisiert: obj MODUL[X :: ADD]. Hierbei ist ADD eine OBJ-Theorie (vgl. Kapitel 2.3). Im Gegensatz zu OBJ-Objekten, die abgesehen vom Isomorphismus genau eine initiale Algebra haben, hat eine Theorie die Varietät 5 In C++ findet sich dieses Konzept unter dem Namen der Templates; in Java gibt es seit der Version J2SE 5.0 Generics. 16 Kapitel 3: Erweiterte Eigenschaften von OBJ von Algebren. Im Kontext der Parametrisierung legt eine Theorie daher die Schnittstelle des Parameters fest. Die Algebra der Theorie muss dann durch ein OBJ-Modul erfüllt werden. In Listing 10 legt die Theorie ADD fest, dass die OBJ-Objekte eine Additions-Operation definiert haben müssen, um die Theorie zu erfüllen. th ADD is sort Elt . op _+_ : Elt Elt -> Elt . endth Listing 10: Theorie als Schnittstellendefinition. Der generische Stack wird mit dem Parameter der Theorie ADD parametrisiert, vgl. Listing 11. Bei der Definition des Stacks können die Sorten und Operationen von ADD benutzt werden. obj STACK[X :: ADD] is sorts Stack NeStack . subsort NeStack < Stack . op empty : -> Stack . op push : Elt Stack -> NeStack . op top : NeStack -> Elt . op pop : NeStack -> Stack . var X : Elt . var S : Stack . eq top(push(X, S)) = X . eq pop(push(X, S)) = S . endo Listing 11: Generischer Stack. Ein OBJ-Objekt kann eine Theorie in mehreren Weisen erfüllen. Mittels eines Views werden deshalb die Sorten und Operationen von Theorie und Objekt eindeutig einander zugeordnet. OBJ generiert automatisch einen Standard-View, indem es versucht, die in den Modulen zuerst definierten Sorten gleichzusetzen, vgl. Listing 12. In den meisten Fällen entfällt deshalb die Definition eines Views. view NAT2ADD from ADD TO NAT is sort Elt to Nat . op _+_ to _+_ . endv. Listing 12: Zuordnung zwischen Theorie und Objekt durch einen View. Bei der Instanzierung, z. B. protecting STACK[NAT], wird dem Modul ein bzgl. der Theorie passendes OBJ-Objekt als Parameter übergeben. Der Standard-View wird dann implizit benutzt [GWM+ 00, S. 40 f.]. 17 Kapitel 3: Erweiterte Eigenschaften von OBJ 3.4 Funktionales Prototyping OBJ ist gemäß ihren Autoren nicht nur eine Spezifikationssprache, sondern dank ihrer operationalen Semantik eine funktionale Programmiersprache. Sie umfasst ein Typsystem sowie Operationen, deren Funktionalität durch Gleichungen definiert ist. OBJ kann daher zur Entwicklung von Prototypen eingesetzt werden. Die Autoren stellen die Hypothese auf, dass kleine bis mittlere Systeme dank moderner Hardware ganz ohne Implementation in einer anderen Programmiersprache auskommen [GM00]. Dieser Frage soll durch Messung der Ausführungsgeschwindigkeit einer OBJ-Spezifikation im Vergleich zur Ausführung in funktionalen Sprachen nachgegangen werden. Evaluiert werden OBJ3 und BOBJ als OBJImplementationen sowie Haskell und Lisp als funktionale Programmiersprachen. Wie in Kapitel 2.5.1 beschrieben, spielt Rekursion beim Aufstellen der Gleichungen in einer OBJ-Spezifikation eine große Rolle. Als Testkandidat wird daher die binär-rekursive Fibonacci-Funktion nach [Sed03, S. 219] evaluiert, vgl. Listing 13. obj FIB is protecting INT . op fib : Int -> Int . vars M N : Int . eq fib(0) = 0 . eq fib(1) = 1 . cq fib(N) = fib(N - 1) + fib(N - 2) if N >= 2 . endo Listing 13: Rekursive Fibonacci-Funktion als OBJ-Spezifikation. Die Ergebnisse der Messung sind in Abbildung 4 dargestellt. Die Ausführungsgeschwindigkeit beider OBJ-Implementationen ist erwartungsgemäß langsamer als die von Lisp und Haskell. Das in Java implementierte BOBJ schneidet im Vergleich zu dem in Lisp entwickelten OBJ3 schlechter ab. Abbildung 4: Laufzeit der Fibonacci-Funktion. 18 Kapitel 3: Erweiterte Eigenschaften von OBJ Durch eine zweite Messung von Kennzahlen des Termersetzungssystems soll der durch die Termersetzung generierte Aufwand eingeschätzt werden. Aus Abbildung 5 lässt sich entnehmen, wie viele Übereinstimmungsversuche der linken Gleichungsseite durchgeführt wurden und wie hoch die Anzahl der Termersetzungen war. Der durch Rekursion verursachte Aufwand für das Termersetzungssystem liegt z. B. für f ib(25) bei bereits über einer Million Termersetzungen. Die exponentielle Zuwachsrate beider Kennzahlen entspricht exakt dem √ theoretischen Ergebnis der Aufwandsabschätzung O(n) = ( 5 + 1)n für die FibonacciFunktion [Sed03, S. 219]. Abbildung 5: Kennzahlen des Termersetzungssystems von OBJ3. Aus den Ergebnissen lässt sich ein erster Eindruck über den Aufwand und die benötigte Laufzeit einer OBJ-Spezifikation gewinnen. Die Ergebnisse legen die Vermutung nahe, dass die Ausführungsgeschwindigkeit für Prototypen brauchbar ist. Eine tiefergehende Untersuchung setzt die Betrachtung weiterer Beispiele voraus, die hier jedoch über den Rahmen der Arbeit hinausgeht. 3.5 Theorem Proving Unter dem Begriff Theorem Proving wird im Rahmen der formalen Methoden eine Technik verstanden, mit der Behauptungen bezüglich einer Spezifikation mithilfe eines Programms verifiziert werden. Wie in Kapitel 2 ausgeführt wurde, basiert OBJ auf einem mathematischen Modell mit Gleichungslogik als zentralem Baustein. Auf der Basis eines Axiomensystems in Gleichungsform beweist die Berechnung eines Terms durch Termersetzung eine Behauptung. [GWM+ 00, S. 67]. Die Beweisführung erfolgt in OBJ nicht automatisiert, sie muss manuell gewählt werden. Beweise verlaufen in OBJ häufig nach dem aus der Mathematik bekannten Schema der Induktion. Nach [GM96, S. 195] kann dieses Verfahren auf Modelle angewendet werden, die eine initiale Algebra besitzen. Dies bedeutet, dass die Induktion bei OBJ-Objekten, wie hier 19 Kapitel 3: Erweiterte Eigenschaften von OBJ z. B. dem ADT Stack oder dem ADT Natürliche Zahlen, anwendbar ist. Im Kontext der Spezifikation ist dessen Grundprinzip, dass eine Eigenschaft P für alle Terme wahr ist, wenn P für alle Operationen σ der Signatur Σ wahr ist [GM96, S. 196]. Um dies zu zeigen, wird im Induktionsanfang bewiesen, dass die Behauptung für einen konstanten Term gilt. Für den Induktionsschritt wird das Ergebnis aus dem Induktionsanfang mitbenutzt, um dann zu zeigen, das die Behauptung allgemein für die Anwendung der Operationen auf den konstanten Term gilt. Die Spezifikation eines Taschenrechners in Listing 14 macht Gebrauch von den in dieser Arbeit vorgestellten Möglichkeiten zur Spezifikation mit OBJ. Es umfasst Polymorphismus der Operationen, partiell geordnete Sorten zwecks Fehlerbehandlung sowie Strukturierung durch Modularisierung und Parametrisierung. Bezüglich dieser Spezifikation soll ein Beweis nach dem oben beschrieben Muster der Induktion vorgestellt werden. Der Taschenrechner funktioniert nach dem Prinzip, dass zuerst zwei Zahlen eingegeben werden müssen (Operation enter), die anschließend addiert werden (Operation add). Durch den Mechanismus der Retracts wird zugesichert, dass eine Addition nur dann ausgeführt wird, wenn bereits zwei Zahlen eingegeben wurden. Die Untersorten State1 und State2 repräsentieren daher die Anzahl der bereits eingegebenen Zahlen. obj CALC[X :: ADD] is sort State State1 State2 . subsorts State2 < State1 < State . op init : -> State . *** Signatur op save : Elt State -> State1 . op save : Elt State1 -> State2 . op enter : Elt State1 -> State2 . op enter : Elt -> State1 . op show : State1 -> Elt . op add : State2 -> State1 . vars I I1 I2 : Elt . *** Axiome var S : State . eq enter(I) = save(I, init) . eq enter(I, S) = save(I, S) . eq show(save(I, S)) = I . eq add(save(I1, save(I2, S))) = save(I1 + I2, S) . endo Listing 14: OBJ-Modul eines Taschenrechners. Die Induktion in Listing 15 beweist die Kommutativität der Addition des Taschenrechners. Es wird also gezeigt, dass die Reihenfolge der Eingaben der Zahlen keine Rolle spielt. In OBJ reicht es zur Beweisführung nicht aus, Terme durch das Termersetzungssystem berechnen zu 20 Kapitel 3: Erweiterte Eigenschaften von OBJ lassen. Das Resultat der Berechnungen muss überprüft werden. Im folgenden Beispiel ist der Beweis durch Induktion nur dann gültig, wenn beide Termersetzungen zum Ergebnis true führen. Die Beweisführung fordert einen starken Eingriff des Spezifizierers: Würde der Induktionsanfang nicht zu true ausgewertet, dann wird die Behauptung dennoch in Form der darauf folgenden Gleichung zur Menge der Axiome hinzugefügt. In OBJ gibt es daher einen speziellen Kommentarstil, ***> should be: true, der bei der Termersetzung darauf hinweisen soll, welches Ergebnis erwartet wurde. *** Laden von Modulen in addable in calc in natpeano *** Instanzierung obj CALCNAT is protecting CALC[NAT] . endo *** Beweis per Induktion open CALCNAT . op M : -> Nat . op N : -> Nat . *** Induktionsanfang reduce add(enter(M, enter(0))) == add(enter(0, enter(M))) . ***> should be: true *** Gleichung hinzufügen eq add(enter(M, enter(N))) = add(enter(N, enter(M))) . *** Induktionsschritt reduce add(enter(M, enter(s(N)))) == add(enter(s(N), enter(M))) . ***> should be: true close Listing 15: Beweisführung der Kommutativität für das OBJ-Modul des Taschenrechners. 21 Kapitel 4: Zusammenfassung und Ausblick 4 Zusammenfassung und Ausblick Der Einsatz von OBJ als Spezifikationssprache setzt Kenntnisse in der Begriffswelt der algebraischen Spezifikation voraus. Auf der Basis einer Signatur legen Terme und Gleichungen die Semantik einer Spezifikation fest. Dank des zugrunde liegenden Gleichungsmodells werden Spezifikationen zu Termersetzungssystemen, mit deren Hilfe das Ergebnis von Termen berechnet wird. Diese Berechenbarkeit erlaubt schließlich den formalen Beweis von Eigenschaften des spezifizierten Systems. An dieser Stelle ist unter Rückgriff auf bekannte Vorgehensweisen, wie z. B. die Induktion, Kreativität zur Beweisführung gefordert. Weiterhin ist der Spezifizierer bei der Ausführung von Beweisen weitgehend auf sich gestellt, OBJ bietet hier kaum Unterstützung. Zur Strukturierung von Spezifikationen steht in OBJ ein vielseitiges Modulkonzept bereit. Zusammen mit der Möglichkeit Module zu parametrisieren, ist OBJ eine Spezifikationssprache, mit der auch komplexeren Problemstellungen entgegengetreten werden kann. Denn es können bereits in dieser frühen Phase des Software-Entwicklungsprozesses Probleme in Teilprobleme aufgeteilt werden. Falls eine später gewählte Programmiersprache, wie z. B. Java, diese Konzepte der Modularisierung ebenfalls unterstützt, kann eine Implementierung nahe auf der Spezifikation aufbauen. Die Orientierung an Datentypen und die oft rekursive Art der Gleichungsdefinition bringen eine starke Nähe zu Programmiersprachen mit sich. Bei der Aufstellung der Gleichungen trifft der Spezifizierer auf Konzepte, die aus der funktionalen sowie regelbasierten Programmierung bekannt sind. Daher kann er Gefahr laufen, eher ein Programm anstelle einer Spezifikation zu schreiben. Die Beschäftigung mit OBJ hat gezeigt, dass es oft nicht leicht ist, sich von einem niedrigen Abstraktionsniveau zu lösen. Dieser Eindruck wird durch Beispiele in der Literatur unterstützt. Dort finden sich u. a. die Spezifikation eines Protokolls zur Datenübertragung oder die Spezifikation einer imperativen Programmiersprache. Neuere Ansätze der verhaltensorientierten Spezifikation, die auf der algebraischen Spezifikation aufsetzt, entgegnen dieser Limitierung. Mittels Hidden-Sorts werden Module durch Zustände, Attribute und Methoden erweitert, wie sie aus der Objektorientierung bekannt sind. Darauf aufbauend lässt sich das Abstraktionsniveau bei der Spezifikation weiter erhöhen. Bezüglich des ökonomisch sinnvollen Einsatzes von OBJ ist zu sagen, dass die formale Spezifikation grundsätzlich einen erhöhten Aufwand mit sich bringt. Ihr Einsatz sollte deshalb auf Teilprobleme beschränkt werden. Geeignete Kriterien zu Auswahl stellen z. B. Sicherheit oder entstehende Kosten im Fehlerfall dar. Im Hinblick auf die endgültige Software darf genau wie bei anderen formalen Methoden nicht vergessen werden, dass durch Verifikation nur Eigenschaften des Programms bezüglich seiner Spezifikation bewiesen werden können. Ob Spezifikation und Programm eine Lösung auf die Problemstellung bieten, muss im Rahmen der Validation gesondert geklärt werden. 22 Kapitel A: Software A Software A.1 Übersicht Software BOBJ OBJ3 Haskell (Hugs) Lisp (GNU Common Lisp) Version 0.9 2.09 Nov 2003 2.6.6 CLtL1 Download http://www.cs.ucsd.edu/groups/tatami/bobj http://secure.ucd.ie/products/opensource/OBJ3/ http://haskell.org/hugs http://www.gnu.org/software/gcl/gcl.html Tabelle 1: Verwendete Software. A.2 Installation von OBJ3 OBJ3 ist die neueste Version einer Serie von OBJ-Systemen, die auf die erste Entwicklung von Joseph Goguen 1976 zurückgehen. Das Kommandozeilen-System ist in Common Lisp entwickelt worden. Unter Linux, hier Debian GNU Linux, wird eine ausführbare Version aus den Quellen erstellt. Unter Debian GNU Linux 3.1 sind weiterhin folgende Pakete nötig (durch Abhängigkeiten zu diesen Pakete werden weitere Pakete installiert): • gcl (GNU Common Lisp compiler) • g++ (The GNU C++ compiler) • make (The GNU version of the make“ utility) ” • emacs21-bin-common (The GNU Emacs editor’s shared, architecture) • flex-old (The old version of the fast lexical analyzer) • bison (A parser generator that is compatible with Y) Nach dem Extrahieren der Quellen wird der Übersetzungsvorgang durch make sources angestoßen. Der OBJ3-Prompt kann anschließend durch bin/obj3-gcl gestartet werden. Damit nach reduce angezeigt wird, wie lange die Reduktion gedauert hat, muss die Variable $$time-red im Sourcecode von OBJ3 in source/obj3/top/ci.lsp auf den boolschen Wert t (true) gesetzt werden. 23 Kapitel A: Software A.3 Installation von BOBJ BOBJ gehört zur den neueren Entwicklungen von OBJ-Systemen. Auch hier ist Joseph Goguen als einer der Autoren zu nennen. BOBJ basiert auf den von OBJ3 bekannten Methoden zur algebraischen Spezifikation und Verifikation. Darüber hinaus werden Möglichkeiten zur verhaltensorientierten Spezifikation und Verifikation zur Verfügung gestellt. Abgesehen von Erweiterungen ist die Syntax von BOBJ nahezu identisch zu der von OBJ3. Da BOBJ vollständig in Java realisiert ist, ist es auf jeder Plattform lauffähig, auf der die Java 2 Runtime installiert ist (Download unter http://java.sun.com). Der BOBJ-Prompt wird durch java -cp bobj.jar gestartet. 24 Kapitel B: Funktionales Prototyping B B.1 Funktionales Prototyping Benchmark-System Betriebssystem Prozessor Arbeitsspeicher Debian GNU Linux 3.1 AMD Athlon XP 3000+ 1 GB Tabelle 2: Eigenschaften des Benchmark-Systems. B.2 Quelltext Benchmark-Module module Bench(bench) where import CPUTime bench f = do time0 <- getCPUTime putStr $ "Result: " ++ f ++ "\n" time1 <- getCPUTime putStr $ (show $ (fromIntegral (time1 - time0) / (10ˆ12))) putStr $ " s\n" Listing 16: Benchmark-Funktionen in Haskell. (defun bench (f) (progn (setq time0 (get-internal-run-time)) (setq res (funcall f)) (setq time1 (get-internal-run-time)) (format t "Result: ˜d˜%" res) (format t "Run time: ˜8,4f˜%" (/ (float (- time1 time0)) internal-time-units-per-second)) )) Listing 17: Benchmark-Funktionen in Lisp. B.3 Quelltext Fibonacci-Funktion import Bench fib :: Integer -> Integer fib 0 = 1 fib 1 = 1 25 Kapitel B: Funktionales Prototyping fib n = fib(n-2) + fib(n-1) bench_fib n = bench (show (fib n)) -- bench_fib 20 Listing 18: Rekursive Fibonacci-Funktion in OBJ. import Bench fib :: Integer -> Integer fib 0 = 1 fib 1 = 1 fib n = fib(n-2) + fib(n-1) bench_fib n = bench (show (fib n)) -- bench_fib 20 Listing 19: Rekursive Fibonacci-Funktion in Haskell. (defun fib (n) (if (or (= n 0) (= n 1)) 1 (+ (fib (- n 1)) (fib (- n 2))) ) ) (load "Bench.lsp") (defun bench_fib (n) (bench (lambda () (fib n)))) ; (bench_fib 10) Listing 20: Rekursive Fibonacci-Funktion in Lisp. 26 Literaturverzeichnis Literatur [EGL89] E HRICH, H. D. ; G OGOLLA, M. ; L IPECK, U.: Algebraische Spezifikation abstrakter Datentypen. Stuttgart : Teubner, 1989 [GM92] G OGUEN, J. A. ; M ESEGUER, J.: Order-sorted algebra I: equational deduction for multiple inheritance, overloading, exceptions and partial operations. In: Theor. Comput. Sci. 105 (1992), Nr. 2, S. 217–273. – ISSN 0304–3975 [GM96] G OGUEN, J. A. ; M ALCOLM, G.: Algebraic Semantics of Imperative Programs. Boston, 1996 [GM00] G OGUEN, J. A. ; M ALCOLM, G.: Introduction. In: G OGUEN, J. A. (Hrsg.) ; M ALCOLM, G. (Hrsg.): Software Engineering with OBJ: Algebraic Specification in Action. Boston, 2000 [GWM+ 00] G OGUEN, J. A. ; W INKLER, T. ; M ESEGUER, J. ; F UTATSUGI, K. ; J OUAN NAUD , J.P.: Introducing OBJ. In: G OGUEN , J. A. (Hrsg.) ; M ALCOLM , G. (Hrsg.): Software Engineering with OBJ: Algebraic Specification in Action. Boston, 2000 [HL89] H ORBEEK, I.V. ; L EWI, J.: Algebraic Specifications in Software Engineering. Springer Verlag, 1989 [LEW96] L OECKX, J. ; E HRICH, H. D. ; W OLF, M.: Specification of Abstract Data Types. Wiley, 1996 [Sed03] S EDGEWICK, R.: Algorithms in Java. Addison-Wesley, 2003 27