• Keine Ergebnisse gefunden

Lenken wir die var-Kanten in einen gc-free-Graphen eines CH-Ausdrucks, dann werden diese zu OOLDG mit einer entscheidenden Zusatzeigenschaft.

letvar bind Lemma 4.2. Sei t ein gc-free CH-Ausdruck und G(t) der aus t konstruierten LDG (siehe Definition 3.2.). Der Graph G-(t) erfüllt die Voraussetzung von (Lemma 2.3.). G-(t) ist ein OOLDG mit eindeutigem Startknoten.

Beweis.

Seien t ein CH-Ausdruck, G(t) der gc-free LDG von t und G-(t) der Graph, der durch umlenken der var-Kanten in G(t) entsteht.

Erreichbarkeit von Startknoten:

Garbagefreiheit impliziert Erreichbarkeit von Startknoten, ohne Benutzung der var-Kanten.

OO(.) Eigenschaft:

DerG-(t) ist ein OOLDG, die ausgehenden Kanten in G(t) sind geordnet bis auf die var-Kanten. Var-kanten gibt es nur zwischen Vaterknoten (letrec) und Kindknoten (letvar).

Werden diese ungelenkt, dann gibt es von jedem Kindknoten nur eine einzige var-Kante zum Vaterknoten (letrec), damit ist G-(t) ein OOLDG.

Eindeutigkeit des Startknotens:

. G-(t) ist Rooted, falls der Startknoten in G(t) kein letrec-Knoten ist. Also ist er ein ROOLDG.

. Falls in G(t) der Startknoten ein letrec-Knoten ist, dann ist G-(t) ein OOLDG mit einem Startknoten der nur var-Kanten als eingehende Kanten hat. Dieser Knoten kann

nur einmal im Graphen vorkommen.

Begründung: Jeder letrec-Knoten der kein Startknoten in G(t) ist, muss notwendigerweise einen Vaterknoten haben (wegen Erreichbarkeit vom Startknoten), von dem aus eine Kante in den letrec-Knoten eingeht, die nicht mit var beschriftet sein kann. (Var-Kanten gibt es nur zwischen letrec-Knoten und letvar-Knoten. Eine Letrec-Knoten kann kein letvar-Knoten sein).

Es ist einfach zu sehen, dass G(t) in G- in Zeit O(n log n) mit n = |G(t)| überführt werden kann.

Es reicht über alle Knoten im Graphen zu iterieren, falls der Knoten ein Letrec ist. Dann greifen wir auf die Liste seiner ausgehenden Kanten zu. Jede Kante die mit var beschriftet ist, kann man mit konstante Zugriffen umlenken ( (v, u, var)−→ (u, v, var)). Benutzt man eine effiziente Datenstruktur zur Speicherung von Knoten des Graphen, kann dies inO(n log(n)) mitn= Anzahl der Knoten durchgeführt werden.

Nach dem Umkehren der var-Kanten im Graphen G(t) bekommt man einen OOLDG mit einemeindeutigenStartknoten (Root, oder Startknoten mit nur Var-eingehenden Kanten). Dies erfüllt die hinreichende Bedingung für einen effizienten Isomorphietest in der OOLDG Klasse.

Proposition 4.3. Aus Lemma (2.1., 2.3. und 4.2.) folgt: Zwei gc-freie-CH-Ausdrücke t1 und t2 sind genau dann alpha-äquivalent, wenn G-(t1)' G-(t2) gilt. Dies kann in O(n log(n)) getestet werden.

Beweis.

Zuerst werden die gc-freien Graphen für die CH-Ausdrücke erstellt, danach werden die modifizierten Graphen daraus konstruiert (Lemma 4.2.), die Isomorphie auf solchen Graphen kann effizient getestet werden (Lemma 2.3.). Die Isomorphie von modifizierten Graphen entspricht die Isomorphie von deren entsprechenden gc-free Graphen(2.3).

Schlussfolgernd sind t1 und t2 genau dann alpha-äquivalent, wenn G- (t1) ' G- (t2) . Anhand der geführten schritte sieht man, dass das inO(n log(n)) geht.

Definition 4.1.

CH-Ausdrücke s und t sind alpha-äquivalent bis auf unbenutzte Bindungen (modulo garbage collection), geschriebens'α,gc,CH tgdw. die gc-Normalformen s0 undt0 von s und t alpha-äquivalent sind.

Proposition 4.4. Für Zwei CH-Ausdrücke s1 und s2 es ist möglich s1 'α,gc,CH s2 in der Zeit O(nlog(n))mit n=|s1|+|s2|zu testen.

Beweis.

Zuerst werden die alpha-äquivalenten gc-Normalformen berechnet mittels Lemma 4.1.

und dann wird Proposition 4.3. angewendet.

5 Algorithmen

Der Weg zur Implementierung eines Algorithmus für algorithmische Probleme geht über die Überlegung, wie man für kritische Stellen bei der Problembehandlung, eine maßgeschneiderte Lösung konzipiert, mit dem Ziel, unter Beachtung der klassischen Komplexitätsgrößen der Informatik, die Zeit und die Flächenkomplexität, eine effiziente Lösung zu präsentieren.

Für das gegenwärtige Problem gliedert sich die Lösung in drei Hauptteile:

• Graphkonstruktion: Aus einem Lch-Ausdruck s wird ein Graph G(s) konstruiert.

• gc-collection: Die nicht benutzten Bindungen werden entfernt.

• aus dem gc-free LDG für den CH-Ausdruck t wird der G- (t) gewonnen durch Umkehren der var-Kanten.

• Isomorphietest: Isomorrphietest auf G-(ti) durchführen.

Dazu gehört zunächst eine geeignete Repräsentation des Problems und eine Überlegung zur günstigen Bereitstellung der Daten. Schließlich muss auf die Daten ständig zugegriffen werden. Sie werden unter anderem gelöscht, aktualisiert, ersetzt und erweitert. Dies muss möglichst effizient geschehen, denn die Auswahl geeigneter Datenstrukturen trägt erheblich dazu bei die gegebene Problemlösung zu beschleunigen.

Anschließend soll diese Lösung in eine Programmiersprache umgesetzt werden. Die Problemlösung ist grundsätzlich unabhängig von der zur Implementierung benutzten Programmiersprache. Allerdings kann neben der Effizienz, die Auswahl einer Sprache ent-scheidende Vorteile mitbringen, wie wir im KapitelImplementierung sehen werden.

5.1 Design

Zur Lösung des Alpha-Äquivalenz-Problems von Ch-Ausdrücken wollen wir die auf Äqui-valenz zu testende Ausdrücke als Graphen darstellen. Dies haben wie wir in Kapitel 3 vorgestellt. Daher brauchen wir eine geeignete Datenstruktur zur Graphendarstellung, die wir schnell durchsuchen und an beliebigen Stellen modifizieren können.

Datenstruktur für Wörtebücher.

Eine Datenstruktur, die aus Effizienzgründen bei der Implementierung in mehreren Stellen benutzt wird, ist die sogenannte dynamische Wörterbuchdatenstruktur. Sie verwaltet Daten in einer Menge von Datensätzen. Die Datensätze werden als (Schlüssel, Wert)-Paar gespeichert. Jedem Schlüssel ist ein Wert zugeordnet und jeder Wert ist über ein Schlüssel eindeutig identifizierbar. Die Datenstruktur ist dynamisch, weil sie neben „Suchen“ die Operationen „Einfügen“ und „Löschen“ unterstützt, statt nur „Suchen“, wie die statische Variante einer Wörterbuchdatenstruktur.

Die Wörterbuchdatenstruktur für eine Menge S besteht aus den folgenden Operationen:

• einfügen (x) : S=S∪ {x}.

• löschen (x) : S=S\ {x}.

• suchen (x) : finde heraus, obxS und greife gegebenenfalls auf den Datensatz von x zu.

Es gibt mehrere Möglichkeit solche Datenstrukturen zu implementieren, wir benutzen die Variante der Wörterbücherdatenstrukturen , die intern als Binärbäume implementiert sind. Dank dieser Implementierung erhalten wir folgende Eigenschaft: Für jeden Knoten n mit dem Schlüssel(n)= k gilt; alle Schlüssel aus dem linken Teilbaum sind kleiner als der Schlüssel k und alle Schlüssel aus dem rechten Teilbaum sind größer als der Schlüssel k. Binärbäume mit dieser Eigenschaft werden auch Suchbäume genannt.

Datenstruktur für Graphen.

Die zwei gängigsten Darstellungsformen von Graphen sind die Adjazenzmatrix und die Adjazenzliste. Beide Darstellungsformen haben ihre Vor- und Nachteile. Adjazenzmatrizen eignen sich besser für dichte2 Graphen und sie ermöglichen es sehr schnell auf die Kanten des Graphen zuzugreifen, haben aber einen quadratischen Speicherbedarf bezüglich der Anzahl der Knoten. Die Adjazenzmatrix verbraucht für Graphen mit n Knoten Θ(n2) Speicher, unabhängig von der Größe m =|E| des Graphen. Denn für je zwei Knoten aus dem Graphen wird ein Eintrag in der Adjazenzmatrix hinterlegt, auch wenn die Knoten nicht in Beziehung zueinander stehen (False oder 0 für nicht vorhandene Kanten).

Dadurch wird Speicherplatz, um irrelevante Informationen zu speichern, verschwendet.

Aus diesem Grund sind Adjazenzlisten die bessere Alternative für dünne3 Graphen. Sie verbrauchen nur linear viel Speicherplatz. Die Knoten des Graphen werden in einer Liste gespeichert. Zu jedem Knoten wird eine weitere Liste mit den Nachbarknoten angehängt.

Um einen Graphen mit n Knoten und m Kanten zu speichern, benötigt eine Adjazenzliste n + m Speicherplätz, falls der Graph gerichtet ist und n +2m Speicherplätze, falls er ungerichtet ist.

Meistens ist es wichtiger die Nachbarn eines Knotens zu bestimmen, als schnell auf eine bestimmte Kante zuzugreifen. Dies ist ein weiteres Effizienzkriterium, das die Adjazenz-listendarstellung gegenüber die Adjazenzmatrixdarstellung auszeichnet.

Graphdarstellung.

In der anschließenden Beschreibung wird davon ausgegangen, dass die Knotennamen Integerwerte von 0 bis (| V | -1) sind. Die Knoten und Kanten enthalten Labels als zusätzliche Informationen. Formal wird eine Kante zwischen zwei Knoten i und j (falls

2Graphen mit vielen Kanten

3Graphen mit wenig Kanten

sie existiert) folgendermaßen definiert: (i, (Labeli ,(j, Labeli,j)).

Um die Daten (Knoten, Kanten, Labels) zu verwalten wird eine Datenstruktur verwen-det, die die Vorteile beider vorgestellten Datenstrukturen, Wörterbücher und Adjazenz-listen kombiniert. Es werden (Schlüssel,Wert)-Paare gebildet. Die Knoten stellen die Schlüsseln dar. Die Werte haben eine komplexere Darstellung in Form eines Tupels (La-bel,[(Knoten,Label)]). Bevor wir die Komponenten dieses Tupels erläutern, beschreiben wir zuerst, wie wir den Graphen definieren wollen.

Der Graph ist eine Liste von Tupeln vom Gestalt: (Knoten,(Label,[(Knoten, Label)]).

Jede solcher Tupel kann man als eine Abbildung von einem Knoten auf seine Nachfolger-knoten sehen und es stellt die Liste seiner ausgehenden Kanten dar.

Knotenv

Liste der ausgehenden Kanten eines Knotens

Werte werden als Tupels gespeichert. Der erste Eintrag im Tupel (Label, [(Knoten,Label)]) steht für das Label des Anfangsknotens. Der zweite Eintrag ist eine Liste der Nachfolger-knoten, die selbst eine Liste von Tupeln [(Knoten,Label)] ist. Jedes Tupel (Knoten,Label) beschreibt den Endknoten einer Kante und ihre Label. Am Ende ergibt sich folgende Darstellung für den Graphen:

[...,(Knoten, (Label,[(Knoten, Label)])), ...].

Die zu konstruierende GraphenG(s) sind sogenannte LDGs, die gerichtete und beschrifte-te Graphen sind. Die Sprache CH ist rekursiv definiert. Terme in dieser Sprache entsbeschrifte-tehen durch Verkettung und Verschachtelung der Teilausdrucke, die mittels Konstruktoren der Sprache gebildet werden.

Aus der Definition der Sprache CH und die in der Definition 3.2. genannten Konstrukti-onsregeln zur Grapherstellung ist es leicht zu sehen, dassG(s) einen Anfangsknoten hat, aus dem alle Knoten desG(s) erreicht werden. Also istG(s) ein zusammenhängender LDG mit Startknoten. Der Anfangsknoten wird als Startknoten bezeichnet.

Ein CH-Ausdruck s liegt uns als Syntaxbaum vor. Die Erstellung des Graphen G(s) entsteht, indem der Syntaxbaum für den Ausdruck von der Wurzel aus absteigend (top-down) durchlaufen wird und dabei die Knoten und Kanten schrittweise, nach dem in Definition 3.2. vorgestellten Verfahren, erstellt werden.

Beispiel für Syntaxbaum eines CH-Ausdrucks.

simpleExpr=letrec x=λt.t, y=λh.k, n=y in constr”my”[(V ar”x”),(V ar”n”)]

letrec

Je nachdem, was wir gerade für einen Unterausdruck bearbeiten, variieren die Details, die wir beachten müssen um den Graphen richtig zu konstruieren.

Lambda- und letrec-Ausdrücke benutzen geschachtelte Geltungsbereiche von lokalen Variablennamen. Problematisch wird es, wenn ein Lch Ausdruck nicht die (dvc) Distinct Variable Convention erfüllt. Damit wir auch in diesem Fall korrekt arbeiten, führen wir Buch über die freien und gebundenen Variablen.4.

Der Übersicht halber führen wir die wesentlichen Schritte anhand eines Beispiels vor. Neh-men wir an, der Ausdruck den wir grade bearbeiten, entspricht den Lambda Konstruktor λx.s. S ist wiederum ein beliebiges Lch Ausdruck den wir body vom Lambda nennen. Die Variable x wird von Lambda gebunden, also speichern wir die Variable x als gebunden.

Speichern heißt gleichzeitig auch, Knoten für x zu erstellen. Alle Vorkommnisse von x in e referenzieren die gebundene Variable x. Sollte in e eine Variable mit dem Namen x vorkommen, dann wird der Knoten für x zurückgegeben. Was natürlich auch passieren kann, ist, dass in einem Unterausdruck von e der Variablenname x ebenfalls von einem λ oder letrec gebunden wird. In diesem Fall wird die Bindung überschrieben, d.h. die innerste nächst liegende Definition von Variable x wird referenziert. Konkret, es wird für x einen neuen Knoten mit der aktuellen Position (aktuellen Knoten counter Stand um 1 inkrementiert) erstellt und in die gebundenen Variablen hinzugefügt. Dies wird rekursiv angewendet auf den gesamten Ausdruck.

Treffen wir zum ersten Mal auf eine Variable , dann erstellen wir einen Knoten mit den Variablennamen und fügen ihn zu den freien Variablen hinzu. Treffen wir beim zweiten oder i-tem Mal auf die selbe frei Variable, wird der Knoten mit de selben Variablennamen zurückgegeben.

4Die Definition und die Methode zur Berechnung von freien und gebundenen Variablen haben wir bereits in den Grundlagen vorgestellt

Dies effizient zu verwalten, speichern wir die freien bzw. die gebundenen Variablen in jeweils eine Wörterbuchdatenstruktur : V ariablen N ame

| {z }

in der wir per lookup schnell nach Variablen abfragen können und die entsprechenden variablen Knoten zurückgeben können, falls sie schon drin ist, oder per insert einfügen, falls sie noch nicht drin ist.

Letrec-Ausdruck oder genauer gesagt die Menge der Bindungen bedarf ein Sonderbe-handlung bei der Knotenerstellung. Die Bindungen sind rekursiv definiert im Ausdruck letrec{x1 =s1, ..., xn=sn}in t, wobeixi verschiedene Variablennamen sind undsi und t CH-Ausdrücke sind.xi kann in jedem si und t vorkommen, man sagt, der Gültigkeits-bereich vonxi, ist si und t, z.B. kann x1 in s1 unds2 vorkommen und gleichzeitigx2 in s1 und s2 vorkommen.

Wir erstellen Knoten für jedenxi und fügen diese zu den gebundenen5 Variablen ein. Erst dann behandeln wir diesi. Grund dafür ist, sollten wir einexi zuerst als frei vorkommend in einsi sehen, dass wir sie als frei speichern. Wenn wir aber die selbe Variable später in der linken Seite einer Bindung (Env) sehen, dann müssen wir einen Knoten für die selben Variablennamen, den wir zuvor als frei gespeichert haben, erstellen und dieses Mal als gebunden speichern, was natürlich falsch wäre.

Betrachten wir das folgende Beispiel:letrec x=y

| {z }

Wir erstellen für x inEnv1 und y in Env2 jeweils einen Knoten und fügen sie zu den gebundenen Variablen ein. Dann betrachten wir y in der rechten Seite von Env1 und geben dafür den Knoten für y in Env2 zurück. Danach betrachten wir z auf der rechten Seite vom Env2. Wir erstellen Knoten für die Variable z und fügen sie zu den freien variablen ein.

Diese Vorgehensweise vermeidet Kollisionen (freie Variablen einzufangen) der Art, dass die selbe Variable im selben Geltungsbereich jeweils einen Eintrag bei den freien und bei den gebundenen Variablen bekommt. Es ermöglicht es stets auf die richtige Variable beim Erstellen von Knoten und Kanten zu beziehen.

Ausblick für den Abschnitt: Effiziente Datenstrukturen zur Graphendarstellung und das Erörtern kritischer Stellen, die Probleme bei der Graphkonstruktion auslösen können, stellen den Schwerpunkt des letzten Abschnitts dar. Daraus haben wir Ansätze zur korrekten Umwandlung eines Lch-Ausdrucksbaums in einen LDG entwickelt.

Jetzt sind wir in der Lage einen Ausdruck verlustfrei als Graphen zu repräsentieren. Dieser Schritt ist Grundlage und Voraussetzung für die nächste Phase bei der algorithmischen Lösung des Alpha-Äquivalenzproblems in CH, nämlich die garbage collection.

5An diese Stelle weisen wir nochmal an die Definition der freien und gebundenen Variablen für letrec hin

Garbage collection.

Garbage oder unbenutzte Bindungen können nur bei letrec vorkommen. Um das festzu-stellen und gegebenenfalls zu entfernen, müssen wir den Graphen traversieren. Aber wie stellen wir graphisch fest, dass eine Variable nicht benutzt wird?

Ein letrec Ausdruck besteht aus dem Konstruktornamen letrec gefolgt von einer Menge von Definitionen, die in Body benutzt werden. Das ist so zu verstehen, wie letrec Definitionen in Einsatz der Definition. Beispiel. "letrec x1 =s1, ..., xn=sn

| {z }

• Environments = Menge von Bindungen der Form (xi := si), wo die Variablen definiert werden.

• Body = S, wo die definierten Variablen eingesetzt werden.

S, si sind beliebige Lch Ausdrücke.

Die Kanten werden folgendermaßen gezogen:

von Knoten letrec:

• Zu jeder Variablen x1 mit dem labelvar

• Zu Body mit dem label in In die Environment:

• Eine Kante mit dem label Bind von Knoten (xi) zu Knoten (si) ziehen.

Von Body:

• Von Body-Knoten werden Kanten zu den in Body benutzten Bindungen, Knoten (xi) gezogen.

• Diejenigen Variablen die erst in Body frei oder gebunden auftauchen, werden entsprechend zu den Konstruktionsregeln in Definiton 3.2 behandelt.

In einem CH-Ausdruck können letrec-Unterausdrücke irrelevante Variablen in ihre Envs enthalten. Intuitiv wollen wir nur die Variablen, die tatsächlich benutzt werden, ermitteln.

So wollen wir den repräsentierenden Graphen durchlaufen und immer, wenn wir einen letrec-Knoten erreichen, müssen wir nur Wege benutzen, die über Body gehen. Anders ausgedrückt, Wege, die nur Kanten benutzen und die nicht mit var beschriftet sind.

Dadurch, dass die Variablen in dem Environment auch rekursiv definiert sein können, wird garantiert (strukturelle Rekursion), dass jede benutzte Variable ohne Benutzung der var-Kanten erreicht wird.

Ziel dieses Abschnitts ist, aus dem konstruierten Graphen G(s) für den Ausdruck s einen Untergraphen zu konstruieren, deren Knoten ausschließlich über Wege, die keine var-Kanten benutzen, vom Startknoten aus erreichbar sind. Also einen Garbage-free

Graph.

Die folgende Schritte beschreiben den Weg zur Gewinnung des gc-free-Graphen ausgehend vom G(s).

• Finde alle von der Wurzel erreichbaren Knoten ohne Benutzung von var-Kanten.

• Lösche alle nicht erreichbaren Knoten.

• lösche alle Kanten, die zu nicht erreichbaren Knoten führen.

Graph Traversieren.

Das Traversieren ist ein geordneter Ablauf der Knoten eines Graphen. Es spielt eine wichtige Rolle in vielen graphbasierten Algorithmen. Die Tiefensuche entspricht der Preorder-Traversierung eines Graphen, bei der man zunächst den erreichten Knoten besucht und dann in die angeschlossenen Knoten absteigt. Graphen können Kreise ent-halten. Um nicht in einer endlosen Schleife zu geraten, werden die bereits besuchten Knoten gemerkt (Tiefensuche = Präorder + Markierung). So wird verhindert, dass die bereits besuchten Knoten während des Ablaufs des Traversierens nochmal besucht werden.

Dies wird üblicherweise mit einer rekursiven Routine realisiert. Zu erst wird geschaut, ob das erreichte Knoten i noch nicht als besucht markiert ist. Erst dann wird die Tiefensu-che auf i aufgerufen. Anschließend wird die Adjazenzliste von i durchgelaufen und die Tiefensuche rekursiv auf alle seine Nachbarn aufgerufen.

Zur Verwaltung der markierten Knoten benutzen wir eine Wörterbuchdatenstruktur mit Knoten als Schlüssel und Booleschewerte als Wert, wo alle bereits besuchten Knoten eintragen werden. In dieser Stelle ist es besonders wichtig, Kanten die den Label var tragen, nicht zu benutzen. Solche Kanten kommen nur bei letrec-Ausdrücke vor. Dement-sprechend wird die Traversierungsfunktion um eine weitere Hilfsfunktion erweitert, die gezielt var-Kanten vermeidet. Falls es in der Adjazenzliste der zuletzt besuchten Knoten ein Tupel (_, var) gibt, dann wird die alte Liste der besuchten Knoten unverändert zurückgegeben und die Traversierungsfunktion rekursiv auf den nächsten Nachbarknoten aufgerufen.

Die Traversierungsfunktion bekommt einen Startknoten, einen Graphen und eine Daten-struktur zur Speicherung der besuchten Knoten als Argumente und gibt eine Liste von besuchten Knoten als Ergebnis zurück. Diese enthält alle vom Startknoten erreichbaren Knoten ohne Benutzung der var-Kanten.

Anhand der ermittelten erreichbaren Knoten können alle (mittels der Schlüssel) Knoten des Graphen effizient geprüft werden, ob sie in der Liste der erreichbaren Knoten enthalten sind. Diejenigen Knoten, die nicht enthalten sind, werden gelöscht. Anschließend wird die Adjazenzliste jeden Knoten auf nicht erreichbaren Knoten überprüfen und diese

löschen, falls sie vorhanden sind. Damit löschen wir gleichzeitig die Kanten, die einen nicht erreichbaren Knoten als Senke haben.

Jetzt stehen wir möglicherweise vor einem weiteren Problem. Wie vorhin beschrieben und gezeigt wurde, könnte durch den ersten Schritt der Garbage-Collection, ein leerer letrec oder auch eine Kette verschachtelter leerer Letrecs entstehen.

Die folgende Grafik zeigt ein Beispiel aus den drei leeren geschachtelten Letrecs .

s∈lch

letrec letrec

letrec

body

in

in

Abbildung 15: Geschachtelter letrec

Dafür ist der zweite Schritt für die Garbage-Collection definiert worden. Die Lösung lautet, verschmelze (ersetze den leeren Letrec durch seinen nicht leeren Body und lösche den Datensatz für den Bodyknoten) jeden leeren Letrec mit seinem nicht leeren Body.

Dieser Schritt zeigt sich als gar nicht so trivial wie es klingt. Die Tatsache das (leere) Letrecs verschachtelt vorkommen können, macht die Ersetzungen schwieriger.

Ein leerer Letrec zeichnet sich dadurch aus, dass der Knoten für den Ausdruck den label Letrec trägt und die Liste seine Nachbarn nur einen Eintrag (Body Knoten) enthält.

Die Art und Weise wie wir die Graphen konstruieren, bietet uns eine gute Grundlage um dieses Problem effizient zu lösen. Bei der Graphkonstruktion erstellen wir immer zuerst den Knoten für den gesamten Ausdruck (Vaterknoten) und erst danach erstellen wir den Knoten für seine Unterausdrücke (Kindknoten). Im Fall eines letrec- Ausdrucks hat der Bodyknoten stets einen größeren Index (Nummer) als der letrec-Knoten (wir erinnern, dass Knoten als Int-Werte definiert sind). Mit dieser Erkenntnis gehen wir auf die Suche nach leeren Letrecs folgendermaßen vor.

Wir durchsuchen den Graphen (bottom up) von unten nach oben nach solchen Kno-ten. Treffen wir einen Knoten, der die oben genannten Eigenschaften aufweist, sprich der letrec als label hat und die Liste seiner Nachfolger aus einem einzigen Element besteht, dann kann es sein, dass es sich um eine Kette verschachtelter leerer Letrecs handelt. Hierbei sehen wir den letzten leeren Letrec in dieser Kette und schauen ob es sich um einen einfachen leeren Letrec handelt. In beiden Fällen verhalten wir uns gleich.

Wir durchsuchen den Graphen (bottom up) von unten nach oben nach solchen Kno-ten. Treffen wir einen Knoten, der die oben genannten Eigenschaften aufweist, sprich der letrec als label hat und die Liste seiner Nachfolger aus einem einzigen Element besteht, dann kann es sein, dass es sich um eine Kette verschachtelter leerer Letrecs handelt. Hierbei sehen wir den letzten leeren Letrec in dieser Kette und schauen ob es sich um einen einfachen leeren Letrec handelt. In beiden Fällen verhalten wir uns gleich.