pdf-Version - Leibniz Universität Hannover

Werbung
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.
Herunterladen