Bachelorarbeit Vergleich von Unifikations-Algorithmen vorgelegt von Christian Müller Betreuer: Prof. Dr. Rainer Parchmann Torben Wichers Leibniz Universität Hannover Institut für Praktische Informatik Fachgebiet für Programmiersprachen und Übersetzer Hannover, im Oktober 2007 Hannover, den 6. November 2007 Ich, Christian Müller, Student der Informatik an der Leibniz Universität Hannover, versichere an Eides statt, dass ich die vorliegende Bachelorarbeit selbstständig verfasst und keine anderen als die angegebenen Hilfsmittel verwendet habe. Die Arbeit wurde in dieser oder ähnlicher Form noch keiner Prüfungskommission vorgelegt. Christian Müller Inhaltsverzeichnis 1 Einleitung 3 2 Grundlagen 6 3 Algorithmen 10 3.1 Allgemeiner Unifikationsalgorithmus . . . . . . . . . . . . . . . . . . . . . . 11 3.2 Robinson . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13 3.3 Ruzicka Privara . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15 3.4 Paterson Wegman . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21 3.5 Suciu . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28 4 Implementierung 33 4.1 Ausgaben der Algorithmen . . . . . . . . . . . . . . . . . . . . . . . . . . . 33 4.2 Testsystem . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35 4.3 Zeitmessung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35 4.4 Speicherverwaltung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35 4.5 Testablauf . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 36 5 Vergleich 5.1 38 Schlussbemerkung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 44 Literaturverzeichnis 45 Kapitel 1 Einleitung Motivation Um zu gewährleisten das die Datentypen in einem Programm nur gemäß ihrer Definition benutzt werden muss man in den meisten gebräuchlichen Programmiersprachen für Variablen und Funktionen die verwendeten Datentypen explizit im Quellcode festlegen. Unter anderem kann das dazu führen das dieselbe Funktion für mehrere Datentypen implementiert werden muss. Beispiel 1. Möchte man die Funktion max(a,b) die das Maximum von zwei Werten bestimmt mit Integer oder Real Werten verwenden, muss man sie für beide Datentypen implementieren, obwohl der Algorithmus if a>b return a else return b derselbe ist. Möchte man die Funktion außerdem mit gemischten Datentypen aufrufen können, muss man sie für alle möglichen Kombinationen der Datentypen definieren. Es wäre also wünschenswert auf die manuelle Typisierung zu verzichten. Soll trotzdem die Typsicherheit gewährleistet sein, muss der Compiler diese Aufgabe übernehmen. Das zugrundeliegende Problem bei dieser Typ-Inferenz ist die Unifikation. Hintergrund Vereinfacht ausgedrückt kann man mit der Unifikation feststellen ob sich zwei Ausdrücke in Präfixnotation, bestehend aus Variablen und Funktionen, vereinheitlichen“ (unifizie” ren) lassen. Das bedeutet das beide Terme nach der Anwendung einer bestimmten Variablenersetzung (Substitution) denselben Term liefern. Außerdem ermittelt die Unifikation 1 Einleitung 4 neben der bloßen Aussage ob zwei Terme unifizierbar sind auch die kleinste“ Substituti” on, den so genannten allgemeinsten Unifikator. Beispiel 2. Die beiden Terme t1 = f (X, g(Y )) und t2 = f (Y, Z) liefern nach Anwendung der Substitution σ = {Y → X, Z → g(X)} den Term f (X, g(X)). Ursprünglich entwickelt wurde die Unifikation von J. Robinson um sie bei der computergestützten Beweisführung einzusetzen. Seitdem spielt sie in allen Anwendungen in denen Vergleiche von Symbolen durchgeführt werden müssen eine wichtige Rolle, zum Beispiel bei der Logischen Programmierung, Term-Rewriting und Typ-Inferenz. In seiner Arbeit A Machine-Oriented Logic Based on the Resolution Principle“ von 1965 [15] stellte er ” dafür einen Algorithmus vor der sowohl einen exponentiellen Rechenzeit-, als auch Speicherplatzbedarf hatte. Weitere Arbeiten zur Unifikation die den Rechenzeitbedarf kontinuierlich verringert haben stammen unter anderem von G. Huet, der 1975 in [8] einen Algorithmus mit fast linearem Rechenzeitbedarf veröffentlichte, sowie M. Paterson und M. Wegman, die 1976 mit [12] den ersten Unifikationsalgorithmus mit linearem Rechenzeitbedarf vorstellten. Andere Arbeiten die sich mit der Unifikation befassen und unterschiedlich effiziente Algorithmen vorstellen sind zum Beispiel [4] [6] [16] und [17]. Aufgabenstellung Ziel dieser Arbeit ist es eine Auswahl der vorhandenen Unifikationsalgorithmen vorzustellen, zu implementieren und in Bezug auf den Rechenzeitbedarf zu vergleichen. Außerdem soll bei der Rechenzeitanalyse auf die Verwendung der Algorithmen für die Typ-InferenzPrüfung eines Compilers eingegangen werden. Gliederung Die Grundlagen die nötig sind um die Unifikation von Termen genauer zu definieren werden im folgenden Kapitel beschrieben. In Kapitel 3 werden die einzelnen Algorithmen, die im Umfang dieser Arbeit verglichen werden, vorgestellt. Als erstes betrachten wir den klassischen Unifikationsalgorithmus von Robinson. Außerdem die Algorithmen von Ruzicka und Privara, sowie Paterson und Wegman, die beide mit Hilfe von ungerichteten azyklischen Graphen arbeiten. Zuletzt betrachten wir den Algorithmus von Suciu, der lediglich eine Tabelle benutzt. 1 Einleitung 5 Kapitel 4 befasst sich mit der Implementierung der Algorithmen. Es wird auf das allgemeine Rahmenwerk der Implementierung eingegangen, sowie auf die Probleme die sich bei der Implementierung der einzelnen Algorithmen ergeben. Außerdem untersuchen wir in wie weit einzelne Algorithmen angepasst werden müssen um einen objektiven Vergleich zu erstellen. Im letzten Kapitel werden die Testergebnisse vorgestellt, sowie die daraus gezogenen Schlussfolgerungen. Kapitel 2 Grundlagen In diesem Kapitel werden wir die notwendigen Grundlagen einführen. Wir verwenden für die Unifikation Ausdrücke in Präfixnotation. Diese Ausdrücke (Terme) werden durch die Elemente eines Variablenalphabetes und eines Funktionsalphabetes gebildet. Definition 2.0.1 (Variable). Eine Variable wird durch einen groß geschriebenen Bezeichner repräsentiert, der indiziert sein kann. Beispiel 3. Wir verwenden in dieser Arbeit vor allem die Variablen X, Y , Z, X1 und Xn . Definition 2.0.2 (Funktion, Konstante). Eine Funktion wird durch einen klein geschriebenen Bezeichner repräsentiert, der indiziert sein kann. Sei f ein Funktionssymbol, dann ist ρ(f ) ≥ 0 die Parameteranzahl von f . Funktionen werden als f (t1 , .., tρ(f ) ) geschrieben. Funktionen ohne Parameter werden auch Konstanten genannt und k anstatt k() geschrieben. Beispiel 4. Wir verwenden in dieser Arbeit vor allem die Funktionen f , g, h, f1 und fn . Definition 2.0.3 (Term). Sei V ein Alphabet von Variablen und F ein Alphabet von Funktionen, dann gilt: • Jede Variable X ∈ V ist ein Term • Ist f ∈ F und sind t1 , .., tn Terme, so ist auch die Funktion f (t1 , .., tn ) ein Term. Beispiel 5. Sei V = {X, Y, Z} das Variablenalphabet und F = {f, g, h} mit ρ(f ) = 2, ρ(g) = 0 und ρ(h) = 1 das Funktionsalphabet, dann sind X, g, f (Y, Z) und f (f (g, X), h(g)) Beispiele für Terme über diesen Alphabeten. 2 Grundlagen 7 f f g h X g Bild 2.1: Der Term f (f (g, X), h(g)) aus Beispiel 5 als Baum betrachtet. Terme können auch als Bäume betrachtet werden, indem man die Knoten mit den entsprechenden Variablen und Funktionssymbolen benennt. Siehe dazu Bild 2.1. Damit haben wir definiert was für Ausdrücke wir unifizieren wollen. Jetzt müssen wir noch definieren wie man einen Term in einen anderen Term umwandeln kann. Definition 2.0.4 (Substitution). Sei V ein Alphabet von Variablen und F ein Alphabet von Funktionen, dann gilt: Eine (ungeordnete) Substitution ist die Abbildung aller Variablen aus V auf jeweils einen Term, gebildet aus V und F . Eine Substitution wird in der Form σ = {v1 → t1 , .., vn → tn } notiert. Dabei wird angegeben, welche Variable vi auf welchen Term ti abgebildet wird. Variablen die nicht in der angegebenen Liste auftreten werden durch die Substitution auf sich selbst abgebildet. Ist n = 1 nennen wir die Substitution auch Einzelsubstitution. Die Anwendung einer Substitution σ auf einen Term t wird tσ geschrieben und ist der Term der durch die gleichzeitige Ersetzung aller Variablen aus t, durch die von σ festgelegten Terme, entsteht. Das Ergebnis dieser Substitution ist eine Instanz von t. Die Identität σid = {} bildet erwartungsgemäß alle Variablen auf sich selbst ab. Beispiel 6. Sei t = f (X, h(Y ), Z) ein Term und σ = {U → V, X → f (U, V, Y ), Y → h(W )} eine Substitution über den sich daraus ergebenden Variablen und Funktionsalphabeten. Dann ist das Ergebnis der Substitution tσ = f (f (U, V, Y ), h(h(W )), Z). Definition 2.0.5 (Verkettung von Substitutionen). Die Verkettung von zwei Substitutionen σ1 = {v1 → t1 , .., vn → tn } und σ2 = {w1 → s1 , .., wk → sk } ist definiert als σ1 σ2 = {v1 → t1 σ2 , .., vn → tn σ2 , w1 → s1 , .., wk → sk }, wobei für die Variablen wi die in beiden Substitutionen vorkommen der Term wi → si ausgelassen wird. Die Verkettung von Substitutionen ist nicht kommutativ. Beispiel 7. Seien die Substitutionen σ1 = {X → g, Y → h} und σ2 = {X → h, Y → g, Z → f (X, Y )}, sowie der Term t = f (X, Y, Z) gegeben, dann ist tσ1 σ2 = f (g, h, f (X, Y, Z)) und tσ2 σ1 = f (h, g, f (h, g, h)). 2 Grundlagen 8 Definition 2.0.6 (geordnete Substitution). Eine geordnete Substitution η ist eine Folge von Einzelsubstitutionen mit η = {v1 → t1 }{v2 → t2 }...{vn → tn }. Mit Hilfe von Termen und Substitution können wir jetzt die Unifikation genauer definieren. Wir betrachten sie, in der gesamten Arbeit, der Einfachheit halber nur zwischen zwei Termen. Definition 2.0.7 (Unifikator). Eine Substitution σ ist ein Unifikator von den zwei Termen t1 und t2 , wenn t1 σ = t2 σ ist. Die Terme sind in diesem Fall unifizierbar. Existiert keine Substitution mit dieser Eigenschaft sind diese Terme nicht unifizierbar. Die Problemstellung, ob die beiden Terme t1 und t2 unifizierbar sind, notieren wir als t1 =? t2 . Beispiel 8. Sei t1 = f (Z, g(Y )) und t2 = f (g(X), Z). Dann ist die Substitution σ = {Z → g(Y ), X → Y } ein Unifikator von t1 und t2 , da t1 σ = f (g(Y ), g(Y )) = t2 σ ist. Die beiden Terme sind damit unifizierbar. Beispiel 9. Die beiden Terme t1 = f (Z, h(Y )) und t2 = f (g(X), Z) sind nicht unifizierbar, da es bei der Substitution der Variable Z an der einen Stelle zu einer Ungleichheit in den Funktionssymbolen an der anderen Stelle kommt und Funktionssymbole nicht substituiert werden können. Definition 2.0.8 (allgemeinster Unifikator). Ein Unifikator σmgu zweier Terme t1 und t2 heißt allgemeinster Unifikator von t1 und t2 , wenn zu jedem Unifikator σ von t1 und t2 eine Substitution θ existiert, so das σ = σmgu θ gilt, also t1 σ eine Instanz von t1 σmgu ist. Beispiel 10. Seien t1 = f (X, Y ) und t2 = f (Y, Z) gegeben, dann sind σ1 = {X → a, Z → a, Y → a} und σ2 = {X → Y, Z → Y } Unifikatoren von t1 und t2 . σ1 kann offensichtlich nicht der allgemeinste Unifikator sein, da σ1 = σ2 {Y → a} ist. σ2 ist der allgemeinste Unifikator von t1 und t2 . Definition 2.0.9 (Variablenumbenennung). Die Substitution σ = {v1 → t1 , .., vn → tn } ist eine Variablenumbenennung, wenn die Menge {v1 , .., vn } identisch mit der Menge {t1 , .., tn } ist. Beispiel 11. Die Substitution σ = {X → Y, Y → Z, Z → X} ist eine Variablenumbenennung. Die Substitutionen θ = {X → Y, Y → Z} und η = {X → Y, Y → Z, Z → Y } sind hingegen keine Variablenumbenennungen. Wenn die beiden Terme t1 und t2 unifizierbar sind kann es mehrere allgemeinste Unifikatoren geben. Sie unterscheiden sich allerdings nur durch eine Variablenumbenennung. 2 Grundlagen 9 Beispiel 12. Betrachtet man die Terme aus Beispiel 10, so ist die Substitution σ3 = {Y → X, Z → X} ebenfalls ein allgemeinster Unifikator. Kapitel 3 Algorithmen In diesem Kapitel werden wir die einzelnen Algorithmen betrachten die im Umfang dieser Arbeit implementiert werden. Außerdem werden wir zuerst einen sehr allgemeinen Unifikationsalgorithmus vorstellen der die Grundlegenden Schritte der Unifikation verdeutlichen soll. Die meisten anderen Unifikationsalgorithmen lassen sich aus diesem Algorithmus ableiten indem man eine bestimmte Reihenfolge festlegt in der die Gleichungen ausgewählt werden und konkrete Datenstrukturen betrachtet. Realisiert man die Menge der Gleichungen in Algorithmus 1 beispielsweise mit einem Stack, so erhält man Robinsons Algorithmus [15] mit exponentieller Laufzeit. Überführt man die Terme in einen gerichteten, azyklischen Graphen in dem jede Variable nur einmal vorkommt erhält man den Algorithmus von Corbin und Bidoit [6] mit der quadratischen Laufzeit O(n2 ), wobei n die Anzahl aller Symbole aus beiden Termen ist. Führt man dabei den Occurcheck nicht mit jeder Variableneliminierung durch, sondern erst nach der eigentlichen Unifikation als Zyklensuche in dem entstandenem Graphen, so erhält man den Algorithmus von Ruzicka und Privara [16] mit der Laufzeit O(n · A−1(n)), wobei A−1 (n) die Umkehrfunktion der Ackermann-Funktion ist. Da diese Funktion sehr langsam wächst kann man den Algorithmus als fast linear bezeichnen. Durchläuft man den Graphen in einer bestimmten Weise erhält man den Algorithmus von Paterson und Wegman [12] mit linearer Laufzeit. 3 Algorithmen 3.1 11 Allgemeiner Unifikationsalgorithmus Die Grundlegenden Schritte die bei der Unifikation ausgeführt werden müssen sind die Termreduktion, die Variableneliminierung und der Occurcheck. Zur Termreduktion kommt es wenn zwei Funktionen auf Unifizierbarkeit überprüft werden sollen. Sind deren Funktionssymbole oder Parameteranzahl unterschiedlich kann offensichtlich sofort abgebrochen werden. Sind sie gleich müssen die Parameter der Funktionen in den entsprechenden Paaren auf Unifizierbarkeit getestet werden, denn nur wenn alle diese Paare mit der selben Substitution unifizierbar sind, sind auch die Funktionen unifizierbar. Zur Variableneliminierung kommt es wenn mindestens einer der beiden Terme eine Variable ist. Kommt diese Variable nicht im zweiten Term vor sind sie unifizierbar durch die Substitution in der die Variable durch den anderen Term ersetzt wird. Der Test in der Variableneliminierung ob die Variable in dem anderen Term vorkommt wird Occurcheck genannt. In vielen Algorithmen ist er mitverantwortlich für eine nichtlineare Laufzeit. In einigen Implementierungen wird er deswegen sogar ganz weggelassen, aber ob das sinnvoll ist oder nicht hängt stark vom Anwendungsgebiet ab. Beispiel 13. Ohne Occurcheck führt die Unifikation X =? f (X) zu dem allgemeinstem Unifikator σ = {X → f (X)}. Erstellt man nun die Instanz Xσ erhält man den unendlich großen Term f (f (f (..))). Im Grunde erfüllt das unsere Definitionen, da Xσ = f (f (f (..))) = f (X)σ ist, aber ob das das gewünschte Ergebnis ist, oder ob damit weiter gearbeitet werden kann ist fraglich. Mit Algorithmus 1 kann man durch anwenden dieser Schritte die Unifikation durchführen. 3 Algorithmen 12 Algorithmus 1 : Unify Eingabe : Die zu unifizierenden Terme t1 und t2 . Ausgabe : Der allgemeinste Unifikator von t1 und t2 , oder die Mitteilung das kein Unifikator existiert. Erstelle die Substitution σ := σid . Erstelle eine leere Menge von Gleichungen und füge t1 =? t2 hinzu. Führe über dieser Menge folgende Transformationen so lange durch bis keine mehr angewandt werden kann: (A) Nimm eine Gleichung der Form t =? x, wobei x eine Variable und t eine Funktion ist und schreibe sie zu x =? t um. (B) Nimm eine Gleichung der Form x =? x, wobei x eine Variable ist und lösche sie. (C) Nimm eine Gleichung der Form t =? t , wobei t und t Funktionen sind. Unterscheiden sich die Funktionssymbole oder die Parameteranzahl, breche mit der Mitteilung das kein Unifikator existiert ab. Andernfalls seien die Parameter von t = f (t 1 , .., t ρ(t ) ) und t = f (t 1 , .., t ρ(t ) ). Ersetze die Gleichung t =? t durch die Gleichungen t 1 =? t 1 bis t ρ(t ) =? t ρ(t ) . (D) Nimm eine Gleichung der Form x =? t, wobei x eine Variable ist die in der Menge der Gleichungen noch einmal auftritt und t ein Term der ungleich x ist. Wenn x in t auftritt breche mit der Mitteilung das kein Unifikator existiert ab. Andernfalls wende die Substitution θ = {x → t} auf beide Terme aller anderen Gleichungen an und ermittle σ := θσ. Gib die Substitution σ aus. 3 Algorithmen 3.2 13 Robinson Die zu unifizierenden Terme werden vom Algorithmus durch rekursive Aufrufe bis auf triviale Fälle unterteilt. Die Substitutionen die sich aus diesen Teilaufgaben ergeben werden zum allgemeinsten Unifikator zusammengefügt. Sollen in einer Teilaufgabe zwei Funktionen mit unterschiedlichem Funktionssymbol oder unterschiedlicher Parameteranzahl unifiziert werden, oder eine Variable mit einem Term, der diese enthält, so bricht der Algorithmus mit der Meldung das kein Unifikator existiert ab. Da der Algorithmus den Robinson in seiner Arbeit [15] vorgestellt hat sich nicht direkt implementieren lässt, beschreibe ich hier eine rekursive, leicht zu implementierende Variante des Algorithmus angelehnt an [11]. Algorithmus 2 : Robinsons Algorithmus Eingabe : Die beiden zu unifizierenden Terme t1 und t2 . Ausgabe : Der allgemeinste Unifikator von t1 und t2 oder die Meldung das kein Unifikator existiert. 1 2 begin unify(t1 ,t2 ) if isVariable(t1 ) then if t1 == t2 then return σid else if t2 contains t1 then stop No Unificator“ ” else return σ := {t1 → t2 } 3 4 5 6 else if isVariable(t2 ) then if t1 == t2 then return σid else if t1 contains t2 then stop No Unificator“ ” else return σ := {t2 → t1 } 7 8 9 10 else 11 if t1 .name != t2 .name or t1 .arity != t2 .arity then stop No Unificator” 12 σ := σid foreach p1 in t1 .parameter and p2 in t2 .parameter do 13 σnew := unify(p1 σ,p2 σ) σ := σnew σ 14 15 16 endfch 17 return σ 18 end Beispiel 14. Seien t1 = f (X, Y ) und t2 = f (g(Z), X) die zu unifizierenden Terme. 3 Algorithmen 14 Der Algorithmus geht im ersten Schritt zu Fall 3 über, da beide Terme Funktionen sind. Es wird die Substitution σ als Identität initialisiert (Zeile 12), welche letztendlich an die aufrufende Funktion zurückgegeben wird (17) und dann werden schrittweise die Parameter der Funktionen unifiziert (14-15). Zunächst sind das die beiden Terme Xσ = X und (g(Z))σ = g(Z), welche in Fall 1 ausgewertet werden. Da X = g(Z) und da X nicht in den Parametern von g(Z) enthalten ist wird die Substitution {X → g(Z)} zurückgegeben (5). Damit ergibt sich ein neues σ aus σnew σ = {X → g(Z)}{} = {X → g(Z)} (15). Im nächsten Schritt der for-Schleife werden die beiden Terme Y σ = Y und Xσ = g(Z) unifiziert. Dies geschieht wieder in Fall 1 und es wird die Substitution {Y → g(Z)} zurückgegeben. Zuletzt wird der Substitution σ der Wert von σnew σ = {Y → g(Z)}{X → g(Z)} = {X → g(Z), Y → g(Z)} zugewiesen (15). Da der Algorithmus nicht vorzeitig abgebrochen hat (4,8,11) wird σ als allgemeinster Unifikator zurückgegeben. Laufzeit Im schlechtesten Fall hat der Algorithmus eine exponentielle Laufzeit. Das hat die folgende Gründe: • Dadurch das die gefundene Substitution fortlaufend angewendet wird können sich die Terme exponentiell aufblähen. Das Durchlaufen dieser Terme und der Occurcheck führen dann zu exponentieller Laufzeit. • Die eventuell vorhandene Struktur der Terme wird nicht ausgenutzt. Beispiel 15. Ein Beispiel für den ersten Punkt ist die Unifikation von f (X1 , .., Xn ) =? f (g(X0, X0 ), .., g(Xn−1, Xn−1 )). Deutlich wird das in Tabelle 3.1. Es sind die Terme angegeben mit denen sich der Algorithmus in Zeile 14 rekursiv aufruft. Wenn er bei Xn σ =? (g(Xn−1 , Xn−1))σ angekommen ist enthält der zweite Term nach der Substitution 2n mal die Variable Xn . X1 σ = X 1 (g(X0 , X0 ))σ = g(X0 , X0 ) X2 σ = X 2 (g(X1 , X1 ))σ = g(g(X0, X0 ), g(X0, X0 )) X3 σ = X 3 (g(X2 , X2 ))σ = g(g(g(X0, X0 ), g(X0, X0 )), g(g(X0, X0 ), g(X0, X0 ))) Tabelle 3.1: Beispiel für die exponentielle Laufzeit des Robinson Algorithmus 3 Algorithmen 3.3 15 Ruzicka Privara Der Algorithmus von Ruzicka und Privara ist eine Optimierung des Algorithmus von Corbin und Bidoit [6], welcher wiederum auf Robinsons Algorithmus beruht. Er wurde 1989 in ihrer Arbeit [16] vorgestellt. Bei großen Termen kann es vorkommen das derselbe Teilterm an mehreren Stellen auftritt. Da Robinsons Algorithmus in keiner Weise die Struktur der zu unifizierenden Terme untersucht macht er an diesen Stellen unnötige Berechnungen die er an einer anderen Stelle schon erledigt hat. Um das zu vermeiden arbeitet der Algorithmus von Ruzicka und Privara nicht mit Bäumen sondern mit daraus hergeleiteten gerichteten, azyklischen Graphen (kurz DAG). Im Idealfall sind in diesem reduzierten DAG alle identische Teilterme durch jeweils nur einen einzigen Teilgraphen dargestellt. Da das Durchsuchen des Baumes nach allen identischen Teilbäumen allerdings sehr viel Zeit in Anspruch nehmen kann arbeitet man in der Praxis mit einem DAG in dem lediglich die Variablen auf diese Art reduziert sind. Grundsätzlich arbeiten die Algorithmen die auf einem DAG beruhen aber umso schneller, desto mehr Teilterme reduziert sind. Außerdem wird der Occurcheck nicht bei jeder Variableneliminierung ausgeführt, sondern erst nach der eigentlichen Unifikation, als Zyklensuche in dem entstandenem DAG. DAG Das man Terme auch als Bäume betrachten kann haben wir in Bild 2.1 gezeigt. Diese Bäume kann man wiederum als gerichtete azyklische Graphen interpretieren. Damit ist es möglich die eventuell vorhandene Struktur der Terme sehr einfach abzubilden, indem man identische Teilterme durch den selben Teilgraphen darstellt. g f x g f y y f x y f x x f y g f f y f x Bild 3.1: Der Term g(f (x, y), f (y, x), f (y, x)) als DAG, komplett reduzierter DAG und als DAG in dem nur die Variablen reduziert sind. Wichtig bei der Verwendung von einem DAG ist das die Söhne eines Knoten in einer festgelegten Reihenfolge vorliegen, um zum Beispiel zwischen den Funktionen f (X, Y ) 3 Algorithmen 16 und f (Y, X) unterscheiden zu können. Algorithmus Die Termreduktion wird wie in Robinsons Algorithmus durch rekursive Aufrufe der unify Funktion realisiert. Die Variableneliminierung wird durch die Funktion replace(u,v) realisiert, wobei u und v unterschiedliche Knoten des DAG sind. Sie ersetzt alle Kanten des DAG die auf den Knoten u zeigen, durch Kanten die auf den Knoten v zeigen. Nach der Anwendung von replace(u,v) ist der Knoten u isoliert, was der Tatsache entspricht das die Variable in keinem Term mehr vorkommt. Die Funktion replace(u,v) kann offensichtlich Zyklen in dem Graphen erzeugen. Im einfachsten Fall passiert das wenn eine Kante von v auf u existiert. Um zu verhindern das der Algorithmus dabei in eine Endlosschleife gerät müssen die Knoten die gerade bearbeitet werden entsprechend markiert werden, da dadurch aber nicht alle Zyklen entdeckt werden können wird nach der eigentlichen Unifikation noch eine Zyklensuche im Graphen durchgeführt. Diese Zyklensuche ist der postOccurCheck und kann durch eine einfache Tiefensuche in linearer Zeit durchgeführt werden. Laufzeit Behauptung. Sei n die Anzahl an unterschiedlichen Variablen, p die gesamte Anzahl an Symbolen, q die gesamte Größe der Terme und A−1 die Umkehrfunktion der AckermanFunktion, dann ist die Laufzeit für den schlechtesten Fall der unify Funktion O(p·A−1 (p)). Beweis. Während der Unifikation werden keine neuen Knoten oder Kanten in dem Graphen erzeugt. Ein Knoten kann maximal p − 2 Söhne besitzen. Da successor in unify für alle Söhne beider Terme aufgerufen wird ist die Anzahl an successor Funktionsaufrufen bei jedem unify Aufruf durch 2p nach oben beschränkt. Jeder Aufruf der replace Funktion isoliert einen Knoten des Graphen, das heißt das sie höchstens p mal aufgerufen werden kann. Da die unify Funktion immer durch einen Aufruf von replace beendet wird, ist auch die Anzahl an unify Aufrufen durch p nach oben beschränkt. 3 Algorithmen 17 Realisiert man den Graphen als Union-Find-Datenstruktur kann man die Funktion successor durch die Funktion find und die Funktion replace durch die Funktion union abbilden. Eine genaue Beschreibung der nötigen Union-Find-Datenstruktur befindet sich in [16]. Da eine Folge von O(p) union und find Operationen mit der Zeitkomplexität O(p · A−1 (p)) realisiert werden kann, hat auch die unify Funktion im schlechtesten Fall die Zeitkomplexität O(p · A−1 (p)). Algorithmus 3 : Algorithmus von Ruzicka und Privara Eingabe : Die beiden Knoten u1 und u2 eines reduzierten DAG die unifiziert werden sollen. Ausgabe : true falls u1 und u2 unifizierbar sind, andernfalls false. 1 begin 2 bool := unify(u1 ,u2 ) 3 if bool then bool := postOccurCheck(u1 ,u2 ) 4 5 6 return bool end 3 Algorithmen 18 Algorithmus 4 : unify Eingabe : Die beiden Knoten u1 und u2 eines reduzierten DAG die unifiziert werden sollen. Ausgabe : true falls u1 und u2 unifizierbar sind, andernfalls false. 1 2 begin unify(v1 ,v2 ) if isVariable(v1 ) then 3 replace(v1 ,v2 ) 4 return true 5 else if isVariable(v2 ) then 6 replace(v2 ,v1 ) 7 return true 8 else if v1 .name != v2 .name then return false 9 else 10 11 k := 0 12 bool := true 13 while k < arity(v1 ) and bool do 14 k := k+1 15 w1 := successor(v1 ,k) 16 w2 := successor(v2 ,k) 17 if w1 .visited or w2 .visited then bool := false 18 else if w1 !=w2 then 19 w1 .visited := true 20 w2 .visited := true 21 bool := unify(w1 ,w2 ) 22 w1 .visited := false 23 w2 .visited := false 24 endw 25 if bool then replace(v1 ,v2 ) 26 end 3 Algorithmen 19 Beispiel 16. Seien t1 = f (X, Z) und t2 = f (g(Z), g(Z)) die zu unifizierenden Terme. Zuerst wird der DAG erstellt und die Funktion unify mit den beiden Wurzeln aufgerufen: unify(1,2 ) f1 f2 g3 X5 g4 Z6 Dann wird unify mit den ersten Söhnen der beiden Knoten aufgerufen (Zeile 21), nachdem sie als besucht markiert wurden (19-20): unify(5,3 ) f1 f2 g3 X5 g4 Z6 Da jetzt eine Variable mit einer Funktion unifiziert werden soll wird replace(5,3 ) aufgerufen (3). Danach sind die beiden ersten Söhne der Wurzeln unifiziert (4) und die Markierungen werden wieder gelöscht (22-23). Es entsteht folgender Graph: f1 f2 g3 X5 g4 Z6 Wir befinden uns wieder in der ursprünglichen Unifikationsaufgabe. Jetzt müssen die beiden zweiten Söhne markiert (19-20) und unifiziert (21) werden: unify(6,4 ) 3 Algorithmen 20 f1 f2 g3 X5 g4 Z6 Da der erste Knoten wieder eine Variable ist, wird replace(6,4 ) aufgerufen (3). In Robinsons Algorithmus würde man vorher noch den Occurcheck durchführen, der hier offensichtlich positiv wäre. Darauf verzichten wir, und es entsteht folgender Graph: f1 f2 g3 X5 g4 Z6 Da alle Söhne bearbeitet wurden ist die while-Schleife beendet (24) und es wird replace(1,2 ) ausgeführt (25), was jedoch den Graphen nicht verändert, da keine Kanten in den beiden Knoten enden. Der entstandene Zyklus wird im postOccurCheck entdeckt und führt damit zum korrekten Ergebnis. Die beiden Terme sind nicht unifizierbar. 3 Algorithmen 3.4 21 Paterson Wegman Der Algorithmus wurde von Paterson und Wegman 1976 in ihrer Arbeit [12] vorgestellt. Er arbeitet ebenfalls mit einem reduziertem DAG, durchläuft diesen aber nicht als Tiefensuche wie Robinsons oder daraus abgeleitete Algorithmen. Es war der erste Unifikationsalgorithmus mit linearer Laufzeit. Allerdings ist er wesentlich schwieriger zu implementieren. Das liegt zum einen daran das in der viel zitierten ursprünglichen Arbeit [12] weder darauf eingegangen wird wie der DAG erstellt wird, noch wie die ermittelte geordnete Substitution in eine normale Substitution umgewandelt werden kann. Des weiteren existiert neben dieser Arbeit auch noch eine stark überarbeitete zweite Version mit dem selben Titel [13]. In dieser Arbeit ist allerdings der angegebene Algorithmus fehlerhaft. Äquivalenzbeziehung Definition 3.4.1 (Äquivalenzklasse). Eine Äquivalenzklasse ist eine Menge von Knoten die untereinander alle eine gültige Äquivalenzbeziehung besitzen. Definition 3.4.2 (gültige Äquivalenzbeziehung). Eine Äquivalenzbeziehung zwischen Knoten eines DAG ist gültig wenn folgende Eigenschaften erfüllt sind: • Wenn zwei Funktionsknoten äquivalent sind, dann sind ihre Söhne ebenfalls paarweise äquivalent. • Jede Äquivalenzklasse ist homogen, das heißt sie enthält keine zwei Funktionsknoten mit unterschiedlichen Funktionssymbolen. • Die Äquivalenzklassen besitzt eine partielle Ordnung, übernommen von der partiellen Ordnung des DAG. Satz 1. Wenn zwischen den zwei Knoten u und v eine gültige Äquivalenzbeziehung u ≡ v besteht, dann sind sie unifizierbar. In diesem Fall existiert eine einzigartige minimale gültige Äquivalenzbeziehung. Grundidee Die Terme die von den Knoten einer Äquivalenzklasse repräsentiert werden sind demnach alle miteinander unifizierbar. Um also zwei Terme zu unifizieren überführt man sie in 3 Algorithmen 22 Algorithmus 5 : Eingabe : Ein reduzierter DAG mit den beiden Wurzeln u und v. Ausgabe : Die Meldung ob die beiden Wurzeln unifizierbar sind oder nicht. 1 Setze u ≡ v. 2 So lange noch ein Paar Funktionsknoten r und s existiert für die r ≡ s gesetzt ist, aber ein Paar r und s ihrer Söhne nicht, setze r ≡ s . 3 Teste die Beziehung u ≡ v auf die Punkte zwei und drei für gültige Äquivalenzbeziehungen. 4 Wenn die Beziehung u ≡ v gültig ist gib aus das die beiden Knoten unifizierbar sind, ansonsten gib aus das die beiden Knoten nicht unifizierbar sind. einen DAG und erstellt alle notwendigen Äquivalenzklassen um herauszufinden ob die beiden Wurzeln des DAG sich in der selben Klasse befinden. Algorithmus 5 leistet das. Da dem Aussuchen der Funktionsknoten in Zeile 2 und damit auch dem Setzen der Äquivalenzbeziehungen in Zeile 3 keine sinnvolle Reihenfolge zugrunde liegt kann es passieren das einer bereits vollständig abgearbeiteten Äquivalenzklasse, durch das Setzen einer weiteren Äquivalenzbeziehung, noch ein weiteres Element hinzugefügt wird. In welchem Fall sich die Arbeit wiederholt und zu nicht linearer Laufzeit führt. Daraus ergibt sich die Grundidee von Paterson und Wegmans Algorithmus. Die Äquivalenzklassen die nicht mehr erweitert werden können müssen zuerst bearbeitet werden. Definition 3.4.3 (Wurzelklasse). Eine Äquivalenzklasse die nur Wurzeln des DAG enthält nennen wir eine Wurzelklasse. Da eine Wurzel keine Väter besitzt kann sie in Zeile 3 nicht Teil einer weiteren Äquivalenzbeziehungen werden, und demnach kann eine Wurzelklasse nicht mehr erweitert werden. Datenstrukturen Der reduzierte DAG besteht aus Variablen- und Funktionsknoten. Jedem Knoten ist das entsprechende Variablen- beziehungsweise Funktionssymbol zugeordnet. Variablenknoten haben keine Söhne, die Söhne der Funktionsknoten entsprechen ihren Parametern. Des weiteren sind folgende Eigenschaften gefordert: • Jeder Knoten kann als finished“ markiert werden. Damit erspart man sich das ” löschen der abgearbeiteten Knoten aus dem Graph. 3 Algorithmen 23 • Zu jedem Knoten kann die Äquivalenzklasse gespeichert werden der er angehört. Wir benutzen als Bezeichnungen der Äquivalenzklassen einen Pointer auf den ersten Knoten der dieser Klasse angehörte. • Zu jedem Knoten muss eine Liste seiner Väter gespeichert werden. • Die Söhne von Funktionsknoten müssen in einer definierten Reihenfolge vorliegen. Außerdem wird ein gewöhnlicher Stack benötigt in dem die Elemente der zu bearbeitenden Äquivalenzklasse gespeichert werden können. Beispiel Die Äquivalenzbeziehungen werden durch ungerichtete Kanten dargestellt und die Knoten id . des DAG wie folgt: SymbolAquivalenzklasse Die Unifikationsaufgabe lautet f (g(V ), h(U, V )) =? f (g(W ), h(W, i(X, Y ))). Nachdem der reduzierte DAG erstellt und eine Äquivalenzbeziehung zwischen den beiden Wurzeln hinzugefügt wurde, ergibt sich folgender Graph: f1 f2 g3 h4 g5 h6 W9 i10 U7 V8 X 11 Y 12 Der erste Funktionsknoten mit dem finish aufgerufen wird ist nicht näher bestimmt. Angenommen wir stoßen zuerst auf Knoten 10. Der Knoten ist noch in keiner Äquivalenzklasse, also erstellen wir eine neue Klasse und fügen ihn hinzu (Zeile 6). Da der Knoten keine Wurzel ist pausieren wir die Bearbeitung und rufen finish(6 ) auf (14). Bei Knoten 6 passiert dasselbe und es wird finish(2 ) aufgerufen. Knoten 2 wird ebenfalls einer Äquivalenzklasse zugeordnet. Da zwischen Knoten 1 und 2 eine Äquivalenzbeziehung besteht und Knoten 1 noch in keiner Klasse ist (19), fügen wir ihn derselben Klasse hinzu (20). Im nächsten Durchlauf der while-Schleife betrachten wir Knoten 1 genauer, weil überprüft werden muss ob die Äquivalenzklasse nach der Erweiterung noch homogen ist (11). 3 Algorithmen 24 Da Knoten 1 keine Väter besitzt (13) und es keine weiteren Elemente in dieser Klasse gibt (16) werden nur noch die Äquivalenzbeziehungen der Söhne in den entsprechenden Paaren erstellt (31), bevor beide Knoten als finished gekennzeichnet werden (32 bzw. 33). f21 f22 g3 h4 g5 h66 W9 i10 10 U7 V8 X 11 Y 12 Da wir mit dem Aufruf finish(2 ) fertig sind kehren wir zurück zu Knoten 6. Da der Knoten jetzt keine Väter mehr hat können wir mit der Untersuchung der Äquivalenzklasse fortfahren und untersuchen die eben erstellte Äquivalenzbeziehung zu Knoten 4. Es entsteht folgender Graph: f21 f22 h46 g3 g5 h66 W9 i10 10 U7 V8 X 11 Y 12 Damit sind wir wieder in Knoten 10 und dem ursprünglichem Aufruf von finish, aber da Knoten 8 noch einen Vater hat können wir die Betrachtung dieser Äquivalenzklasse immer noch nicht abschließen. Wir würden dann auch übersehen das die Knoten 7 und 9 ebenfalls zu dieser Klasse gehören. 3 Algorithmen 25 f21 f22 g33 h46 g35 h66 W9 i10 10 U7 V108 X 11 Y 12 Nachdem wir jetzt auch die Betrachtung der Äquivalenzklasse von Knoten 10 abschließen können kehren wir zurück in die Hauptfunktion und rufen finish mit den übrigen Variablen auf. Am Ende ergibt sich folgendes Bild: f21 f22 g33 h46 g35 h66 9 W10 i10 10 7 U10 V108 11 X11 Y1212 3 Algorithmen 26 Algorithmus 6 : Algorithmus von Paterson und Wegman Eingabe : Ein reduzierter DAG mit zwei Wurzeln. Ausgabe : Der allgemeinste Unifikator der Terme die durch die beiden Wurzeln repräsentiert werden -als geordnete Substitution- oder die Meldung das kein Unifikator existiert. 1 begin 2 Create link between the two roots 3 σ := σid foreach f in Function Nodes do 4 5 finish(f ) 6 endfch 7 foreach v in Variable Nodes do 8 9 10 11 finish(v) endfch return σ end 3 Algorithmen 27 Algorithmus 7 : finish Eingabe : Der Knoten r eines reduzierten DAG. 1 2 begin finish(r ) if r.finished == false then 3 if r.Pointer != null then 4 stop No Unificator“ ” else 5 6 r.Pointer := r 7 initialize Stack S 8 push(S,r ) 9 while notEmpty(S ) do 10 s := pop(S ) 11 if isFunction(s) and isFunction(r ) and r.Symbol != s.Symbol then stop No Unificator“ ” foreach t in s.Father do 12 13 14 finish(t) 15 endfch 16 foreach t in s.Link do if t.finished or t == r then 17 skip 18 else if t.Pointer == null then 19 20 t.Pointer := r 21 push(S,t) 22 else if t.Pointer != r then 23 stop No Unificator“ ” else 24 skip 25 26 endfch 27 if s != r then if isVariable(s) then 28 σ := σ{s → r} 29 else 30 create links between corresponding sons in s and r 31 s.finished := true 32 r.isFinished := true 33 endw 34 35 end 3 Algorithmen 3.5 28 Suciu Der Algorithmus von Suciu wurde 2006 in der Arbeit [17] vorgestellt. Er besteht aus zwei Hauptschritten. Im ersten Schritt wird aus den zu unifizierenden Termen t1 und t2 der so genannte Unification Table (UT) aufgebaut. In diese Tabelle werden alle Teilterme von t1 und t2 geschrieben. Tritt ein Term mehrmals auf, kann er auch mehrere identische Einträge im UT haben. Variablen hingegen dürfen nur genau einen Eintrag haben. Im zweiten Schritt wird dann die unify Funktion, mit den Indizes die t1 und t2 im UT haben, aufgerufen. Die Ausgabe von unify ist dann entweder true, falls die Terme unifizierbar sind, oder false. Ein Occurcheck wird allerdings nicht durchgeführt, das bedeutet das Terme wie zum Beispiel f (X) =? X als unifizierbar betrachtet werden. Datenstrukturen Die Einträge im Unification Table sind entweder Variablen oder Funktionen. Konstanten werden als Funktionen ohne Parameter dargestellt. Der Unification Table besteht aus den fünf Spalten: • Index: der Index beginnt bei Null und identifiziert jeden Eintrag eindeutig • Symbol: der Variablenname, bzw. das Funktionssymbol • Typ: für Variablen VAR; für Funktionen STR • Parameteranzahl: für Variablen Null; für Funktionen die Anzahl der Parameter • Parameterliste: für Variablen die leere Liste; für Funktionen eine Liste mit den Parametern, dargestellt durch deren Indizes im UT; die Reihenfolge der Parameter muss bei allen Funktionen einheitlich sein Neben dem Unification Table werden zwei gewöhnliche Stacks benötigt, auf denen die zu vergleichenden Teilterme gespeichert werden. Außerdem wird noch eine Datenstruktur benötigt in der gespeichert werden kann welche Variable an welchen Term gebunden ist. Algorithmus Auf den Stacks Sx und Sy liegen die Indizes aller Terme die noch erfolgreich unifiziert werden müssen. Zu Beginn des Algorithmus sind das dementsprechend die Indizes von t1 3 Algorithmen 29 Algorithmus 8 : Algorithmus von Suciu Eingabe : Die Indizes der zu unifizierenden Terme im UT. Ausgabe : true falls die Terme (ohne einen Occurcheck) unifizierbar sind, andernfalls false. 1 begin unify(ix,iy) 2 initialize Stack Sx, initialize Stack Sy 3 push(Sx,ix ), push(Sy,iy) 4 while notEmpty(Sx ) and notEmpty(Sy) do 5 i := pop(Sx ), j := pop(Sy) 6 // case 1: i is bound to a term and j is bound to a term 7 if type(i )==STR and type(j )==STR then if main functors of i and j match (both name and arity) then 8 if arity > 0 then 9 10 push components of i on Sx in sequence 11 push components of j on Sy in sequence else return false 12 13 // case 2: i is bound to a term and j is bound to a variable 14 if type(i )==STR and type(j )==VAR then 15 if j is a free variable then bind j to i and set mgu[j] = 1 16 else 17 j := dereference(j ) 18 if j is bound to a STR then 19 push(Sx,i ), push(Sy,j ) else bind j to i 20 21 // case 3: i is bound to a variable and j is bound to a term 22 if type(i )==VAR and type(j )==STR then // perfectly symmetric to case 2 23 24 // case 4: i is bound to a variable and j is bound to a variable 25 if type(i )==VAR and type(j )==VAR then 26 if (i is free and j is free) then bind i to j and set mgu[i] = 1 27 else if i is free and j is bound then bind i to j and set mgu[i] = 1 28 else if i is bound and j is free then bind j to i and set mgu[j] = 1 29 else if i is bound and j is bound then 30 push the index of the term to which i is bound on Sx 31 push the index of the term to which j is bound on Sy 32 endw 33 return true 34 end 3 Algorithmen 30 und t2 (Zeile 2-3). Innerhalb der while-Schleife werden bei jedem Durchlauf die oberen Terme der Stacks entfernt (5) und wenn möglich unifiziert. Sind die Terme Funktionen (7) wird das Fuktionssymbol und die Parameteranzahl verglichen (8-9). Stimmen sie nicht überein ist die Unifikation fehlgeschlagen und es wird abgebrochen (12). Stimmen beide überein sind die Funktionen möglicherweise unifizierbar. Um das zu überprüfen werden die einzelnen Parameter, in einheitlicher Reihenfolge, auf den entsprechenden Stacks gespeichert (10-11). Ist einer der Terme eine Funktion und der andere eine Variable (14 bzw. 22) muss unterschieden werden ob die Variable noch frei ist, dann wird sie einfach an die Funktion gebunden (15), oder ob sie gebunden ist. Wenn sie schon an eine andere Funktion gebunden ist muss diese Funktion mit der aktuellen Funktion unifiziert werden. Dazu werden beide Funktionen auf die entsprechenden Stacks geschrieben (19). Wenn sie an eine freie Variable gebunden ist wird sie ebenfalls an die aktuelle Funktion gebunden (20). Wenn die Terme Variablen sind (25) werden die freien Variablen entsprechend gebunden (26-28). Sind beide Variablen schon an Terme gebunden müssen diese Terme unifiziert werden. Dazu werden wieder beide Terme auf die entsprechenden Stacks geschrieben (3031). Sind am Ende beide Stacks leer und ist kein Fehler aufgetreten sind t1 und t2 unifizierbar (33). Beispiel Es sollen die beiden Terme t1 = p(Z, h(Z, W ), f (W )) und t2 = p(f (X), h(Y, f (a)), Y ) unifiziert werden. Im ersten Schritt wird der in Tabelle 3.2 dargestellte UT erstellt. Im zweiten Schritt wird dann unify(6,11) aufgerufen und die while-Schleife des Algorithmus mehrmals durchlaufen. Die Belegungen der Stacks Sx und Sy in den einzelnen Durchläufen kann Tabelle 3.3 entnommen werden. Da am Ende beide Stacks leer sind und der Algorithmus nicht mit einer Fehlermeldung abgebrochen hat sind die beiden Terme unifizierbar. Der allgemeinste Unifikator ist σmgu = {W → f (a), X → f (a), Y → f (f (a)), Z → f (f (a))}. Laufzeit Auf die Laufzeit des Algorithmus geht Suciu in seiner Arbeit [17] nicht ein, es wird lediglich die Behauptung aufgestellt das er effizient sei. Das nachzuvollziehen fällt schwer, da bei der Unifikation von zwei bereits gebundenen Variablen in Zeile 29-31 nicht überprüft wird 3 Algorithmen 31 Term Index Symbol Typ Parameteranzahl Parameterliste Y 0 Y VAR 0 a 1 a STR 0 f (a) 2 f STR 1 1 h(Y, f (a)) 3 h STR 2 02 X 4 X VAR 0 f (X) 5 f STR 1 4 p(f (X), h(Y, f (a)), Y ) 6 p STR 3 530 W 7 W VAR 0 f (W ) 8 f STR 1 Z 9 Z VAR 0 h(Z, W ) 10 h STR 2 97 p(Z, h(Z, W ), f (W )) 11 p STR 3 9 10 8 7 Tabelle 3.2: Unification Table # Sx Sy Term i Term j 1 6 11 p(f (X), h(Y, f (a)), Y ) p(Z, h(Z, W ), f (W )) 1 2 530 9 10 8 f (X) Z 2 3 30 10 8 h(Y, f (a)) h(Z, W ) 1 4 020 978 Y Z 4 Y →Z 5 20 78 f (a) W 2 W → f (a) 6 0 8 Y f (W ) 3 7 5 8 f (X) f (W ) 1 8 4 7 X W 4 Tabelle 3.3: schrittweiser Ablauf des Algorithmus Fall Substitution Z → f (X) X→W 3 Algorithmen 32 ob deren Terme schon einmal unifiziert wurden. Da außerdem der Occurcheck fehlt kann es an dieser Stelle scheinbar auch zu einer Endlosschleife kommen. Beispiel 17. Bei der Unifikation von f (X, X) =? f (g(X), X) wird zuerst X an g(X) gebunden. Im nächsten Schritt der while-Schleife wird dann von beiden Stacks X gelesen. Da beide Terme gebunden sind wird g(X) auf die Stacks geschrieben, was sich in einer Endlosschleife wiederholt. Kapitel 4 Implementierung In diesem Kapitel wird der zugrunde liegende Aufbau der Implementierung vorgestellt und es werden die Probleme die bei der Implementierung der verschiedenen Algorithmen gelöst werden müssen besprochen. Außerdem wird untersucht ob einzelne Algorithmen für den späteren Vergleich angepasst werden müssen. Den Abschluss des Kapitels bildet ein UML Diagramm in dem die wichtigsten Pakete und Klassen enthalten sind. 4.1 Ausgaben der Algorithmen Algorithmen kann man nur objektiv vergleichen wenn sie die selbe Arbeit erledigen, aber die hier vorgestellten Algorithmen haben teilweise sehr unterschiedliche Ausgaben: • Robinsons Algorithmus erzeugt eine normale Substitution. • Der Algorithmus von Ruzicka und Privara erzeugt überhaupt keine Substitution, sondern stellt nur fest ob die beiden Terme unifizierbar sind oder nicht. • Der Algorithmus von Paterson und Wegman erzeugt eine geordnete Substitution. Diese kann man aber nicht einfach in eine normale (ungeordnete) Substitution umwandeln. • Der Algorithmus von Suciu erzeugt ebenfalls keine Substitution. Außerdem führt er keinen Occurcheck durch, das heißt es kann zu fehlerhaften Ergebnissen kommen. Beispielsweise wird bei der Unifikation von f (X) =? X kein Fehler ausgegeben, versucht man dann auch noch die Substitution aus dem Unification Table auszulesen entsteht sogar eine Endlosschleife. 4 Implementierung 34 Eine geordnete Substitution lässt sich in jedem Fall sehr schnell erstellen wenn man bei der Variableneliminierung einfach die entsprechenden Terme beziehungsweise Knoten speichert. Als Ausgabe der Unifikation ist diese Darstellung aber ungeeignet, da die Reihenfolge der Variableneliminierungen nicht der Reihenfolge in der die Einzelsubstitutionen ausgeführt werden müssen entsprechen muss. Beispiel 18. In Paterson und Wegmans Algorithmus hängt die Reihenfolge der Variableneliminierungen davon ab in welcher Reihenfolge die Äquivalenzbeziehungen erstellt werden, und damit vom Aufbau der Terme und von der Reihenfolge in der die Knoten in der Hauptfunktion durchlaufen werden. Erhalten wir zum Beispiel die Substitutionen in der Reihenfolge {D → E}, {C → D}, {A → B}, {B → C}, {E → F } und wollen damit den Term h(A, B, C, D, E, F ) substituieren, muss bei jedem Parameter die Menge mehrmals durchsucht werden. Mit jedem Algorithmus eine normale Substitution auszugeben ist auch keine Option, da bei bestimmten Unifikationsproblemen der allgemeinste Unifikator exponentiell groß im Verhältnis zur Eingabe ist und dessen Erzeugung die Laufzeiten einzelner Algorithmen übersteigen würde. Beispiel 19. Bei der Unifikation von f (X1 , .., Xn ) =? f (g(X0, X0 ), .., g(Xn−1, Xn−1 )) aus Beispiel 15 tritt dieses Problem auf. Der allgemeinste Unifikator ist σ = {X1 → g(X0 , X0 ), .., Xn → g(g(..g(X0, X0 )..), g(..g(X0, X0 )..))}. Es bleibt nur die Möglichkeit die Unifikation auf die Ausgabe true und false zu beschränken und den allgemeinsten Unifikator, als normale Substitution, so zu konstruieren das er bei Bedarf ausgelesen werden kann. Wir werden die Algorithmen deshalb wie folgt implementieren: • Durch die abstrakte Klasse Unify wird eine Schnittstelle für den Vergleich bereitgestellt. Alle Unifikationsalgorithmen werden von ihr abgeleitet. Für den Test wird die Funktion unify(t1 ,t2 ) benutzt, die true beziehungsweise false ausgibt. • Die eigentlichen Algorithmen werden in der Funktion unify rek(t1 ,t2 ) implementiert. Zusätzlich wird die Funktion getMGU implementiert, mit der man den allgemeinsten Unifikator als normale Substitution auslesen kann. • Robinsons Algorithmus kann unverändert übernommen werden. • Für die Algorithmen von Paterson und Wegman, sowie Ruzicka und Privara wird der allgemeinste Unifikator nach der Methode von Champeaux [5] erstellt. Dabei 4 Implementierung 35 wird auch die Substitution als DAG dargestellt, da so in linearer Zeit eine geordnete Substitution erstellt werden kann. • Sucius Algorithmus wird einmal in seiner ursprünglichen Form ohne Occurcheck implementiert und zusätzlich mit einem einfachen Occurcheck. 4.2 Testsystem • Prozessor: AMD Athlon 64 3000+ (2000 MHz) • Hauptspeicher: 2GB • Betriebssytem: Windows 2000 SP4 • Java Runtime Environment: 1.6.0 01 4.3 Zeitmessung Da wir ein Windows System zum testen verwenden müssen wir uns überlegen wie wir die Laufzeiten der Algorithmen messen, da hier die Systemuhr einen Takt von 10 Millisekunden aufweist und wir für kleine Terme wesentlich kürzere Abstände messen müssen. Es stellt sich heraus das wir die Funktion nanoTime aus der Klasse java.lang.System verwenden können. Sie benutzt nicht wie in der Java API angegeben die Systemuhr, sondern die Windows Funktion QueryPerformanceCounter, welche die Anzahl der Taktzyklen seit dem Systemstart liefert. Dieser Wert wird mit Hilfe der Funktion QueryPerformanceFrequency in Nanosekunden umgerechnet und dann ausgegeben. Das Ergebnis ist im Bereich von Nanosekunden präzise, aber nicht zwingend genau, was bedeutet das wir bei gleichbleibenden Rahmenbedingungen immer den selben Wert messen, dieser aber nicht zwingend dem realem Wert entsprechen muss. Da wir nur die Laufzeiten der Algorithmen untereinander, auf demselben Testsystem, vergleichen wollen stört uns das nicht. 4.4 Speicherverwaltung Besonders problematisch bei der Zeitmessung ist das Garbage Collecting der JVM. Tritt es im Hintergrund einer Messung auf verlangsamt es deren Ausführung meist soweit das 4 Implementierung 36 diese Messung unbrauchbar wird. Steuern lässt es sich nur durch die Funktion gc unter java.lang.System, allerdings tut diese Funktion genau das was wir vermeiden wollen. Sie startet das Garbage Collecting in einem Thread im Hintergrund und es gibt keine Möglichkeit den Programmfluss solange zu unterbrechen bis die Speicherverwaltung abgeschlossen ist. Wir sind daher gezwungen jede Messung sehr oft zu wiederholen um brauchbare Werte zu erhalten. 4.5 Testablauf Die Testdurchläufe mit den einzelnen Termen werden mit der Klasse Comparison durchgeführt. Dafür muss im Quelltext nur die Liste der Algorithmen angepasst und die entsprechende Term-Factory (abgeleitet von TestTerm) initialisiert werden. In einer Schleife wird dabei jeder Algorithmus für jedes n mehrmals aufgerufen. Von diesen Aufrufen wird jeweils die Zeit gemessen um am Ende das Minimum auszugeben. Bei dem verwendetem Testsystem hat sich eine Anzahl von 105 Aufrufen als ausreichend erwiesen. 4 Implementierung Bild 4.1: UML Klassendiagramm der wichtigsten Klassen und Pakete 37 Kapitel 5 Vergleich In diesem Kapitel werden wir anhand einiger Beispielterme die Laufzeiten der verschiedenen Algorithmen untersuchen. Dabei interessieren wir uns vor allem für die folgenden Punkte: • Wie sieht der Worst-Case von Robinsons Algorithmus aus? • Wie wirkt sich der zusätzliche Occurcheck bei Suciu aus? • Ist der Algorithmus von Suciu wirklich effizient? • Wie sehr unterscheiden sich die Laufzeiten der Algorithmen von Paterson und Wegman und Ruzicka und Privara? • Ab welcher Eingabegröße ist Paterson und Wegman der schnellste Algorithmus? 5 Vergleich 39 t1 = f (X1 , .., Xn ) t2 = f (g(X0 , X0 ), .., g(Xn−1, Xn−1 )) Die beiden Terme sind unifizierbar und es entsteht ein exponentiell großer allgemeiner Unifikator. An dem Unterschied zwischen Sucius Algorithmus mit und ohne Occurcheck kann man sehr gut sehen das dieser bei bestimmten Termen zu exponentieller Laufzeit führt. Ab circa n > 48 ist der Algorithmus von Paterson und Wegman schneller als der von Ruzicka und Privara. 70 Robinson Suciu mit Occurcheck Suciu Ruzicka Privara Paterson Wegman 65 60 55 50 45 40 35 30 25 20 15 10 5 0 1 1 2 3 4 5 6 7 8 9 10 Auf den folgenden Diagrammen ist jeweils auf der Abszisse die Größe n und auf der Ordinate die Zeit in Mikrosekunden eingetragen. 5 Vergleich 40 t1 = f (p(X1 , .., Xn ), q(X1 , .., Xn )) t2 = f (p(g(X0 , X0 ), .., g(Xn−1, Xn−1)), q(g(X0, X0 ), .., g(Xn−1, Xn−1 ))) Wie beim vorhergehenden Testfall wird hier in Funktion p jede Variable Xi durch einen immer größer werdenden Baum substituiert. In der Funktion q müssen diese Bäume dann mit jeweils einem identischem Baum unifiziert werden. Offensichtlich ist der Algorithmus von Suciu, selbst ohne Occurcheck, für diesen Testfall nicht effizient! Ab circa n > 16 ist der Algorithmus von Paterson und Wegman schneller als der von Ruzicka und Privara. 100 Robinson Suciu mit Occurcheck Suciu Ruzicka Privara Paterson Wegman 90 80 70 60 50 40 30 20 10 0 1 2 3 4 5 5 Vergleich 41 t1 = f (X1 , .., Xn−1 , h) t2 = f (g(X0 , X0 ), .., g(Xn−1, Xn−1 )) Die beiden Terme sind nicht unifizierbar, da der letzte Parameter unterschiedliche Funktionssymbole hat. In diesem Testfall ist der Algorithmus von Ruzicka und Privara auch für kleine Terme schlechter als der Algorithmus von Paterson und Wegman. Das liegt daran das aufgrund der Tiefensuche die Terme h und g(Xn−1 , Xn−1) als letztes betrachtet werden. Der Algorithmus von Paterson und Wegman betrachtet diese Terme früher, da er den Graphen von oben nach unten abarbeitet. 75 Robinson Suciu mit Occurcheck Suciu Ruzicka Privara Paterson Wegman 70 65 60 55 50 45 40 35 30 25 20 15 10 5 0 1 2 3 4 5 6 7 8 9 10 5 Vergleich 42 t1 = f (X, g n (X), X) t2 = f (g n (Y ), Y, Y ) Die beiden Terme sind wegen des Occurcheck nicht unifizierbar. Der Algorithmus von Suciu ohne Occurcheck gerät wie erwartet in eine Endlosschleife und ist deshalb nicht in dem Diagramm enthalten. 25 Robinson Suciu mit Occurcheck Ruzicka Privara Paterson Wegman 22,5 20 17,5 15 12,5 10 7,5 5 2,5 0 1 2 3 4 5 6 7 8 9 10 5 Vergleich 43 t1 = f (X1 , .., Xn ) t2 = h(g(X0, X0 ), .., g(Xn−1, Xn−1 )) Die beiden Terme sind nicht unifizierbar, da sie unterschiedliche Funktionssymbole haben. Robinsons Algorithmus hat hier bei beliebiger Eingabelänge eine konstante Laufzeit, da er keine Vorverarbeitung benötigt und gleich beim ersten Vergleich das Ergebnis feststellen kann. 32,5 Robinson Suciu mit Occurcheck Suciu Ruzicka Privara Paterson Wegman 30 27,5 25 22,5 20 17,5 15 12,5 10 7,5 5 2,5 0 1 2 3 4 5 6 7 8 9 10 5 Vergleich 5.1 44 Schlussbemerkung Im Rahmen dieser Arbeit wurden vier Algorithmen für die Unifikation von Termen vorgestellt, in Java implementiert und anhand einiger Beispiele bezüglich der Laufzeit verglichen. Es konnte gezeigt werden das der Algorithmus von Suciu im Worst-Case nicht effizient ist und das er bei bestimmten Termen in eine Endlosschleife gerät. Um das Halteproblem zu lösen wurde der Algorithmus um einen Occurcheck erweitert, was allerdings bei den anderen Tests zu einer noch schlechteren Laufzeit führte. Insgesamt ist der Algorithmus damit nicht wesentlich besser als Robinsons. Bei dem Vergleich wurde weiterhin deutlich das der lineare Algorithmus von Paterson und Wegman selbst bei den sehr konstruierten Testtermen erst bei großen Werten für n schneller ist als der fast lineare Algorithmus von Ruzicka und Privara. Letzterer scheint deshalb für praktische Anwendungen allgemein besser geeignet zu sein. Betrachtet man diese Algorithmen in Zusammenhang mit der Typ-Inferenz-Prüfung eines Compilers, bei der hauptsächlich kleine Terme unifiziert werden müssen, stellt man fest das der Algorithmus von Robinson eine gute Wahl zu seien scheint. Denn selbst wenn der Worst-Case auftreten sollte, ist er dort für kleine n immer noch schneller als der Algorithmus von Ruzicka und Privara. Das vereinzelte Auftreten des Worst-Case mit größeren n lässt sich sehr wahrscheinlich durch den Geschwindigkeitsvorteil bei den restlichen Unifikationen ausgleichen. Literaturverzeichnis [1] L. Albert, R. Casas, F. Fages, A. Torrecillas, and P. Zimmermann. Average case analysis of unification algorithms. In STACS, pages 196–213, 1991. [2] M. J. Atallah and S. Fox, editors. Algorithms and Theory of Computation Handbook. CRC Press, Inc., Boca Raton, FL, USA, 1998. Produced By-Suzanne Lassandro. [3] F. Baader and J. H. Siekmann. Unification theory. Handbook of logic in artificial intelligence and logic programming, pages 41–125, 1994. [4] L. Baxter. A Practically Linear Unification Algorithm. Waterloo, Ont., Canada : University of Waterloo, Computer Science Dept., 1976. [5] D. D. Champeaux. About the paterson-wegman linear unification algorithm. J. Comput. Syst. Sci., 32(1):79–90, 1986. [6] J. Corbin and M. Bidoit. A rehabilitation of robinson’s unification algorithm. In IFIP Congress, pages 909–914, 1983. [7] R. Green. Benchmarking, measuring elapsed time. http://mindprod.com/jgloss/time.html, 2007. [8] G. P. Huet. A unification algorithm for typed lambda-calculus. Theor. Comput. Sci., 1(1):27–57, 1975. [9] E. Jacobsen. Unification and anti-unification, 1991. [10] A. Martelli and U. Montanari. An efficient unification algorithm. ACM Trans. Program. Lang. Syst., 4(2):258–282, 1982. [11] R. Parchmann. Skript zur vorlesung programmiersprachen und Übersetzer, 2006. [12] M. S. Paterson and M. N. Wegman. Linear unification. In STOC ’76: Proceedings of the eighth annual ACM symposium on Theory of computing, pages 181–186, New York, NY, USA, 1976. ACM Press. LITERATURVERZEICHNIS 46 [13] M. S. Paterson and M. N. Wegman. Linear unification. J. Comput. Syst. Sci., 16(2):158–167, 1978. [14] D. Rémy and B. Yakobowski. A graphical presentation of mlf types with a lineartime unification algorithm. In TLDI ’07: Proceedings of the 2007 ACM SIGPLAN international workshop on Types in languages design and implementation, pages 27– 38, New York, NY, USA, 2007. ACM. [15] J. A. Robinson. A machine-oriented logic based on the resolution principle. J. ACM, 12(1):23–41, 1965. [16] P. Ruzicka and I. Privara. An almost linear robinson unification algorithm. Acta Inf., 27(1):61–71, 1989. [17] A. Suciu. Yet another efficient unification algorithm, 2006.