Ubersetzerbau - fldit

Übersetzerbau
Algebraic Compiler Construction
Peter Padawitz, TU Dortmund, Germany
Webseite zur LV: fldit-www.cs.uni-dortmund.de/ueb.html
Thursday 24th March, 2016 00:33
1
Inhalt
Mit ∗ markierte Abschnitte bzw. Kapitel werden zur Zeit in der LV nicht behandelt.
Zur Navigation auf Titel, nicht auf Seitenzahlen klicken!
1 Einleitung
2 Algebraische Modellierung
- Mengen und Typen
- Signaturen
- 2.9 Konstruktive Signaturen
- 2.10 Destruktive Signaturen
- 2.11 Algebren
- 2.12 Terme und Coterme
- 2.13 Beispiele
- 2.14 Die Algebren Bool und Regword (BS)
- 2.15 Die Algebren Beh(X, Y ), Pow (X), Lang(X) und Bro(BS)
- 2.16 Die Erreichbarkeitsfunktion
- 2.17 Termfaltung und Zustandsentfaltung
- 2.21 * Compiler für reguläre Ausdrücke
- 2.22 * Minimale Automaten
7
14
14
30
31
34
38
44
49
56
57
61
62
74
77
2
- 2.25 * Baumautomaten
3 * Rechnen mit Algebren
- Unteralgebren
- Kongruenzen und Quotienten
- Automatenminimierung durch Quotientenbildung
- Transitionsmonoid und syntaktische Kongruenz
- 3.6 Termsubstitution
- Termäquivalenz und Normalformen
- 3.10 Normalformen regulärer Ausdrücke
- 3.11 Die Brzozowski-Gleichungen
- 3.12 Optimierter Brzozowski-Automat
- 3.13 Erkenner regulärer Sprachen sind endlich
4 Kontextfreie Grammatiken (CFGs)
- Nicht-linksrekursive CFGs
- Abstrakte Syntax
- Wort- und Ableitungsbaumalgebra
- Modelle der Ausdrücke von JavaLight
5 Parser und Compiler für CFGs
- 5.1 Funktoren und Monaden
- Compilermonaden
- Monadenbasierte Parser und Compiler
80
82
83
87
91
92
94
96
101
102
104
106
109
114
117
126
131
136
138
150
152
3
6 LL-Compiler
7 LR-Compiler
8 Haskell: Typen und Funktionen
9 Haskell: Listen
10 Haskell: Datentypen und Typklassen
11 Algebren in Haskell
- 11.1 Beispiele
- 11.2 Datentyp der JavaLight-Algebren
- 11.3 Die Termalgebra von JavaLight
- 11.4 Die Wortalgebra von JavaLight
- 11.5 Das Zustandsmodell von JavaLight
- 11.6 * Die Ableitungsbaumalgebra von JavaLight
12 Attributierte Übersetzung
- 12.1 * Binärdarstellung rationaler Zahlen
- 12.2 * Strings mit Hoch- und Tiefstellungen
- 12.3 * Übersetzung regulärer Ausdrücke in erkennende Automaten
- 12.4 * Darstellung von Termen als hierarchische Listen
- 12.5 Eine kellerbasierte Zielsprache für JavaLight
13 JavaLight+ = JavaLight + I/O + Deklarationen + Prozeduren
- 13.1 Assemblersprache mit I/O und Kelleradressierung
- 13.2 Grammatik und abstrakte Syntax von JavaLight+
157
177
205
215
226
240
241
246
247
248
250
253
256
257
259
261
267
270
276
276
283
4
14
15
16
17
18
19
- 13.3 JavaLight+-Algebra javaStackP
* Mehrpässige Compiler
Funktoren und Monaden in Haskell
Monadische Compiler
- 16.1 Compilerkombinatoren
- 16.2 Monadische Scanner
- 16.3 Monadische LL-Compiler
- 16.4 Generischer JavaLight-Compiler
- 16.5 Korrektheit des JavaLight-Compilers
- 16.6 Generischer XMLstore-Compiler
* Induktion, Coinduktion und rekursive Gleichungen
- Induktion
- Induktive Lösungen
- Coinduktion
- Coinduktive Lösungen
- 17.11 Cotermbasierte Erkenner regulärer Sprachen
* Iterative Gleichungen
- 18.2 Das iterative Gleichungssystem einer CFG
- 18.5 Von Erkennern regulärer Sprachen zu Erkennern kontextfreier Sprachen
* Interpretation in stetigen Algebren
- 19.1 Schleifensemantik
287
310
318
327
331
333
335
338
340
344
346
346
348
353
360
376
378
379
384
394
399
5
- 19.2 Semantik der Assemblersprache StackCom ∗
- Erweiterte Σ-Bäume
- Cosignaturen und ihre Algebren
Literatur
Index
401
408
415
6
1 Einleitung
Die Folien dienen dem Vor- (!) und Nacharbeiten der Vorlesung, können und sollen aber
deren regelmäßigen Besuch nicht ersetzen!
Interne Links (einschließlich der Seitenzahlen im Index) sind an ihrer dunkelroten Färbung,
externe Links (u.a. zu Wikipedia) an ihrer magenta-Färbung erkennbar. Dunkelrot gefärbt
ist auch das jeweils erste Vorkommen einer Notation. Fettgedruckt ist ein Begriff in der
Regel nur an der Stelle, an der er definiert wird.
Jede Kapitelüberschrift und jede Seitenzahl in der rechten unteren Ecke einer Folie ist mit
dem Inhaltsverzeichnis verlinkt. Namen von Haskell-Modulen wie Java.hs sind mit den jeweiligen Programmdateien verknüpft.
Ein Scanner (Programm zur lexikalischen Analyse) fasst Zeichen zu Symbolen zusammen, übersetzt also eine Zeichenfolge in eine Symbolfolge. Bezüglich der Grammatik,
die der nachfolgenden Syntaxanalyse zugrundeliegt, heißen die Symbole auch Terminale.
Man muss unterscheiden zwischen Symbolen, die Komponenten von Operatoren sind (if,
then, =, etc.), und Symbolen wie 5 oder True, die der Scanner einer Basismenge (Int,
Float, Bool, Identifier, etc.) zuordnet. In der Grammatik kommt ein solches Symbol nicht
vor, jedoch der Namen der Basismenge, der es angehört.
7
Ein Parser (Programm zur Syntaxanalyse) übersetzt eine Symbolfolge in einen Syntaxbaum, der den für ihre Übersetzung in eine Zielsprache notwendigen Teil ihrer Bedeutung (Semantik) wiedergibt.
Der Parser scheitert, wenn die Symbolfolge in diesem Sinne bedeutungslos ist. Er orientiert sich dabei an einer (kontextfreien) Grammatik, die korrekte von bedeutungslosen
Symbolfolgen trennt und damit die Quellsprache definiert.
Ein Compiler (Programm zur semantischen Analyse und Codeerzeugung) übersetzt
ein Programm einer Quellsprache in ein semantisch äquivalentes Programm einer Zielsprache.
Ein Interpreter
• wertet einen Ausdruck in Abhängigkeit von einer Belegung seiner Variablen aus bzw.
• führt eine Prozedur in Abhängigkeit von einer Belegung ihrer Parameter aus.
Letzteres ist ein Spezialfall von Ersterem. Deshalb werden wir einen Interpreter stets als die
Faltung von Termen (Ausdrücken, die aus Funktionssymbolen einer Signatur bestehen)
in einer Algebra (Interpretation der Signatur) modellieren.
8
Auch Scanner, Parser und Interpreter sind Compiler, insofern sie Objekte von einer Repräsentation in eine andere übersetzen. Die Zielrepräsentation eines Interpreters ist i.d.R.
eine Funktion, die Eingabedaten verarbeitet. Daher liegt es nahe, alle o.g. Programme nach
denjenigen Prinzipien zu entwerfen, die bei Compilern im engeren Sinne Anwendung finden.
Die Entwicklung dieser Prinzipien und ihrer formalen Grundlagen wurzelt in der mathematischen Logik und dort vor allem in der Theorie formaler Sprachen und der
Universellen Algebra, deren Anwendung im Compilerbau in dieser LV eine zentrale Rolle spielen wird. Die Aufgabe, Übersetzer zu schreiben, hat auch entscheidend die
Weiterentwicklung von Programmiersprachen, insbesondere der logischen und
funktionalen, vorangetrieben.
Insbesondere Konzepte der universellen Algebra und der funktionalen Programmierung im
Übersetzerbau erlauben es, bei Entwurf und Implementierung von Scannern, Parsern, Compilern und Interpretern ähnliche Methoden einzusetzen. So ist z.B. ein wie oben definierter
Interpreter gleichzeitig ein Compiler, der den Ausdruck oder die Prozedur in eine Funktion übersetzt, die eine Belegung auf einen Wert des Ausdrucks bzw. eine vom Aufruf der
Prozedur erzeugte Zustandsfolge abbildet.
9
Der algebraische Ansatz klärt nicht nur die Bezüge zwischen den oben genannten Programmen, sondern auch eine Reihe weiterer in der Compilerbauliteratur meist sehr speziell definierter und daher im Kern vager Begriffe wie attributierter Grammatiken und
mehrpässiger Compilation.
Mehr Beachtung als die nicht wirklich essentielle Trennung zwischen Scanner, Parser, Compiler und Interpreter verdienen die Ansätze, Compiler generisch (neu-informatisch: agil)
zu entwerfen, so dass sie mit verschiedenen Quellsprachen und - wichtiger noch - mehreren
Zielsprachen instanziiert werden können. Es handelt sich dann im Kern um einen Parser,
der keine Symbol-, sondern Zeichenfolgen verarbeitet und ohne den klassischen Umweg über
Syntaxbäume gleich die gewünschten Zielobjekte/programme aufbaut. Sein front end kann
mit verschiedenen Scannern verknüpft werden, die mehr oder weniger detaillerte Symbolund Basistypinformation erzeugen, während sein back end mit verschiedenen Interpretationen ein und derselben - üblicherweise als kontextfreie Grammatik eingegebenen - Signatur
instanziiert werden kann, die verschiedenen Zielsprachen entsprechen.
Die beiden folgenden Grafiken skizzieren die Struktur des algebraischen Ansatzes und seine
Auswirkung auf die Generizität der Programme. Auf die Details wird in den darauffolgenden Kapiteln eingegangen. Dabei wird die jeweilige Funktionalität der o.g. Programme
in mathematisch präzise Definitionen gefasst, auf denen Entwurfsregeln aufbauen, deren
Befolgung automatisch zu korrekten Compilern, Interpretern, etc. führen.
10
Erkenner
Programmeigenschaften
Σ(G)-Formeln
Bool
Σ(G)-Algebren
Quellsprache
L(G)
Verifizierer
Unparser
konkrete
Syntax
Grammatik G
abstrakte
Syntax
Signatur Σ(G)
Syntaxbäume
Termalgebra
TΣ(G)
Optimierer
allgemein:
Termfaltung
Parser
Quellsprache
L(G)
Unparser
Compiler
Ableitungsbäume
Abl(G)
optimierte
Syntaxbäume
Zielsprache
Von konkreter über abstrakte Syntax zu Algebren
11
Menge
Faltung
kontextfreie
Sprache
Parser
Termalgebra
Faltung
Menge
Faltung
Menge
Algebra
kontextfreie
Sprache
Parser
Termalgebra
generische
Faltung
Algebra
Algebra
Algebra
kontextfreie
Sprache
generischer
Compiler
Algebra
Algebra
Der Weg zum generischen Compiler
12
fact =1; while x > 1 {fact = fact*x; x = x-1;}
Eingabewort (Element der JavaLight-Algebra javaWord)
parse
fold
Seq
Assign
fact
Embed
Sum
Prod
EmbedI
Loop
NilS
NilP
1
EmbedC
Block
EmbedL
Seq
Atom
Sum
Prod
Var
>
Sum
NilS
NilP
x
Assign
Prod
EmbedI
fact
NilS
NilP
1
Sum
Prod
Var
fact
Embed
Assign
NilS
x
Prodsect
*
Var
x
NilP
Sum
Prod
Var
x
Syntaxbaum
(Element der JavaLight-Algebra
javaTerm)
NilP
Sumsect
-
Prod
EmbedI
NilS
NilP
1
fold
fold
Zustandstransitionsfunktion
(Element der JavaLight-Algebra javaState)
Assemblerprogramm
(Element der JavaLight-Algebra javaStack)
13
2 Algebraische Modellierung
Mengen und Typen
∅ bezeichnet die leere Menge, 1 die Menge {} und 2 die Menge {0, 1}. 0 und 1 stehen hier
in der Regel für die Wahrheitswerte false und true. Für alle n > 1, [n] =def {1, . . . , n}.
Wir setzen voraus, dass die Definitionen einer Potenzmenge, binären Relation bzw.
(partiellen oder totalen) Funktion sowie der Vereinigung, des Durchschnitts,
des Komplements, der Disjunktheit und der Mächtigkeit (Kardinalität) von
Mengen bekannt sind.
λ-Abstraktionen zur anonymen Definition und die (sequentielle) Komposition mehrerer Funktionen sollten ebenfalls bekannt sein.
Für jede Menge A bezeichnet |A| die Kardinalität von A, P(A) die Potenzmenge von A
und idA : A → A die durch idA(a) = a für alle a ∈ A definierte Identität auf A.
Für jede Teilmenge B von A bezeichnet incB : B → A die durch incB (b) = b für alle
b ∈ B definierte Inklusion.
Für alle Mengen A, B bezeichnen A → B und B A die Menge der totalen und A (→ B die
Menge der partiellen Funktionen von A nach B. def (f ) bezeichnet den Definitionsbereich
einer partiellen Funktion f : A (→ B.
14
Mehrsortige Mengen und Funktionen
Sei S eine Menge. Eine S-sortige oder S-indizierte Menge ist ein Tupel A = (As)s∈S
von Mengen. Manchmal schreiben wir A auch für die Vereinigung der Mengen As über alle
s ∈ S.
Seien A1, . . . , An S-sortige Mengen. Eine S-sortige Teilmenge von A1 × · · · × An ist
eine S-sorted Menge R = (Rs)s∈S mit Rs ⊆ A1,s × . . . × An,s für alle s ∈ S.
Seien A = (As)s∈S und B = (Bs)s∈S S-sortige Mengen. Eine S-sortige Funktion
f : A → B ist eine S-sortige Menge (fs)s∈S derart, dass für alle s ∈ S, fs eine Funktion
von As nach Bs ist.
Sei I eine Menge. Das kartesische Produkt (der Komponenten) einer I-sortigen Menge
A = (Ai)i∈I ist wie folgt definiert:
[
Xi∈I Ai = {f : I → Ai | ∀ i ∈ I : f (i) ∈ Ai}.
i∈I
Xi∈I Ai aus der einzigen Funktion von ∅ nach i∈I Ai.
Gibt es eine Menge A mit Ai = A für alle i ∈ I, dann stimmt Xi∈I Ai offenbar mit AI
Im Fall I = ∅ besteht
S
überein.
Im Fall I = [n] für ein n > 1 schreibt man auch A1 × · · · × An anstelle von
An anstelle von AI .
Xi∈I Ai und
15
Sei I eine Menge. Die disjunkte Vereinigung (der Komponenten) einer I-sortigen Menge A = (Ai)i∈I ist wie folgt definiert:
]
[
Ai =
(Ai × {i}).
i∈I
i∈I
Die disjunkte Vereinigung des leeren Mengentupels wird definiert als die leere Menge.
U
Gibt es eine Menge A mit Ai = A für alle i ∈ I, dann stimmt i∈I Ai offenbar mit A × I
überein.
U
Im Fall I = [n] für ein n > 1 schreibt man auch A1 + · · · + An anstelle von i∈I Ai.
Der Plusoperator + und der Sternoperator ∗ bilden jede Menge A, die das leere
Wort nicht enthält, auf die Menge der (nichtleeren) Wörter oder Listen über A ab:
S
n
A+ =def
n>0 A ,
1 =def {},
A∗ =def 1 ∪ A+.
Die Folge der Komponenten eines Elements von A+ wird manchmal (z.B. in Haskell) nicht
wie oben mit runden, sondern mit eckigen Klammern eingeschlossen oder (wie in der Theorie
formaler Sprachen) zusammen mit den Kommas ganz weggelassen.
Teilmengen von A∗ werden auch Sprachen über A genannt.
16
Die Konkatenationen · : A∗ × A∗ → A∗ und · : P(A∗) × P(A∗) → P(A∗) sind wie folgt
induktiv definiert: Für alle s, v = (a1, . . . , am), w = (b1, . . . , bn) ∈ A∗ und B, C ⊆ A∗,
· w = w,
v · = v,
v · w = (a1, . . . , am, b1, . . . , bn),
B · C = {v · w | v ∈ B, w ∈ C}.
Für alle w ∈ A+ bezeichnet head(w) das erste Element von w und tail(w) die Restliste,
d.h. es gilt w = head(w) · tail(w).
|w| bezeichnet die Länge eines Wortes w, d.h. die Anzahl seiner Elemente.
17
L ⊆ A∗ heißt präfixabgeschlossen, wenn für alle w ∈ A∗ und a ∈ A aus wa ∈ L
w ∈ L folgt.
Eine partielle Funktion f : A∗ (→ B mit präfixabgeschlossenem Definitionsbereich heißt
deterministischer Baum.
f ist wohlfundiert (a: e), wenn es n ∈ N gibt mit |w| ≤ n für alle w ∈ def (t). Anschaulich
gesprochen ist f wohlfundiert, wenn f endliche Tiefe hat, wenn also alle von der Wurzel
ausgehenden Pfade endlich sind.
Tatsächlich kann man sich f als (möglicherweise unendlichen) Baum tf mit Kantenmarkierungen aus A und Knotenmarkierungen aus B vorstellen, der keine zwei Kanten mit
derselben Quelle, aber verschiedener Markierung besitzt. f () ∈ B ist die Markierung der
Wurzel von tf und jeder von der Wurzel ausgehende Pfad p endet in einem Knoten, der mit
f (w) markiert ist, wobei w ∈ A+ die Folge der Markierungen der Kanten von p ist.
Demnach kann ein deterministischer Baum als eine Art Record notiert werden:
tf = f (){x → tλw.f (xw) | x ∈ def (t) ∩ A}.
Die Menge aller deterministischer Bäume von A∗ nach B wird mit dtr(A, B) bezeichnet,
die Menge der wohlfundierten darunter mit wdtr(A, B).
18
Für alle Mengen A, B, Funktionen f : A → B, C ⊆ A, D ⊆ B und n > 0,
∆nA =def {(a1, . . . , an) ∈ An | ∀ 1 ≤ i < n : ai = ai+1} Diagonale von A
f (C) =def {f (a) | a ∈ C}
Bild von C unter f
= P(f )(C) (siehe Mengenfunktor)
img(f ) =def f (A)
f −1(D) =def {a ∈ A | f (a) ∈ D}
Urbild von D unter f
ker(f ) =def {(a, a0) ∈ A × A | f (a) = f (a0)}
Kern von f
graph(f ) =def {(a, f (a)) ∈ A × B | a ∈ A}
f |C : C
→ B
c 7→ f (c)
f [b/a] : A → B
Graph von f
Einschränkung von f
auf C
Update von f
c 7→ if c = a then b else f (c)
χ : P(A) → χ(C)
C
7→ λa.if a ∈ C then 1 else 0
charakteristische
Funktion von C
19
f ist surjektiv ⇔def img(f ) = B
f ist injektiv ⇔def ker(f ) = ∆2A
f ist bijektiv ⇔def ∃ g : B → A : g ◦ f = idA ∧ f ◦ g = idB
A und B heißen isomorph, geschrieben: A ∼
= B, wenn es eine bijektive Funktion von A
nach B gibt.
Aufgabe Zeigen Sie: P(A) ∼
= 2A .
Aufgabe Zeigen Sie: P(A × B) ∼
= P(B)A.
o
Aufgabe Zeigen Sie: f : A → B ist bijektiv ⇔ f ist injektiv und surjektiv.
o
o
Aufgabe Zeigen Sie: Ist f : A → B bijektiv, dann gibt es genau eine (üblicherweise mit
f −1 bezeichnete) Funktion g : B → A mit g ◦ f = idA und f ◦ g = idB .
o
20
Produkte und Summen als universelle Konstruktionen
Sei I eine nichtleere Menge und A eine I-sortige Menge.
Das kartesische Produkt und die disjunkte Vereinigung eines Mengentupels A haben bestimmte universelle Eigenschaften, d.h. erstens, dass alle Mengen, die diese Eigenschaften haben, isomorph sind und zweitens, dass jede Menge, die zu einer anderen Menge,
die diese Eigenschaften hat, isomorph ist, selbst diese Eigenschaften hat.
Jede Menge, die die universellen Eigenschaften des kartesischen Produkts bzw. der disjunkten Vereinigung von A hat, nennt man Produkt bzw. Summe von A:
Sei A = (Ai)i∈I ein Mengentupel, P eine Menge und π = (πi : P → Ai)i∈I ein Funktionstupel.
Das Paar (P, π) heißt Produkt von A, wenn es für alle Tupel f = (fi : B → Ai)i∈I
genau eine Funktion g : B → P gibt derart, dass für alle i ∈ I Folgendes gilt:
πi ◦ g = f i
(1)
πi heißt i-te Projektion von P und g Produktextension oder Range-Tuplung
von f . g wird mit hfiii∈I und im Fall I = [n], n > 1, auch mit hf1, . . . , fni bezeichnet.
Demnach ist im Fall I = ∅ eine Menge P genau dann ein Produkt, wenn es zu jeder Menge
B genau eine Funktion von B nach P gibt, wenn also P einelementig ist.
21
Wegen der Eindeutigkeit von Produktextensionen sind zwei Funktionen f, g : B → P genau
dann gleich, wenn ihre Produktkomponenten paarweise miteinander übereinstimmen:
(∀ i ∈ I : πi ◦ f = πi ◦ g)
⇒
f = g.
(2)
Mit f = λx.a : 1 → P und g = λx.b : 1 → P folgt aus (2), dass zwei Elemente
a, b ∈ P genau dann gleich sind, wenn für alle i ∈ I πi(a) = πi(b) gilt.
Beweis.
(2)
∀ i ∈ I : πi(a) = πi(b) ⇒ ∀ i ∈ I : πi ◦ f = πi ◦ g ⇒ f = g ⇒ a = f () = g() = b. o
Aufgabe 2.1
Sei P = Xi∈I Ai (s.o.) und πi(f ) =def f (i) für alle i ∈ I und f ∈ P . Zeigen Sie, dass
(P, (πi)i∈I ) ein Produkt von A ist, wenn die Extension von (fi : B → Ai)i∈I wie folgt
definiert wird:
• Für alle b ∈ B und i ∈ I, hfiii∈I (b)(i) =def fi(b).
o
Satz 2.2 (Produkte sind bis auf Isomorphie eindeutig)
(i) Seien (P, π) und (P 0, π 0) Produkte von A. Dann sind P und P 0 isomorph.
(ii) Sei (P, π) ein Produkt von A, P 0 eine Menge und h : P 0 → P eine bijektive Abbildung.
22
Dann ist (P 0, π 0) mit π 0 = (πi ◦ h)i∈I ebenfalls ein Produkt von A.
Beweis von (i). Um die Extensionen auf P ersten von denen auf P 0 zu unterscheiden, wird
die schließende Klammer letzterer gestrichelt.
Sei h =def hπii0i∈I : P → P 0 und h0 =def hπi0 ii∈I : P 0 → P . Dann gilt
(1)
(1)
(1)
(1)
πi ◦ h0 ◦ h = πi ◦ hπi0 ii∈I ◦ hπii0i∈I = πi0 ◦ hπii0i∈I = πi,
πi0 ◦ h ◦ h0 = πi0 ◦ hπii0i∈I ◦ hπi0 ii∈I = πi ◦ hπi0 ii∈I = πi0
für alle i ∈ I, also h0 ◦ h = idP und h ◦ h0 = idP 0 wegen (2).
Beweis von (ii). Sei f = (fi : B → Ai)i∈I und g =def h−1 ◦ hfiii∈I : B → P 0. g erfüllt
(1) mit π 0 anstelle von π: Für alle i ∈ I,
(1)
πi0 ◦ g = πi ◦ h ◦ g = πi ◦ h ◦ h−1 ◦ hfiii∈I = πi ◦ hfiii∈I = fi.
g ist eindeutig: Sei g 0 : B → P 0 eine weitere Funktion, die (1) erfüllt, für die also πi0 ◦ g 0 = fi
für alle i ∈ I gilt. Daraus folgt
πi ◦ h ◦ g = πi0 ◦ g = fi = πi0 ◦ g 0 = πi ◦ h ◦ g 0,
also h ◦ g = h ◦ g 0 wegen (2) und damit
g = h−1 ◦ h ◦ g = h−1 ◦ h ◦ g 0 = g 0.
o
23
Sei (P, π) ein Produkt von (Ai)i∈I , (P 0, π 0) ein Produkt von (Bi)i∈I und
f = (fi : Ai → Bi)i∈I .
Die Funktion
Y
fi =def hfi ◦ πii : P → P 0
i∈I
heißt Produkt von f .
Daraus folgt
πk ◦ (
Y
f i ) = f k ◦ πk
i∈I
für alle k ∈ I.
Sei I eine nichtleere Menge und n > 0.
f I =def
Q
i∈I
[n]
f,
f1 × · · · × fn =def f .
Aufgabe 2.3 Zeigen Sie, dass im Fall P = Xi∈I Ai und P 0 = Xi∈I Bi (siehe Aufgabe 2.1) für
Q
alle f ∈ P ( i∈I fi)(f ) mit λi.fi ◦ f übereinstimmt.
o
24
Vom Produkt kommt man zur Summe, indem man alle Funktionspfeile umdreht:
Sei A = (Ai)i∈I ein Mengentupel, S eine Menge und ι = (ιi : Ai → S)i∈I ein Funktionstupel.
Das Paar (S, ι) heißt Summe oder Coprodukt von A, wenn es für alle Tupel f = (fi :
Ai → B)i∈I genau eine Funktion g : S → B gibt mit
g ◦ ιi = fi
(3)
für alle i ∈ I.
ιi heißt i-te Injektion von S und g Summenextension oder Domain-Tuplung
von f . g wird mit [fi]i∈I und im Fall I = [n], n > 1, auch mit [f1, . . . , fn] bezeichnet.
Demnach ist im Fall I = ∅ eine Menge S genau dann eine Summe, wenn es zu jeder Menge
B genau eine Funktion von S nach B gibt, wenn also S die leere Menge ist.
Wegen der Eindeutigkeit von Summenextensionen sind zwei Funktionen f, g : S → B genau
dann gleich, wenn ihre Summenkomponenten paarweise miteinander übereinstimmen:
(∀ i ∈ I : f ◦ ιi = g ◦ ιi)
⇒
f = g.
(4)
Aus (4) folgt, dass für alle a ∈ S genau ein Paar (b, i) mit ιi(b) = a existiert.
25
Beweis. Gäbe es a ∈ S \
S
i∈I ιi (Ai ),
dann würden f = λx.0 : S → 2 und
g = (λx.if x ∈ S \ {a} then 0 else 1) : S → 2
(4) verletzen, weil für alle i ∈ I und b ∈ Ai Folgendes gilt:
(f ◦ ιi)(b) = f (ιi(b)) = 0 = g(ιi(b)) = (g ◦ ιi)(b).
Für alle i ∈ I und a ∈ Ai sei fi(a) = (a, i). Dann gilt für alle i, j ∈ I, a ∈ Ai und b ∈ Aj
mit ιi(a) = ιj (b):
(3)
(a, i) = fi(a) = ([fi]i∈I ◦ ιi)(a) = [fi]i∈I (ιi(a)) = [fi]i∈I (ιj (b)) = ([fi]i∈I ◦ ιj )(b)
(3)
= fj (b) = (b, j).
o
Aufgabe 2.4
U
Sei S = i∈I Ai (s.o.) und ιi(a) =def (a, i) für alle i ∈ I und a ∈ Ai. Zeigen Sie, dass
(S, (ιi)i∈I ) eine Summe von A ist, wenn die Extension von (fi : Ai → B)i∈I wie folgt
definiert wird: Für alle i ∈ I und a ∈ Ai, [fi]i∈I (a) =def fi(a).
o
Aufgabe 2.5
S
Sei S = i∈I Ai und ιi(a) =def a für alle i ∈ I und a ∈ Ai. Zeigen Sie, dass (S, (ιi)i∈I )
eine Summe von A ist, falls A aus paarweise disjunkten Mengen besteht.
o
26
Aufgabe 2.6
Sei A eine Menge, S = A∗ (s.o.) und ιn(a) =def a für alle n ∈ N und a = (a1, . . . , an) ∈ An.
Zeigen Sie, dass (S, (ιn)n∈N) eine Summe von (An)n∈N ist.
o
Satz 2.7 (Summen sind bis auf Isomorphie eindeutig)
(i) Seien (S, ι) und (S 0, ι0) Summen von A. Dann sind P und P 0 isomorph.
(ii) Sei (S, ι) eine Summe von A, S 0 eine Menge und h : S → S 0 eine bijektive Abbildung.
Dann ist (S 0, ι0) mit ι0 = (h ◦ ιi)i∈I ebenfalls eine Summe von A.
Beweis. Analog zu Satz 2.2. Es müssen nur alle Funktionspfeile umgedreht werden.
o
Sei (S, ι) eine Summe von (Ai)i∈I , (S 0, ι0) eine Summe von (Bi)i∈I und
f = (fi : Ai → Bi)i∈I .
Die Funktion
a
fi =def [ι0i ◦ fi] : S → S 0
i∈I
heißt Summe von f .
27
Daraus folgt
(
a
fi) ◦ ιk = ιk ◦ fk
i∈I
für alle k ∈ I.
Sei n > 0.
f + =def
f ∗ =def
f1 + · · · + fn =def
[n]
n∈N f ,
id1 + f +,
`
`
i∈[n] fi .
U
U
Aufgabe 2.8 Zeigen Sie, dass im Fall S = i∈I Ai und S 0 = i∈I Bi (siehe Aufgabe 2.4)
`
für alle i ∈ I und a ∈ Ai ( i∈I fi)(a, i) mit (fi(a), i) übereinstimmt.
o
28
Typen
Sei S eine endliche Menge von – Sorten genannten – Symbolen und I eine Menge nichtleerer Indexmengen, die implizit die Mengen 1, 2 und [n], n > 1, enthält.
Die Menge T (S, I) der I-polynomialen Typen über S ist die kleinste Menge von
Ausdrücken mit folgenden Eigenschaften:
• S ∪ I ⊆ T (S, I).
`
Q
• Für alle I ∈ I und {ei}i∈I ⊆ T (S, I), i∈I ei, i∈I ei ∈ T (S, I).
Q
`
Ein Typ der Form i∈I ei bzw. i∈I ei heißt Produkttyp bzw. Summentyp.
Für alle I ∈ I, n > 1 und e, e1, . . . , en ∈ T (S, I) verwenden wir folgende Kurzschreibweisen:
Q
e1 × · · · × en =def
i∈[n] ei ,
`
e1 + · · · + en =def
i∈[n] ei ,
Q
eI =def
i∈I e,
en =def e[n],
e+ =def e +
`
n>1 e
+
n
,
e∗ =def 1 + e .
29
Signaturen
Eine Signatur Σ = (S, I, F ) besteht aus Mengen S und I wie oben sowie einer endlichen
Menge F typisierter Funktionssymbole f : e → e0 mit e, e0 ∈ T (S, I), die üblicherweise
Operationen genannt werden.
dom(f ) = e heißt Domain, ran(f ) = e0 Range von f .
f : e → e0 ∈ F heißt Konstruktor bzw. Destruktor, wenn e bzw. e0 zu S gehört.
Σ heißt konstruktive bzw. destruktive Signatur, wenn F aus Konstruktoren bzw.
Destruktoren besteht und für alle s ∈ S die Menge aller f ∈ F mit ran(f ) = s bzw.
dom(f ) = s implizit zu I gehört.
Die komponentenweise Vereinigung zweier Signaturen Σ und Σ0 wird mit Σ ∪ Σ0 bezeichnet.
Konstruktoren dienen der Synthese von Elementen einer S-sortigen Menge, Destruktoren
liefern Werkzeuge zu ihrer Analyse.
Die abstrakte Syntax einer kontextfreien Grammatik (siehe Kapitel 4) ist eine konstruktive
Signatur, während Parser, Interpreter und Compiler auf Automatenmodellen beruhen, die
destruktive Signaturen interpretieren.
30
Die syntaktische Beschreibung jedes mathematischen Modells, das auf induktiv definierten
Mengen basiert, kann als konstruktive Signatur formuliert werden.
Solchen Modellen stehen die zustandsorientierten gegenüber, die Objekte nicht anhand
ihres Aufbaus, sondern ausschließlich durch Beobachtung ihres Verhaltens voneinander unterscheiden. Dementsprechend werden aus den Operationssymbolen der jeweils zugrundeliegenden destruktiven Signatur keine Objekte, sondern Beobachtungsinstrumente zusammengesetzt.
In den folgenden Beispielen steht hinter 1 die Trägermenge der initialen bzw. finalen Algebra der jeweiligen Signatur (siehe 2.11 und 2.17). Unter I listen wir nur die nicht implizit
in I enthaltenen (1, 2 und [n]; s.o.) auf.
Seien X, Y, X1, . . . , Xn, Y1, . . . , Yn, E1, . . . , En beliebige Mengen und BS eine endliche
Menge von Basismengen, die ∅ und 1 enthält.
2.9 Konstruktive Signaturen
• Mon 1 Unmarkierte binäre Bäume (Monoide sind Mon-Algebren; siehe 2.11.)
S = {mon},
I = ∅,
F = { one : 1 → mon,
mul : mon × mon → mon }.
31
• Nat 1 N
S = {nat},
I = ∅,
F = { zero : 1 → nat,
succ : nat → nat }.
• Lists(X, Y ) 1 X ∗ × I
S = {list},
I = {X, Y },
F = { nil : Y → list,
cons : X × list → list }.
• List(X) =def Lists(X, 1) 1 X ∗, alternativ:
S = {list},
I = {X, N>1},
F = {[. . . ] : X ∗ → list}.
• Bintree(X) 1 binäre Bäume endlicher Tiefe mit Knotenmarkierungen aus X
S = {btree},
I = {X},
F = { empty : 1 → btree,
bjoin : btree × X × btree → btree }.
32
• Tree(X) 1 Bäume endlicher Tiefe und endlichen Knotenausgrads mit Knotenmarkierungen aus X
S = {tree, trees},
I = {X}, F = { join : X × trees → tree,
nil : 1 → trees,
cons : tree × trees → trees }.
• Reg(BS) 1 reguläre Ausdrücke über BS
S = { reg }, I = {BS},
F = { par : reg × reg → reg,
seq : reg × reg → reg,
iter : reg → reg,
base : BS → reg }.
(parallele Komposition)
(sequentielle Komposition)
(Iteration)
(Einbettung der Basismengen)
• CCS(Act) 1 Calculus of Communicating Systems (kommunizierende Prozesse)
S = { proc }, I = {Act},
F = { pre : Act → proc,
cho : proc × proc → proc,
par : proc × proc → proc,
res : proc × Act → proc,
rel : proc × ActAct → proc }.
(prefixing by an action)
(choice)
(parallelism)
(restriction)
(relabelling)
33
2.10 Destruktive Signaturen
∗
• DAut(X, Y ) 1 Y X = Verhalten deterministischer Moore-Automaten mit Eingabemenge X und Ausgabemenge Y
S = { state },
(Zustandsmenge)
I = { X, Y },
(Eingabemenge X und Ausgabemenge Y )
F = { δ : state → stateX ,
(Transitionsfunktion)
β : state → Y }.
(Ausgabefunktion)
∗
• Acc(X) =def DAut(X, 2) 1 P(X) ∼
= 2X = Verhalten deterministischer Akzeptoren
von Sprachen über X
• Stream(X ) =def DAut(1, X) 1 X N
S = {stream},
I = {X},
F = { head : stream → X,
tail : stream → stream },
alternativ:
S = {stream},
I = {X, N},
F = {get : stream → X N}.
• coList(X) 1 X ∗ ∪ X N
S = {list},
I = {X},
F = {split : list → 1 + X × list}.
34
• Infbintree(X) 1 binäre Bäume unendlicher Tiefe mit Knotenmarkierungen aus X
S = {btree},
I = {X},
F = { root : btree → X,
left, right : btree → btree }.
• coBintree(X) 1 binäre Bäume beliebiger Tiefe mit Knotenmarkierungen aus X
S = {btree},
I = {X},
F = {split : btree → 1 + btree × X × btree}.
• Inftree(X) 1 endlich verzweigende Bäume unendlicher Tiefe mit Knotenmarkierungen
aus X
S = {tree},
I = {X, N>1},
F = { root : tree → X,
subtrees : tree → tree+ }.
• FBtree(X) 1 endlich verzweigende Bäume beliebiger Tiefe mit Knotenmarkierungen
aus X
S = {tree},
I = {X, N>1},
F = { root : tree → X,
subtrees : tree → tree∗ }.
35
• coTree(X) 1 beliebig verzweigende Bäume beliebiger Tiefe mit Knotenmarkierungen
aus X
S = { tree }, I = {X},
F = { root : tree → X,
subtrees : tree → trees,
split : trees → 1 + tree × trees }.
∗
• PAut(X, Y ) 1 (1 + Y )X = Verhalten partieller Moore-Automaten
S = {state},
I = {X, Y },
F = { δ : state → (1 + state)X ,
β : state → Y }.
∗
• NAut(X, Y ) 1 (Y ∗)X = Verhalten nichtdeterministischer (bildendlicher) Moore-Automaten
S = { state }, I = {X, Y, N>1},
F = { δ : state → (state∗)X ,
β : state → Y }.
• SAut(X) 1 ([0, 1] × Y )∗ = Verhalten stochastischer Automaten ohne Eingabe
S = { state }, I = {Y, [0, 1], N>1},
F = { δ : state → ([0, 1] × state)∗,
β : state → Y }.
36
• Proctree(Act) 1 Prozessbäume, deren Kanten mit Aktionen markiert sind
S = { state }, I = {Act, N>1},
F = { δ : tree → (Act × tree)∗ }.
+
• Mealy(X, Y ) 1 Y X = Verhalten von Mealy-Automaten
S = {state},
I = {X, Y },
F = { δ : state → stateX ,
β : state → Y X }.
+
Y X ist isomorph zur Menge der kausalen Stromfunktionen f : X ∗ → Y ∗.
f heißt kausal, wenn für alle s, s0 ∈ AN and n ∈ N Folgendes gilt:
take(n)(s) = take(n)(s0)
⇒
f (s)(n) = f (s0)(n).
• Class(I) 1 Verhalten von n Methoden einer Objektklasse [17]
S = { state },
I = { X1, . . . , Xn, Y1, . . . , Yn, E1, . . . , En },
F = { mi : state → ((state × Yi) + Ei)Xi | 1 ≤ i ≤ n }.
37
Algebren
Seien S und I wie oben.
Eine T (S, I)-sortige Menge A heißt typverträglich, wenn für alle I ∈ I, AI mit I
übereinstimmt und es für alle {ei}i∈I ⊆ T (S, I) Funktionstupel
π = (πi : AQi∈I ei → Aei )i∈I und ι = (ιi : Aei → A`i∈I ei )i∈I
gibt derart, dass (AQi∈I ei , π) ein Produkt und (A`i∈I ei , ι) eine Summe von (Aei )i∈I ist.
Für alle e ∈ T (S, I) und a ∈ Ae nennen wir e den Typ von a.
Das Typlifting einer S-sortigen Menge A ist Erweiterung von A zu einer typverträglichen
T (S, I)-sortigen Menge, die entsteht, wenn man Indexmengen, Produkte und Summen wie
folgt interpretiert: Für alle I ∈ I und {ei}i∈I ⊆ T (S, I),
AI = I,
AQi∈I ei = Xi∈I Aei ,
U
A`i∈I ei = i∈I Aei .
Aufgabe
Sei A typverträglich. Zeigen Sie, dass für alle I ∈ I, n > 1 und e, e1, . . . , en ∈ T (S, I)
Folgendes gilt:
38
Ae1×···×en
Ae1+···+en
AeI
Aen
Ae+
Ae∗
∼
=
∼
=
∼
=
∼
=
∼
=
∼
=
Ae1 × . . . × Aen ,
Ae1 + · · · + Aen ,
AIe ,
Ane,
A+
e,
A∗e .
o
Seien A, B typverträgliche T (S, I)-sortige Mengen.
Eine T (S, I)-sortige Funktion h : A → B heißt typverträglich, wenn für alle I ∈ I und
{ei}i∈I ⊆ T (S, I) Folgendes gilt:
• hI = idI ,
Q
`
• hQi∈I ei = i∈I hei und h`i∈I ei = i∈I hei .
(1)
(2)
Das Typlifting einer S-sortigen Funktion h : A → B ist die – ebenfalls mit h bezeichnete
– Erweiterung von h zu einer typverträglichen T (S, I)-sortigen Funktion.
Aufgabe
Sei h typverträglich. Zeigen Sie, dass für alle I ∈ I, n > 0 und e, e1, . . . , en ∈ T (S, I)
Folgendes gilt:
39
he1×···×en
he1+···+en
heI
hen
he+
he∗
=
=
=
=
=
=
he1 × . . . × hen ,
he1 + · · · + hen ,
hIe ,
[n]
he ,
h+
e,
h∗e .
o
Sei Σ = (S, I, F ) eine Signatur. Eine Σ-Algebra A = (A, Op) besteht aus einer typverträglichen T (S, I)-sortigen Menge A und einer F -sortigen Menge
Op = (f A : Ae → Ae0 )f :e→e0∈F
von Funktionen, den Operationen von A.
Für alle e ∈ T (S, I) heißt Ae Trägermenge (carrier set) oder Interpretation von e
in A.
Für alle f : e → e0 ∈ F heißt f A : Ae → Ae0 Interpretation von f in A. Im Fall
|Ae| = 1 schreibt man f A anstelle von f A(a).
Häufig wird auch die Vereinigung aller Trägermengen von A mit A bezeichnet!
AlgΣ bezeichnet die Klasse aller Σ-Algebren.
Algebren destruktiver Signaturen werden auch Coalgebren genannt.
40
Seien A, B Σ-Algebren. Das Typlifting einer S-sortigen Funktion h : A → B heißt ΣHomomorphismus, wenn für alle f : e → e0 ∈ F
he0 ◦ f A = f B ◦ he
gilt. Ist h bijektiv, dann heißt h Σ-Isomorphismus und A und B sind Σ-isomorph.
h induziert die Bildalgebra h(A):
• Für alle e ∈ T (S, I), h(A)e =def he(Ae).
• Für alle f : e → e0 ∈ F und a ∈ Ae, f h(A)(h(a)) =def f B (h(a)).
Algebra-Beispiele
Die Menge der natürlichen Zahlen ist Trägermenge der Nat-Algebra
NN = (A, Op = {zeroNN : 1 → N, succNN : N → N})
und auch der List(1)-Algebra
NL = (B, Op = {nilNL : 1 → N, consNL : 1 × N → N}),
die wie folgt definiert sind:
41
Anat = Blist = N und für alle n ∈ N,
zeroNN
succNN (n)
nilNL
consNL(n)
=
=
=
=
0,
n + 1,
0,
n + 1.
Die Menge der Wörter über einer Menge X ist Trägermenge der List(X)-Algebra
SL(X ) = (A, Op = {nilSL : 1 → X ∗, consSL : X × X ∗ → X ∗})
und auch der Mon-Algebra
SM (X ) = (B, Op = {oneSM (X ) : 1 → X ∗, mulSM (X ) : X ∗ × X ∗ → X ∗}),
die wie folgt definiert sind:
Alist = Bmon = X ∗ und für alle x ∈ X und v, w ∈ X ∗,
nilSL(X )
consSL(X )(x, w)
oneSM (X )
mulSM (X )(v, w)
=
=
=
=
,
xw,
,
v · w.
Die Menge X X der Funktionen von X nach X ist Trägermenge der Mon-Algebra
FM (X ) = (A, Op = {oneFM (X ) : 1 → X X , mulFM (X ) : X X × X X → X X }),
42
die wie folgt definiert ist:
Amon = X X und für alle f, g : X → X,
oneFM (X )
= idX ,
mulFM (X )(f, g) = g ◦ f.
Eine Mon-Algebra A heißt Monoid, wenn mulA assoziativ und oneA ein links- und rechtsneutrales Element bzgl. mulA ist. Demnach sind SM(X) und FM(X) Monoide.
Die folgende Stream(Z)-Algebra zo repräsentiert die Ströme 0, 1, 0, 1, . . . und 1, 0, 1, 0, . . . :
zostream
headzo(Blink )
tailzo(Blink )
headzo(Blink 0)
tailzo(Blink 0)
=
=
=
=
=
{Blink , Blink 0},
0,
Blink 0,
1,
Blink .
Die folgende Acc(Z)-Algebra eo dient der Erkennung der Parität der Summe von Zahlenfolgen (siehe Beispiel 2.23) und ist wie folgt definiert:
eostate
δ eo(Esum)
δ eo(Osum)
β eo
=
=
=
=
{Esum, Osum},
λx.if even(x) then Esum else Osum,
λx.if odd(x) then Esum else Osum,
λst.if st = esum then 1 else 0.
o
43
2.12 Terme und Coterme
Sei Σ = (S, I, F ) eine Signatur, V eine S-sortige Menge,
[
[
XΣ =def
I ∪ {sel} und YΣ,V =def
I ∪ V ∪ {tup}.
Im Folgenden verwenden wir die im Abschnitt Mengen und Typen eingeführte Notation für
deterministische Bäume.
Sei Σ konstruktiv.
CTΣ(V ) bezeichnet die größte T (S, I)-sortige Menge M von Teilmengen von dtr(XΣ, YΣ,V )
mit folgenden Eigenschaften:
• Für alle I ∈ I, MI = I.
(1)
• Für alle s ∈ S und t ∈ Ms gehört t zu Vs
(2)
oder gibt es c : e → s ∈ F und t0 ∈ Me mit t = c{sel → t0}.
(3)
• Für alle I ∈ I, {ei}i∈I ⊆ T (S, I), t ∈ MQi∈I ei und i ∈ I gibt es ti ∈ Mei mit
t = tup{i → ti | i ∈ I}.
(4)
• Für alle I ∈ I, {ei}i∈I ⊆ T (S, I) und t ∈ M`i∈I ei gibt es i ∈ I und t0 ∈ Mei mit
t = i{sel → t0}.
(5)
44
(1)
(2/6)
(3/7)
(4)
(5)
Knoten und Kanten eines Σ-Terms.
Neben jedem Knoten n steht der Typ von Termen mit Wurzel n.
CTΣ(V ) ist typverträglich.
Beweis. Für all I ∈ I und {ei}i∈I ⊆ T (S, I) ist (CTΣ(V )Qi∈I , π) mit
πi({tup → ti | i ∈ I}) =def ti
und ti ∈ CTΣ(V )ei ein Produkt und (CTΣ(V )`i∈I , ι) mit ιi(t) =def i{sel → t} und
t ∈ CTΣ(V )ei eine Summe von (CTΣ(V )i)i∈I .
o
Die Elemente von CTΣ(V ) heißen Σ-Terme über V .
45
Die Elemente von CTΣ =def CTΣ(∅) heißen Σ-Grundterme.
Aufgabe Zeigen Sie, dass TΣ(V ) =def CTΣ(V ) ∩ wdtr(XΣ, YΣ,V ) die kleinste T (S, I)sortige Menge M von Teilmengen von dtr(XΣ, YΣ,V ) mit (1) und folgenden Eigenschaften
ist:
• Für alle s ∈ S, Vs ⊆ Ms.
(6)
• Für alle c : e → s ∈ F und t ∈ Me gehört c{sel → t} zu Ms.
(7)
• Für alle I ∈ I, {ei}i∈I ⊆ T (S, I) und ti ∈ Mei , i ∈ I, gehört tup{i → ti | i ∈ I} zu
(8)
MQi∈I ei .
• Für alle I ∈ I, {ei}i∈I ⊆ T (S, I), i ∈ I und t ∈ Mei gehört i{sel → t} zu M`i∈I ei .
(9)
TΣ(V ) und CTΣ(V ) sind die Trägermengen von Σ-Algebren (s.u.). Darüberhinaus sind
wohlfundierte Σ-Terme Bestandteile von Formeln wie z.B. den in Kapitel 18 iterativen
Gleichungen wie (1) in Abschnitt 2.13.
46
Sei Σ destruktiv.
DTΣ(V )bezeichnet die größte T (S, I)-sortige Menge M von Teilmengen von dtr(XΣ, YΣ,V )
mit (1), (4), (5) und folgender Eigenschaft:
• Für alle s ∈ S und t ∈ Ms gibt es x ∈ Vs und für alle d : s → e ∈ F gibt es td ∈ Me
mit t = x{d → td | d : s → e ∈ F }.
(10)
(1)
(10/11)
(4)
(5)
Knoten und Kanten eines Σ-Coterms.
Neben jedem Knoten n steht der Typ von Cotermen mit Wurzel n.
Die Elemente von DTΣ(V ) heißen Σ-Coterme über V .
47
Die Elemente von DTΣ =def DTΣ(1) heißen Σ-Grundcoterme.
Aufgabe Zeigen Sie, dass coTΣ(V ) =def DTΣ(V ) ∩ wdtr(XΣ, YΣ,V ) die kleinste T (S, I)sortige Menge M von Teilmengen von dtr(XΣ, YΣ,V ) mit (1), (8), (9) und folgender Eigenschaft ist:
• Für alle s ∈ S, x ∈ Vs, d : s → e ∈ F und td ∈ Me gehört x{d → td | d : s → e ∈ F }
zu Ms.
(11)
Demgegenüber bezeichnet TΣ(V ) die kleinste T (S, I)-sortige Menge M von Teilmengen
von dtr(XΣ, YΣ,V ) mit (1), (8), (9) und folgender Eigenschaft:
• Für alle s ∈ S, d : s → e ∈ F und td ∈ Me gehört {d → td | d : s → e ∈ F } zu
Ms.
(12)
Wie im Fall einer konstruktiven Signatur werden die Elemente von TΣ(V ) (wohlfundierte)
Σ-Terme über V genannt.
coTΣ(V ) und DTΣ(V ) sind die Trägermengen von Σ-Algebren (s.u.). Im Gegensatz zu
Σ-Termen kommen Σ-Coterme in Formeln nicht vor. Stattdessen werden auch im Fall einer destruktiven Signatur nur wohlfundierte Σ-Terme in Formeln verwendet wie z.B. den
Gleichungen (2) und (3) von Abschnitt 2.13.
48
Zur Vereinfachung der Wortdarstellung deterministischer Bäume verwenden wir die folgenden Abkürzungen: Sei c ∈ YΣ,V und n > 1.
c
steht für
c{sel → },
c(t)
steht für
c{sel → t},
c(t1, . . . , tn)
steht für
c{sel → tup{1 → t1, . . . , n → tn}},
c{. . .}
steht für
c{sel → tup{. . .}},
{. . .}
steht für
{. . .}.
2.13 Beispiele
Sei Σ = Nat (siehe 2.9).
CTΣ,nat ist die größte Teilmenge M von dtr(XΣ, YΣ,V ) mit folgender Eigenschaft:
• Für alle t ∈ M gilt t = zero oder gibt es u ∈ M mit t = succ(u).
TΣ,nat ist die kleinste Teilmenge M von dtr(XΣ, YΣ,V ) mit folgenden Eigenschaften:
• zero ∈ M .
• Für alle t ∈ M , succ(t) ∈ M .
49
Sei Σ = List(X) (siehe 2.9).
CTΣ,list ist die größte Teilmenge M von dtr(XΣ, YΣ,V ) mit folgender Eigenschaft:
• Für alle t ∈ M gilt t = nil oder gibt es x ∈ X und u ∈ M mit t = cons(x, u).
TΣ,list ist die kleinste Teilmenge M von dtr(XΣ, YΣ,V ) mit folgenden Eigenschaften:
• nil ∈ M .
• Für alle x ∈ X und t ∈ M , cons(x, t) ∈ M .
Sei V = {blink, blink 0}. Die folgenden iterativen Gleichungen zwischen List(Z)-Termen
über V haben eine eindeutige Lösung in CTList(Z) (siehe Kapitel 18):
blink = cons(0, blink 0), blink 0 = cons(1, blink).
(1)
Deshalb definiert (1) blink und blink 0 als zwei Elemente von CTList(Z). Als eindeutige
Lösungen iterativer Gleichungen repräsentierbare unendliche Terme heißen rational.
Sei Σ = Reg(BS) (siehe 2.9).
TΣ,reg ist die kleinste Teilmenge M von dtr(XΣ, YΣ,V ) mit folgenden Eigenschaften:
• Für alle t, u ∈ M , par(t, u), seq(t, u) ∈ M .
• Für alle t ∈ M , iter(t) ∈ M .
50
• Für alle B ∈ BS, base(B) ∈ M .
Sei Σ = Stream(X) (siehe 2.10). DTΣ,stream ist die größte Teilmenge M von dtr(XΣ, YΣ,V )
mit folgender Eigenschaft:
• Für alle t ∈ M gibt es x ∈ X und t0 ∈ M mit t = {head → x, tail → t0} ∈ M .
ε
head
tail
0
ε
tail
head
1
ε
head
2
tail
ε
Stream(N)-Coterm, der den Strom aller natürlichen Zahlen darstellt
Sei V = {blink, blink 0}. Da zo eine Stream(Z)-Algebra (siehe 2.11) und DTStream(Z) eine
finale Stream(Z)-Algebra ist (s.u.), lösen die Coterme unfold zo(Blink ) und unfold zo(Blink 0)
die folgenden Gleichungen zwischen Stream(Z)-Cotermen über V eindeutig in blink bzw.
blink 0:
51
blink = {head → 0, tail → blink 0}, blink 0 = {head → 1, tail → blink}
(2)
(siehe Kapitel 18). Deshalb definiert (2) blink und blink 0 als zwei Elemente von DTStream(Z).
Als eindeutige Lösungen iterativer Gleichungen repräsentierbare unendliche Coterme heißen
rational.
Sei Σ = Colist(X) (siehe 2.10).
DTΣ,list ist die größte Teilmenge M von dtr(XΣ, YΣ,V ) mit folgender Eigenschaft:
• Für alle t ∈ M gilt t = {split → 1} oder gibt es x ∈ X und u ∈ M mit
t = {split → 2{1 → x, 2 → u}}.
Sei Σ = DAut(X, Y ) (siehe 2.10).
DTΣ,state ist die größte Teilmenge M von dtr(XΣ, YΣ,V ) mit folgender Eigenschaft:
• Für alle t ∈ M und x ∈ X gibt es tx ∈ M und y ∈ Y mit
t = {δ → tup{x → tx | x ∈ X}, β → y}.
52
ε
β
0
δ
1
β
ε
x
y
ε
0
tup
x
z
ε
y
δ
ε
tup
β
z
δ
tup
ε
δ
ε
ε
x
1
β
tup
x
z
ε
ε
z
ε
y
ε
y
ε
Acc({x, y, z})-Coterm, der einen Akzeptor der Sprache {w ∈ {x, y, z}∗ | w enthält x oder z} darstellt
Sei V = {esum, osum}. Da eo eine Acc(Z)-Algebra (siehe 2.11) und DTAcc(Z) eine finale
Acc(Z)-Algebra ist (s.u.), lösen die Coterme unfold eo(Esum) und unfold eo(Osum) die
folgenden iterativen Gleichungen zwischen Acc(Z)-Cotermen über V eindeutig in esum
bzw. esum:
53
esum = {δ → tup({x → esum | x ∈ even} ∪ {x → osum | x ∈ odd}), β → 1},
osum = {δ → tup{x → osum | x ∈ even} ∪ {x → esum | x ∈ odd}), β → 0}.
even und odd bezeichnen die Mengen der geraden bzw. ungeraden ganzen Zahlen.
(3)
o
CTΣ(V ) und DTΣ(V ) sind Σ-Algebren
Sei Σ = (S, I, C) eine konstruktive Signatur.
CTΣ(V ) wird wie folgt zur Σ-Algebra erweitert:
• Für alle c : e → s ∈ C, t ∈ CTΣ(V )e, cCTΣ(V )(t) =def c{sel → t}.
• Für alle I ∈ I, ti ∈ CTΣ(V )ei , i ∈ I, and k ∈ I, πk (tup{i → ti | i ∈ I}) =def tk .
• Für alle I ∈ I, i ∈ I and t ∈ CTΣ(V )ei , ιi(t) =def i{sel → t}.
Sei Σ = (S, I, D) eine destruktive Signatur.
DTΣ(V ) wird wie folgt zur Σ-Algebra erweitert:
• Für alle s ∈ S, x ∈ Vs and td ∈ DTΣ(V )e, d : s → e ∈ D, and d0 : s → e0,
d0DTΣ(V )(x{d → td | d : s → e ∈ D}) =def td0 .
54
• Für alle I ∈ I, ti ∈ DTΣ(V )ei , i ∈ I, and k ∈ I, πk (tup{i → ti | i ∈ I}) =def tk .
• Für alle I ∈ I, i ∈ I and t ∈ DTΣ(V )ei , ιi(t) =def i{sel → t}.
Die Interpretation von Destruktoren in DTΣ(V ) machen Coterme zu eier Art analytischer
Funktionen: Zwei Coterme t, t0 ∈ DTΣ(V )s sind genau dann gleich, wenn die “Anfangswerte” t() und t0() und für alle d : s → e die “Ableitungen” dDTΣ(V )(t) und dDTΣ(V )(t0)
miteinander übereinstimmen.
Die atomaren Formeln der Prädikatenlogik sind aus Σ-Termen und Prädikaten (= Relationssymbolen) zusammengesetzt. Prinzipiell kommt man dort mit einer einzigen Sorte aus,
muss dann aber die Trennung zwischen mehreren Datenbereichen durch die Einführung
eines einstelligen Prädikats für jeden Datenbereich wiedergeben.
So entspricht z.B. die Sorte nat der Signatur Nat (siehe 2.9) das Prädikat isNat mit den
Axiomen
isNat(zero),
isNat(x) ⇒ isNat(succ(x)).
(1)
(2)
Im Rahmen einer Nat umfassenden Signatur Σ wäre ein Σ-Term t genau dann ein NatGrundterm des Typs nat, wenn isNat(t) mit Inferenzregeln der Prädikatenlogik aus (1)
und (2) ableitbar ist.
55
2.14 Die Algebren Bool und Regword (BS) (siehe 2.9)
Die Menge 2 = {0, 1} ist Trägermenge der Reg(BS)-Algebra Bool :
Für alle x, y ∈ 2 und B ∈ BS \ 1,
parBool (x, y)
seq Bool (x, y)
iterBool (x)
baseBool (1)
baseBool (B)
=
=
=
=
=
max{x, y},
x ∗ y,
1,
1,
0.
Die üblichen Wortdarstellungen regulärer Ausdrücke e wie z.B. aab∗ + c(aN)∗ bilden die
Reg(BS)-Algebra Regword (BS):
Sei
syms(BS) = {+,∗ , (, )} ∪ {B | B ∈ BS}.
Im Fall |B| = 1 setzen wir B mit dem einen Element von B gleich. Ob ein Teilausdruck
e geklammert werden muss oder nicht, hängt von der Priorität des Operationssymbols f
ab, zu dessen Domain e gehört. Die Trägermenge von Regword (BS) besteht deshalb aus
Funktionen, die, abhängig von der Priorität von f , die Wortdarstellung von e mit oder ohne
Klammern zurückgibt:
Regword (BS)reg = N → syms(BS)∗.
56
Aus den Prioritäten 0,1,2 von par, seq bzw. iter ergeben sich folgende Interpretation der
Operationssymbole von Reg(BS):
Für alle f, g : N → syms(BS)∗, B ∈ BS und w ∈ syms(BS)∗,
parRegword (BS)(f, g)
seq Regword (BS)(f, g)
iterRegword (BS)(f )
baseRegword (BS)(B)
enclose(True)(w)
enclose(False)(w)
=
=
=
=
=
=
λn.enclose(n > 0)(f (0) + g(0)),
λn.enclose(n > 1)(f (1) g(1)),
λn.enclose(n > 2)(f (2)∗),
λn.B,
(w),
w.
Ist die Funktion f : N → syms(BS)∗ das Ergebnis der Faltung eines Reg(BS)-Terms t in
Regword (BS) (s.u.), dann liefert f (0) eine Wortdarstellung von t, die keine überflüssigen
Klammern enthält.
Regword(String) ist im Haskell-Modul Compiler.hs durch regWord implementiert.
o
2.15 Die Algebren Beh(X, Y ), Pow (X), Lang(X) und Bro(BS) (siehe 2.10)
Seien X und Y Mengen.
57
Funktionen von X ∗ nach Y nennen wir Verhaltensfunktionen (behavior functions).
Sie bilden die Trägermenge folgender DAut(X, Y )-Algebra Beh(X, Y ):
∗
Beh(X, Y )state = Y X .
Für alle f : X ∗ → Y , x ∈ X und w ∈ X ∗,
δ Beh(X,Y )(f )(x)(w) = f (xw) und β Beh(X,Y )(f ) = f ().
Beh(X, 2) ist im Haskell-Modul Compiler.hs durch behFun implementiert.
∗
Eine zu Beh(X, 2)state = 2X bijektive Menge ist die Potenzmenge P(X ∗) (siehe Mengen
und Typen). Daraus ergibt sich die Acc(X)-Algebra Pow (X) mit
Pow (X)state = P(X ∗).
Für alle L ⊆ X ∗ und x ∈ X,
(
δ Pow (X)(L)(x) = {w ∈ X ∗ | xw ∈ L} und β Pow (X)(L) =
1 falls ∈ L,
0 sonst.
58
∗
Aufgabe Zeigen Sie, dass die Funktion χ : P(X ∗) → 2X , die jeder Teilmenge von X ∗ ihre
charakteristische Funktion zuordnet (siehe Mengen und Typen), ein Acc(X)-Homomorphismus von Pow (X) nach Beh(X, 2) ist.
o
Sei BS eine endliche Menge von Basismengen, die ∅ und 1 enthält, und X =
S
BS \ 1.
P(X ∗) ist nicht nur die Trägermenge der Acc(X)-Algebra Pow (X), sondern auch der
folgendermaßen definierten Reg(BS)-Algebra Lang(X) der Sprachen über X:
Lang(X)reg = P(X ∗).
Für alle B ∈ BS und L, L0 ⊆ X ∗,
parLang(X)(L, L0)
seq Lang(X)(L, L0)
iterLang(X)(L)
baseLang(X)(B)
=
=
=
=
L ∪ L0 ,
L · L0 ,
L∗,
B.
Anstelle von Lang(X) wird in Compiler.hs die Reg(BS)-Algebra regB mit der Trägermen∗
ge 2X implementiert. Sie macht χ zum Reg(BS)-Homomorphismus.
Aufgabe Zeigen Sie, dass χ ein Reg(BS)-Homomorphismus von Lang(X) nach regB ist. o
59
Die Menge der Reg(BS)-Grundterme ist nicht nur eine Trägermenge der Reg(BS)-Algebra
TReg(BS), sondern auch der wie folgt definierten Acc(X)-Algebra Bro(BS) (accT in Compiler.hs; siehe [2, 15]), die Brzozowski-Automat genannt wird:
Bro(BS)state = TReg(BS),reg .
Die Interpretationen von δ und β in Bro(BS) werden induktiv über dem Aufbau von
Reg(BS)-Grundtermen definiert:
Für alle t, u ∈ TReg(BS) und B ∈ BS \ 1,
δ Bro(BS)(par(t, u)) = λx.par(δ Bro(BS)(t)(x), δ Bro(BS)(u)(x)),
δ Bro(BS)(seq(t, u)) = λx.par(seq(δ Bro(BS)(t)(x), u),
if β Bro(BS)(t) = 1 then δ Bro(BS)(u)(x) else reg(∅)),
δ Bro(BS)(iter(t))
= λx.seq(δ Bro(BS)(t)(x), iter(t)),
δ Bro(BS)(base(1)) = base(∅),
δ Bro(BS)(base(B)) = λx.if x ∈ B then base(1) else base(∅),
β Bro(BS)(par(t, u)) = max{β Bro(BS)(t), β Bro(BS)(u)},
β Bro(BS)(seq(t, u)) = β Bro(BS)(t) ∗ β Bro(BS)(u),
β Bro(BS)(iter(t)) = 1,
β Bro(BS)(base(1)) = 1,
β Bro(BS)(base(B)) = 0.
60
2.16 Die Erreichbarkeitsfunktion
Sei A = (A, Op) eine DAut(X, Y )-Algebra und Z = Astate. Die Erreichbarkeitsfunktion
reachA : X ∗ → Z Z
von A ist wie folgt induktiv definiert: Für alle x ∈ X und w ∈ X ∗,
reachA() = idA,
reachA(xw) = reachA(w) ◦ λa.δ A(a)(x).
Der Definitionsbereich X ∗ von reachA ist Trägermenge der Mon-Algebra SM(X), der
Wertebereich Z Z ist Trägermenge der Mon-Algebra FM(Z) (siehe 2.11).
reachA ist Mon-homomorph:
reachA(oneSM (X )) = reachA() = idA = oneFM (Z ).
Für alle x ∈ X und v, w ∈ X ∗,
reachA(mulSM (X )(, w)) = reachA(w) = reachA(w) = reachA(w) ◦ idA
= reachA(w) ◦ reachA() = mulFM (Z )(reachA(), reachA(w)),
reachA(mulSM (X )(xv, w)) = reachA(xvw) = reachA(x(mulSM (X )(v, w)))
= reachA(mulSM (X )(v, w)) ◦ λa.δ A(a)(x)
ind. hyp.
=
(mulFM (Z )(reachA(v), reachA(w)) ◦ λa.δ A(a)(x)
61
= (reachA(w) ◦ reachA(v)) ◦ λa.δ A(a)(x) = reachA(w) ◦ (reachA(v) ◦ λa.δ A(a)(x))
= reachA(w) ◦ reachA(xv) = mulFM (Z )(reachA(xv), reachA(w)).
o
2.17 Termfaltung und Zustandsentfaltung
Sei Σ = (S, I, C) eine konstruktive Signatur, A = (A, Op) eine Σ-Algebra, V eine S-sortige
Menge von “Variablen” und g : V → A eine – Variablenbelegung (valuation) genannte
– S-sortige Funktion.
In Abhängigkeit von g wertet die wie folgt induktiv definierte T (S, I)-sortige Funktion
g ∗ : TΣ(V ) → A,
die Extension von g, wohlfundierte Σ-Terme über V in A aus:
• Für alle
• Für alle
• Für alle
• Für alle
∗
πk (gQ
i∈I
I ∈ I, gI∗ = idI .
s ∈ S und x ∈ Vs, gs∗(x) = gs(x).
c : e → s ∈ F und t ∈ TΣ(V )e, gs∗(c{sel → t}) = cA(ge∗(t)).
I ∈ I und {ei}i∈I ⊆ T (S, I), ti ∈ TΣ(V )ei , i ∈ I, und k ∈ I,
∗
ei ({tup → ti | i ∈ I})) = gek (tk ).
• Für alle I ∈ I, {ei}i∈I ⊆ T (S, I), k ∈ I und t ∈ TΣ(V )ek ,
∗
∗
g`
ei (k{sel → t}) = ιk (gek (t)).
i∈I
(1)
(2)
(3)
(4)
(5)
62
Satz 2.18 ([32], Theorem FREE)
g ∗ ist Σ-homomorph und der einzige Σ-Homomorphismus von TΣ(V ) nach A ist, der (2)
erfüllt.
Offenbar hängt die Einschränkung von g ∗ auf Grundterme nicht von g ab. Sie wird Termfaltung genannt und mit fold A bezeichnet.
Umgekehrt stimmt g ∗ mit der Termfaltung fold A(g) : TΣ0 → A(g) überein, wobei
Σ0 =def (S, I ∪ {Vs | s ∈ S}, C ∪ {vars : Vs → s | s ∈ S})
A(g)
und A(g) ∈ AlgΣ0 definiert ist durch A(g)|Σ = A und vars
= g für alle s ∈ S.
Aus Satz 2.18 folgt sofort:
fold A ist der einzige Σ-Homomorphismus von TΣ nach A.
(6)
Wegen (6) nennt man TΣ eine initiale Σ-Algebra.
Wie die Produkt- oder Summeneigenschaft ist auch Initialität eine universelle Eigenschaft
(vgl. Satz 2.2 bzw. 2.7):
Alle initialen Σ-Algebren sind Σ-isomorph und jede zu einer initialen Σ-Algebra isomorphe
Σ-Algebra ist initial. TΣ wird deshalb auch die initiale Σ-Algebra genannt.
63
Z.B. ist neben TNat auch NN eine initiale Nat-Algebra und neben TList(X) auch SL(X ) eine
initiale List(X)-Algebra (siehe 2.11).
Aufgabe Wie lauten die Isomorphismen von TNat nach NN bzw. von TList(X) nach SL? o
Die Faltung fold Lang(X)(t) eines Reg(BS)-Grundterms – also eines regulären Ausdrucks –
t in Lang(X) heißt Sprache von t (siehe 2.15).
Umgekehrt nennt man L ⊆ X ∗ eine reguläre Sprache, wenn L zum Bild von fold Lang(X)
gehört.
Die Haskell-Funktion foldReg von Compiler.hs implementiert die Faltung
fold A : TReg(BS) → A
in einer beliebigen Reg(BS)-Algebra A.
Aufgabe Zeigen Sie fold Bool = β Bro(BS).
o
Aufgabe Zeigen Sie durch Induktion über den Aufbau von Reg(BS)-Grundtermen, dass
für alle t ∈ TReg(BS) die folgende Äquivalenz gilt:
∈ fold Lang(X)(t)
⇔
fold Bool (t) = 1.
o
64
Sei Σ = (S, I, D) eine destruktive Signatur, A = (A, Op) eine Σ-Algebra, V eine S-sortige
Menge von “Farben” und g : A → V eine – Färbung (coloring) genannte – S-sortige
Funktion.
In Abhängigkeit von g berechnet die wie folgt definierte T (S, I)-sortige Funktion
g # : A → DTΣ(V ),
die Coextension von g, zu jedem Zustand a ∈ A das Verhalten von a in Gestalt eines
Σ-Coterms über V :
• Für alle I ∈ I, gI# = idI .
• Für alle s ∈ S und a ∈ As, gs#(a) = gs(a){d → ge#(dA(a)) | d : s → e ∈ D}.
• Für alle I ∈ I, (ei)i∈I ∈ T (S, I) und a ∈ AQi∈I ei ,
#
#
gQ
ei (a) = tup{i → gei (πi (a)) | i ∈ I}.
i∈I
• Für alle I ∈ I, {ei}i∈I ⊆ T (S, I), k ∈ I und a ∈ Aek ,
#
#
g`
ei (ιk (a)) = k{sel → gek (a)}.
i∈I
(1)
(2)
(3)
(4)
Nicht g # selbst, wohl aber jedes einzelne Bild
g #(a) : XΣ∗ (→ YΣ,V
von g # ist induktiv definiert. So ist z.B. (2) eine Kurzform für:
65


falls w = ,
 gs(a)
gs#(a)(w) = ge#(dA(a))(w0) falls ∃ d : s → e ∈ D, w0 ∈ XΣ∗ : w = dw0,

 undefiniert
sonst.
Satz 2.19 ([32], Theorem COFREE)
g # ist Σ-homomorph und der einzige Σ-Homomorphismus von A nach DTΣ(V ) ist, der für
alle a ∈ A g #(a)() = g(a) erfüllt.
Offenbar hängt die Einschränkung von g # auf Grundcoterme nicht von g ab. Sie wird
Zustandsentfaltung genannt und mit unfold A bezeichnet.
Umgekehrt stimmt g # mit der Zustandsentfaltung unfold A(g) : A(g) → DTΣ0 überein,
wobei
Σ0 =def (S, I ∪ {Vs | s ∈ S}, D ∪ {vars : s → Vs | s ∈ S})
A(g)
und A(g) ∈ AlgΣ0 definiert ist durch A(g)|Σ = A und vars
= g für alle s ∈ S.
Aus Satz 2.19 folgt sofort:
unfold A ist der einzige Σ-Homomorphismus von A nach DTΣ.
(5)
Wegen (5) nennt man DTΣ eine finale oder terminale Σ-Algebra.
66
Wie Initialität ist auch Finalität eine universelle Eigenschaft:
Alle finalen Σ-Algebren sind Σ-isomorph und jede zu einer finalen Σ-Algebra isomorphe
Σ-Algebra ist final. DTΣ wird deshalb auch die finale Σ-Algebra genannt.
Z.B. ist neben DTStream(X) auch StreamFun(X ) eine finale Stream(X)-Algebra (siehe
17.5), neben DTDAut(X,Y ) auch Beh(X, Y ) eine finale DAut(X, Y )-Algebra und neben
DTAcc(X) auch Pow (X) eine finale Acc(X)-Algebra (siehe 2.15).
Beispiel 2.20
Sei Σ = DAut(X, Y ). Die Trägermengen von DTΣ und Beh(X, Y ) sind isomorph:
Sei L = {(δ, x) | x ∈ X}. DTΣ besteht aus allen Funktionen von L∗ + L∗β nach 1 + Y ,
die für alle w ∈ L∗ w auf und wβ auf ein Element von Y abbilden, m.a.W.:
DTΣ ∼
= 1
L∗
×Y
L∗ β
∼
= Y
L∗ β
L∗ β ∼
=X ∗
∼
=
∗
YX .
ξ : Beh(X, Y ) → DTΣ bezeichne den entsprechenden DAut(X, Y )-Isomorphismus.
o
Aufgabe Wie lautet der Isomorphismus zwischen DTAcc(X) und Pow (X)?
o
67
Die Acc(X)-Algebra ist DTAcc(X) ist im Haskell-Modul Compiler.hs durch accC implementiert.
Coterme kann man als verallgemeinerte Verhaltensfunktionen betrachten. Auch der Realisierungsbegriff von Abschnitt 2.11 lässt sich von Beh(X, Y ) auf beliebige destruktive Signaturen übertragen:
Für alle Σ-Algebren, s ∈ S und a ∈ As,
(A, a) realisiert t ∈ DTΣ,s ⇔def
unfold A
s (a) = t.
Aufgabe Zeigen Sie, dass χ : P(X) → 2X ein Acc(X)-Homomorphismus von Pow (X)
nach Beh(X, 2) ist.
o
Sei A = (A, Op) eine Acc(X)-Algebra. Da Pow (X) final ist, gibt es genau einen Acc(X)Homomorphismus
unfoldP A : A → Pow (X).
68
Daraus ergeben sich folgende Äquivalenzen:
fold Lang(X) ist Acc(X)-homomorph
⇔ fold Lang(X) = unfoldP Bro(BS) : TReg(BS) → P(X ∗)
(1)
(2)
⇔ unfoldP Bro(BS) ist Reg(BS)-homomorph.
(3)
Aufgabe Zeigen Sie (2), indem Sie (1) oder (3) beweisen.
o
(2) folgt auch aus der Form der Gleichungen, die Bro(BS) definieren (siehe Beispiel 17.10).
Sei A = (A, Op) eine DAut(X, Y )-Algebra. Da Beh(X, Y ) final ist, gibt es genau einen
DAut(X, Y )-Homomorphismus
unfoldB A : A → Beh(X, Y ).
unfoldB A ordnet jedem Zustand a ∈ Astate eine Verhaltensfunktion zu, die für jedes Eingabewort w ∈ X ∗ die Ausgabe desjenigen Zustandes zurückgibt, den A nach Verarbeitung
von w erreicht hat:
runA (a)
βA
unfoldB A(a) = X ∗ −→ Astate −→ Y.
∗
A
X
runA : Astate → AX
state setzt die Transitionsfunktion δ : Astate → Astate auf Wörter fort:
69
Für alle a ∈ Astate, x ∈ X und w ∈ X ∗,
runA(a)() =def a,
runA(a)(xw) =def runA(δ A(a)(x))(w).
(4)
(5)
runA ist die Erreichbarkeitsfunktion von 2.16 mit vertauschten Argumenten:
runA(a)(w) = reachA(w)(a).
Außerdem gilt für alle v, w ∈ X ∗:
runA(a)(vw) = runA(runA(a)(v))(w).
(6)
(4) und (6) machen (die dekaskadierte Version von) runA zur Aktion des Monoids X ∗:
Für eine beliebige Zustandsmenge Q und ein beliebiges Monoid M mit Multiplikation ∗
und neutralem Element e heißt eine Funktion (·) : Q × M → Q Aktion von M , wenn
für alle q ∈ Q und m, m0 ∈ M Folgendes gilt:
q · e = q,
q · (m ∗ m0) = (q · m) · m0.
(7)
(8)
Aufgabe Sei h der Isomorphismus von DTDAut(X,Y ) nach Beh(X, Y ). Zeigen Sie:
runA = h ◦ id#
A.
o
(9)
70
Initiale Automaten
Sei a ∈ Astate. Man nennt das Paar (A, a) einen initialen Automaten, der die Verhaltensfunktion
unfoldB A(a) : X ∗ → Y
realisiert (siehe 2.15).
Sei L ⊆ X ∗ und Y = 2. (A, a) erkennt oder akzeptiert L, wenn (A, a) die charakteristische Funktion χ(L) : X ∗ → 2 von L realisiert.
∗
Da χ : P(X ∗) → 2X Acc(X)-homomorph ist (s.o.) und Beh(X, 2) eine finale Acc(X)Algebra, stimmt unfoldB A mit χ ◦ unfoldP A überein. Folglich wird L ⊆ X ∗ genau dann
von (A, a) erkannt, wenn L mit unfoldP A(a) übereinstimmt:
unfoldP A(a) = L ⇔ χ(unfoldP A(a)) = χ(L) ⇔ unfoldB A(a) = χ(L)
⇔ (A, a) erkennt L.
(6)
Insbesondere gilt für jeden regulären Ausdruck t ∈ TReg(BS), dass der initiale Automat
(Bro(BS), t) die Sprache von t erkennt:
(Bro(BS), t) erkennt fold Lang(X)(t)
(6)
(2)
⇔ unfoldP Bro (BS)(t) = fold Lang(X)(t) ⇔ fold Lang(X)(t) = fold Lang(X)(t).
71
Damit erfüllt (Bro(BS), t) dieselbe Aufgabe wie der klassische Potenzautomat zur Erkennung der Sprache von t, der über den Umweg eines nichtdeterministischen Automaten mit
-Übergängen aus t gebildet wird (siehe Beispiel 12.3).
Übersicht über die oben definierten Reg(BS)- bzw. DAut(X, Y )-Algebren und ihre jeweiligen Implementierungen in Compiler.hs:
72
Signatur
Reg(BS)
Acc(X)
TReg(BS)
RegT
TReg(BS)
regT
initial
Bro(BS)
accT
P(X ∗)
Lang(X)
Trägermenge
2X
∗
YX
2
Algebra
χ(Lang(X))
regB
Pow (X)
final
χ(Pow (X))
Beh(X, 2)
behFun
final
Beh(X, Y )
final
∗
N → syms(BS)∗
DAut(X, Y )
Regword (BS)
regWord
Bool
73
2.21 Compiler für reguläre Ausdrücke
Im Folgenden werden wir einen Parser für die Wortdarstellung eines regulären Ausdrucks
t mit dessen Faltung in einer beliebigen Reg(BS)-Algebra A verknüpfen und damit ein
erstes Beispiel für einen Compiler erhalten, der die Elemente einer Wortmenge ohne den
Umweg über Reg(BS)-Terme nach A – übersetzt.
Werden reguläre Ausdrücke als Wörter syms(BS) (siehe 2.14) eingelesen, dann muss dem
Erkenner fold χ(Lang(X))(t) eine Funktion
parseREG : syms(BS)∗ → M (TReg(BS))
vorgeschaltet werden, die jedes korrekte Eingabewort w in einen oder mehrere Reg(BS)Terme t mit fold Regword (BS)(t)(0) = w überführt. M ist ein monadischer Funktor (siehe
5.1), der die Ausgabe des Parsers steuert und insbesondere Fehlermeldungen erzeugt, falls
w keinem Reg(BS)-Term entspricht.
REG steht hier für eine konkrete Syntax, d.h. eine kontextfreie Grammatik, deren Sprache
mit dem Bild von TReg(BS) unter flip(fold Regword (BS))(0) übereinstimmt (siehe Beispiel 4.1).
Sei X eine Menge, G eine kontextfreie Grammatik mit abstrakter Syntax Σ und Sprache
L ⊆ X ∗. In Kapitel 5 werden wir einen generischen Compiler für L, der jedes Wort von
L in Elemente einer Σ-Algebra A übersetzt, als natürliche Transformation compileG des
konstanten Funktors const(X ∗) nach M M formulieren.
74
parseG ist diejenige Instanz von compileG, die Wörter von L in Syntaxbäume (= ΣT
Grundterme) übersetzt: parseG = compileGΣ .
Im Fall G = REG, Σ = Reg(BS), X = syms(BS) und M (A) = A+1 für alle Σ-Algebren
A stimmt compileA
G (w) mit der Faltung des durch w dargestellten regulären Ausdrucks t
in der Reg(BS)-Algebra A überein:
compile
ist nat. Transformation
T
G
compileA
=
M (fold A)(compileGReg(BS) (w))
G (w)
= M (fold A)(parseG(w)) = (fold A + id1)(parseG(w))
= (fold A + id1)(parseG(fold Regword (BS)(t)(0))) = (fold A + id1)(t) = fold A(t).
Im Haskell-Modul Compiler.hs ist compileREG durch die Funktion regToAlg implementiert, deren Zahlparameter eine von 8 Zielalgebren auswählt.
75
Im folgenden Diagramm sind die wichtigsten Beziehungen zwischen den behandelten Algebren und Homomorphismen zusammengefasst:
parseREG
1 + fold Lang(X)
1+χ
1 + Lang(X)
1 + χ(Lang(X))
syms(BS)
1 + TReg(BS)
≺
f
f
f
=
=
inc
=
inc
inc
Regword (BS)
π1 ◦ fold
+
∪
∪
Bool≺≺
fold
Bool
=
β
Bro(BS)
TReg(BS)
w
w
w
w
w
w
w
w
w
fold
Lang(X)
=
Bro(BS)
unfold
δ Bro(BS)
g
Bro(BS)X
Bro(BS)
Lang(X)
w
w
w
w
w
w
w
w
w
Pow (X)
δ Pow (X)
g
Pow (X)X
∪
χ(Lang(X))
w
w
w
w
w
w
=
w
w
w
χ
χ
Beh(X, 2)
δ Beh(X,2)
g
Beh(X, 2)X
76
2.22 Minimale Automaten
Wie zeigt man, dass initiale Automaten bestimmte Verhaltensfunktionen realisieren bzw.
Sprachen erkennen?
Sei A = (A, Op) eine Acc(X)-Algebra, und f : A → P(X ∗).
Da Pow (X) eine finale Acc(X)-Algebra ist (s.o.), erkennt für alle a ∈ Astate der initiale
Automat (A, a) genau dann die Sprache f (a), wenn f Acc(X)-homomorph ist.
Beispiel 2.23 Sei
Pn
L = {(x1, . . . , xn) ∈ Z |
i=1 xi ist gerade},
P
n
L0 = {(x1, . . . , xn) ∈ Z∗ |
i=1 xi ist ungerade}.
∗
Die Funktion h : eo → Pow (X) (siehe 2.11) mit h(Esum) = L und h(Osum) = L0 ist
Acc(Z)-homomorph. Also wird L vom initialen Automaten (eo, Esum) und L0 vom initialen Automaten (eo, Osum) erkannt.
o
Sei (A, a) ein initialer Automat. Die Elemente der Menge hai =def runA(a)(X ∗) heißen
Folgezustände von a in A.
77
Satz 2.24 Sei A = (A, Op) eine DAut(X, Y )-Algebra.
(i) Für alle a ∈ A ist hai die (Trägermenge der) kleinste(n) DAut(X, Y )-Unteralgebra
von A (siehe Kapitel 3), die a enthält.
(ii) Für alle Σ-Homomorphismen h : A → B gilt h(hai) = hh(a)i.
(iii) Für alle f : X ∗ → Y ist (hf i, f ) eine minimale Realisierung von f .
Beweis. (i): Wir zeigen zunächst durch Induktion über |w|, dass für alle a ∈ A, x ∈ X und
w ∈ X ∗ Folgendes gilt:
runA(a)(wx) = δ A(runA(a)(w))(x).
(5)
(7)
(4)
runA(a)(x) = runA(a)(x) = runA(δ A(a)(x))() = δ A(a)(x).
Für alle y ∈ X,
(5)
runA(a)(ywx) = runA(δ A(a)(y))(wx)
ind. hyp.
=
δ A(runA(δ A(a)(y))(w))(x)
(5)
= δ A(runA(yw))(x)
Wegen (7) gilt δ A(b)(x) ∈ hai für alle b ∈ hai und x ∈ X. Also ist hai eine Unteralgebra
von A.
Sei B eine Unteralgebra von A, die a enthält. Durch Induktion über |w| erhält man aus
(4) und (5), dass runA(a)(w) für alle w ∈ X ∗ zu B gehört. Also ist hai die kleinste
Unteralgebra von A, die a enthält.
78
(ii): Da h Σ-homomorph ist, ist h auch mit run verträglich (s.o.), d.h. für alle w ∈ X ∗ gilt
h(runA(a)(w)) = runB (h(a))(w). Daraus folgt sowohl h(hai) ⊆ hh(a)i als auch hh(a)i ⊆
h(hai).
∗
(iii): Da Beh(X, Y ) final ist, stimmt unfold B Beh(X,Y ) (s.o.) mit der Identität auf Y X überein. Also ist unfold B Beh(X,Y )(f ) = f und damit (hf i, f ) wegen (i) ein initialer Automat,
der f realisiert.
Sei (A = (A, Op), a) ein initialer Automat, der f realisiert.
Wegen (ii) ist h : hai → hunfoldB A(a)i mit h(b) =def unfoldB A(b) für alle b ∈ hai
wohldefiniert und surjektiv. Daraus folgt |hf i| = |hunfoldB A(a)i| ≤ |hai| ≤ |A|.
o
79
2.25 Baumautomaten [3, 33, 34, 42]
Sei Σ eine konstruktive Signatur und Y eine Menge. Ein (Σ, Y )-Baumautomat (A, out)
enthält neben einer Σ-Algebra A = (A, Op) eine S-sortige Funktion out : A → Y .
Die Funktion out ◦ fold A : TΣ → Y nennen wir das von (A, out) realisierte Baumverhalten.
Wie man leicht sieht, ist die Funktion
h : TList(X) → X ∗
nil 7→ cons(x, t) 7→ x · h(t)
bijektiv. Folglich verallgemeinert der Begriff eines Baumverhaltens f : TΣ → Y den einer
Verhaltensfunktion f : X ∗ → Y .
Dementsprechend nennen wir im Fall Y = 2 (S-sortige) Teilmengen von TΣ Baumsprachen und nennen L(A, out) =def χ−1(out ◦ fold A) die vom (Σ, Y )-Baumautomaten
(A, out) erkannte Baumsprache und L ⊆ TΣ regulär, wenn es einen endlichen (!)
(Σ, 2)-Baumautomaten (A, out) gibt, der L erkennt, d.h. die Gleichung out ◦ fold A = χ(L)
erfüllt.
Ein initialer Automat (A = (A, Op), a) liefert den (List(X), Y )-Baumautomaten (B, β A)
80
mit
Blist = Areg , nilB = a und consB = λ(x, a).δ A(a)(x),
der unfoldB A(a) ◦ h realisiert.
Umgekehrt liefert ein (List(X), Y )-Baumautomat (B, f ) den initialen Automaten (A, nilB )
mit
Areg = Blist, δ A = λa.λx.consB (x, a) und β A = f,
der f ◦ fold A ◦ h−1 (im Sinne von 2.17) realisiert.
Eine Baumsprache L ⊆ TList(X) ist also genau dann regulär, wenn es einen endlichen
initialen Automaten (A, a) gibt, der h(L) erkennt, was wiederum – wegen 3.13 oder 12.3 –
genau dann gilt, wenn h(L) – im Sinne von 2.17 – regulär ist.
Baumautomaten werden zum Beispiel bei der formalen Behandlung von XML-Dokumenten
eingesetzt (siehe Beispiel 4.3).
81
3 Rechnen mit Algebren
In diesem Abschnitt werden die beiden wichtigsten einstelligen Algebratransformationen: die
Bildung von Unteralgebren bzw. Quotienten, und ihr Zusammenhang zu Homomorphismen
vorgestellt. Unteralgebren und Quotienten modellieren Restriktionen bzw. Abstraktionen
eines gegebenen Modells. Beide Konstrukte sind aus der Mengenlehre bekannt. Sie werden
hier auf S-sortige Mengen fortgesetzt.
Sei Σ = (S, I, F ) eine Signatur, A = (Ae)e∈T (S,I) eine typverträgliche T (S, I)-sortige
Menge, n > 0 und Rs ⊆ Ans für alle s ∈ S.
R = (Rs)s∈S wird wie folgt zur T (S, I)-sortigen Relation geliftet:
• For all I ∈ I, RI =def ∆nI.
• For all I ∈ I and {ei}i∈I ⊆ T (S, I),
RQi∈I ei =def {(a1, . . . , an) ∈ AnQ
i∈I ei
| ∀ i ∈ I : (πi(a1), . . . , πi(an)) ∈ Rei },
R`i∈I ei =def {(ιi(a1), . . . , ιi(an)) | (a1, . . . , an) ∈ Rei , i ∈ I} ⊆ An`
i∈I ei
.
82
Lemma LIFT Seien g, h : A → B S-sortige Funktionen und R = (Rs)s∈S und R0 =
(Rs0 )s∈S die S-sortigen Relationen mit
Rs = {a ∈ As | g(a) = h(a)} und Rs0 = {(g(a), h(a)) | a ∈ As}
für alle s ∈ S. Dann gilt
Re = {a ∈ Ae | ge(a) = he(a)} und Re0 = {(ge(a), he(a)) | a ∈ Ae}
für alle e ∈ T (S, I).
o
Unteralgebren
Sei Σ = (S, I, F ) eine Signatur und A = (A, Op) eine Σ-Algebra.
Eine S-sortige Teilmenge R = (Rs)s∈S von A heißt Σ-Invariante, wenn für alle f : e →
e0 ∈ F und a ∈ Re f A(a) ∈ Re0 gilt.
Man nennt R Σ-Invariante, weil die Zugehörigkeit von Elementen von A zu R invariant
gegenüber der Anwendung von Operationenvon Σ ist.
R induziert die Σ-Unteralgebra A|R von A:
• Für alle e ∈ T (S, I), (A|R )e =def Re.
• Für alle f : e → e0 ∈ F und a ∈ Re, f A|R (a) =def f A(a).
83
Beispiel 3.1
S
Sei X = BS \ 1. Die Menge fold Lang(X)(TReg(BS)) der regulären Sprachen über X ist eine
Reg(BS)-Invariante von Lang(X). Das folgt allein aus der Verträglichkeit von fold Lang(X)
mit den Operationen von Reg(BS).
Für jede Signatur Σ, jede Σ-Algebra A = (A, Op) und jeden Σ-Homomorphismus h : A →
B ist (h(As))s∈S eine Σ-Invariante. Die von ihr induzierte Unteralgebra von A stimmt mit
der weiter oben definierten Bildalgebra h(A) überein.
Für jede konstruktive Signatur Σ ist TΣ(V ) eine Σ-Unteralgebra von CTΣ(V ).
Für jede destruktive Signatur Σ ist coTΣ(V ) eine Σ-Unteralgebra von DTΣ(V ).
o
Satz 3.2
(1) Sei B = (B, Op0) eine Unteralgebra von A = (A, Op). Die T (S, I)-sortige Funktion
incB = (incBe : Be → Ae)e∈T (S,I) ist Σ-homomorph.
(2) (Homomorphiesatz) Sei h : C → A ein Σ-Homomorphismus. h(C) ist eine Unteralgebra
von A.
h0 : C → h(C)
c 7→ h(c)
ist ein surjektiver Σ-Homomorphismus.
84
Ist h injektiv, dann ist h0 bijektiv.
Alle Σ-Homomorphismen h00 : C → h(C) mit inch(C) ◦ h00 = h stimmen mit h0 überein.
h
A
C
h0
inch(A)
h(A)
(3) Sei Σ eine konstruktive Signatur und A eine Σ-Algebra. fold A(TΣ) ist die kleinste ΣUnteralgebra von A und TΣ ist die einzige Σ-Unteralgebra von TΣ.
Demnach erfüllen alle zu TΣ isomorphen Σ-Algebren A das Induktionsprinzip, d.h. eine
prädikatenlogische Formel ϕ gilt für alle Elemente von A, wenn ϕ von allen Elementen einer
Σ-Unteralgebra von A erfüllt wird.
(4) Sei A eine Σ-Algebra und die A die einzige Σ-Unteralgebra von A. Dann gibt es für alle
Σ-Algebren B höchstens einen Σ-Homomorphismus h : A → B.
Beweis von (3). Sei B eine Unteralgebra von A. Da TΣ initial und incB Σ-homomorph
ist, kommutiert das folgende Diagramm:
85
fold A
A
TΣ
fold
=
B
incB
B
Daraus folgt für alle t ∈ TΣ,
fold A(t) = incB (fold B (t)) = fold B (t) ∈ B.
Also ist das Bild von fold A in B enthalten.
Sei B eine Unteralgebra von TΣ. Da TΣ initial ist, folgt incB ◦fold B = idTΣ aus (1). Da idTΣ
surjektiv ist, ist auch incB surjektiv. Da incB auch injektiv ist, sind B und TΣ isomorph.
Also kann B nur mit TΣ übereinstimmen.
Beweis von (4). Seien g, h : A → B Σ-Homomorphismen. Dann ist
C = {a ∈ A | g(a) = h(a)}
eine Σ-Unteralgebra von A: Sei f : e → e0 ∈ F und a ∈ Ae mit ge(a) = he(a). Da g und
h Σ-homomorph sind, gilt ge0 (f A(a)) = f B (ge(a)) = f B (he(a)) = he0 (f A(a)). Da g und h
S-sortig sind, folgt f A(a) ∈ Ce aus Lemma LIFT.
86
Da A die einzige Σ-Unteralgebra von A ist, stimmt C mit A überein, d.h. für alle a ∈ A
gilt g(a) = h(a). Also ist g = h.
o
Beispiel 3.3 (siehe Beispiel 3.1) Nach Satz 3.2 (3) ist fold Lang(X)(TReg(BS)) die kleinste
Unteralgebra von Lang(X).
o
Kongruenzen und Quotienten
Sei Σ = (S, I, F ) eine Signatur und A = (A, Op) eine Σ-Algebra.
Eine S-sortige Teilmenge R = (Rs)s∈S von A2 heißt Σ-Kongruenz, wenn für alle s ∈ S Rs
eine Äquivalenzrelation ist und für alle f : e → e0 ∈ F und (a, b) ∈ Re (f A(a), f B (b)) ∈ Re0
gilt.
R induziert die Σ-Quotientenalgebra A/R von A:
• Für alle e ∈ T (S, I), (A/R)e =def {[a]R | a ∈ Ae},
wobei [a]R = {b ∈ Ae | (a, b) ∈ Re}.
• Für alle f : e → e0 ∈ F und a ∈ Ae, f A/R ([a]R ) =def [f A(a)]R .
87
Satz 3.4 (ist dual zu Satz 3.2)
(1) Sei A eine Σ-Algebra und R eine Σ-Kongruenz auf A. Die natürliche Abbildung
natR : A → A/R, die jedes Element a ∈ A auf seine Äquivalenzklasse [a]R abbildet, ist
Σ-homomorph.
(2) (Homomorphiesatz) Sei h : A → B ein Σ-Homomorphismus und ker(h) ⊆ A2 der
Kern von h, d.h. ker(h) = {(a, b) | h(a) = h(b)}.
ker(h) ist eine Σ-Kongruenz.
h0 : A/ker(h) → B
[a]ker(h) 7→ h(a)
ist ein injektiver Σ-Homomorphismus.
Ist h surjektiv, dann ist h0 bijektiv.
Alle Σ-Homomorphismen h00 : A/ker(h) → B mit h00 ◦ nat = h stimmen mit h0 überein.
h
B
A
h0
natker(h)
A/ker(h)
88
(3) Sei Σ eine destruktive Signatur, A eine Σ-Algebra und Fin eine finale Σ-Algebra (s.o).
Der – Verhaltenskongruenz von A genannte – Kern des eindeutigen Σ-Homomorphismus unfold A : A → Fin ist die größte Σ-Kongruenz auf A und die Diagonale von Fin 2 die
einzige Σ-Kongruenz R auf Fin.
Demnach erfüllen alle zu Fin isomorphen Σ-Algebren A das Coinduktionsprinzip: Sei
E eine Menge von Σ-Gleichungen t = u zwischen Σ-Termen über V , die denselben Typ
haben. E gilt in A, wenn es eine Σ-Kongruenz R auf A gibt, die für alle t = u ∈ E und
g : V → A das Paar (g ∗(t), g ∗(u)) enthält.
(4) Sei B eine Σ-Algebra und die Diagonale von B 2 die einzige Σ-Kongruenz R auf B.
Dann gibt es für alle Σ-Algebren A höchstens einen Σ-Homomorphismus h : A → B.
Beweis von (3). Sei R eine Kongruenz auf A. Da Fin final und natR Σ-homomorph ist,
kommutiert das folgende Diagramm:
unfold A
Fin
A
unfold A/R
natR
A/R
89
Daraus folgt für alle a, b ∈ A,
(a, b) ∈ R ⇒ [a]R = [b]R
⇒ unfold A(a) = unfold A/R ([a]R ) = unfold A/R ([b]R ) = unfold A(b).
Also ist R im Kern von unfold A enthalten.
Sei R eine Kongruenz auf Fin. Da Fin final ist, folgt unfold Fin/R ◦natR = idFin aus (1). Da
idFin injektiv ist, ist auch natR injektiv. Da natR auch surjektiv ist, sind Fin und Fin/R
isomorph. Also kann R nur die Diagonale von Fin 2 sein.
Beweis von (4). Seien g, h : A → B Σ-Homomorphismen. Dann ist
R = {(g(a), h(a)) | a ∈ A}
eine Σ-Kongruenz auf B: Sei f : e → e0 ∈ F , a ∈ Ae und (ge(a), he(a)) ∈ Re. Da g und h
Σ-homomorph sind, gelten f B (ge(a)) = ge0 (f A(a)) und f B (he(a)) = he0 (f A(a)).
Da g und h S-sortig sind, folgt
(f B (ge(a)), f B (he(a))) = (ge0 (f A(a)), he0 (f A(a))) ∈ Re0
aus Lemma LIFT. Da ∆2B die einzige Σ-Kongruenz auf B ist, stimmt R mit ∆2B überein,
d.h. für alle a ∈ A gilt g(a) = h(a). Also ist g = h.
o
90
Automatenminimierung durch Quotientenbildung
Eine minimale Realisierung einer Verhaltensfunktion f : X ∗ → Y erhält man auch als
Quotienten eines gegebenen initialen Automaten A:
Im Beweis von Satz 2.24 (iii) wurde der DAut(X, Y )-Homomorphismus h : hai → hf i
definiert. Wegen der Surjektivität von h ist hai/ker(h) nach Satz 3.4 (2) DAut(X, Y )isomorph zu hf i. Nach Definition von h ist ker(h) = ker(unfold A) ∩ hai2.
Nach Satz 3.4 (3) ist ker(unfold A) die größte Σ-Kongruenz auf A, also die größte binäre
Relation R auf A, die für alle a, b ∈ Astate folgende Bedingung erfüllt:
(a, b) ∈ R
⇒
β A(a) = β A(b) ∧ ∀ w ∈ X ∗ : (δ A(a)(w), δ A(b)(w)) ∈ R.
(1)
Ist Astate endlich, dann lässt sich ker(unfold A) schnell und elegant mit dem in Abschnitt
2.3 von [27] und in [31] beschriebene (und in Haskell implementierte) Paull-UngerVerfahren berechnen. Es startet mit R0 = A2state und benutzt die Kontraposition
(a, b) 6∈ R
⇐
β A(a) 6= β A(b) ∨ ∀ w ∈ X ∗ : (δ A(a)(w), δ A(b)(w)) 6∈ R
(2)
von (1), um R0 schrittweise auf den Kern von unfold A zu reduzieren.
91
Transitionsmonoid und syntaktische Kongruenz
Sei A = (A, Op) eine DAut(X, Y )-Algebra. Das Bild der Mon-homomorphen Erreichbarkeitsfunktion
reachA : X ∗ → (Astate → Astate)
von A heißt Transitionsmonoid von A.
Satz 3.5 (Transitionsmonoide und endliche Automaten)
Sei a ∈ Astate und R der Kern von reachhai : X ∗ → (hai → hai). hai ist genau dann
endlich, wenn R endlich viele Äquivalenzklassen hat oder das Transitionsmonoid von hai
endlich ist.
Beweis. Nach Satz 3.4 (2) ist das Transitionsmonoid von hai Mon-isomorph zum Quotienten X ∗/R. Außerdem ist die Abbildung
h : X ∗/R → hai
[w]R 7→ δA∗(a)(w)
wohldefiniert: Sei (v, w) ∈ R. Dann gilt reachhai(v) = reachhai(w), also insbesondere
h([v]R ) = runA(a)(v) = δ hai∗(a)(v) = reachhai(v)(a) = reachhai(w)(a) = δ hai∗(a)(w)
= runA(a)(w) = h([w]R ).
92
Da h surjektiv ist, liefert die Komposition h ◦ g mit dem Isomorphismus
g : reachhai(X ∗) → X ∗/R
eine surjektive Abbildung von reachhai(X ∗) nach hai. Also überträgt sich die Endlichkeit
des Transitionsmonoids von hai auf hai selbst. Umgekehrt ist das Transitionsmonoid jedes
endlichen Automaten A endlich, weil es eine Teilmenge von Astate → Astate ist.
o
Sei L ⊆ X ∗. Das Transitionsmonoid von hLi heißt syntaktisches Monoid und der Kern
von reachhLi syntaktische Kongruenz von L.
Letztere lässt sich als Menge aller Paare (v, w) ∈ X ∗ × X ∗ mit uvu0 ∈ L ⇔ uwu0 ∈ L
für alle u, u0 ∈ X ∗ charakterisieren. Satz 3.5 impliziert, dass sie genau dann endlich viele
Äquivalenzklassen hat, wenn L von einem endlichen initialen Automaten (A, a) erkannt
wird, was wiederum zur Regularität von L äquivalent ist (siehe 3.13 für einen direkten Beweis
oder Beispiel 12.3 für den klassischen über die Konstruktion eines nichtdeterministischen
Akzeptors von L).
Folglich kann die Nichtregularität einer Sprache L oft gezeigt werden, indem aus der Endlichkeit der Zustandsmenge eines Akzeptors von L ein Widerspruch hergeleitet wird.
Wäre z.B. L = {xny n | n ∈ N} regulär, dann gäbe es einen endlichen Automaten (A, a)
mit unfold A(a) = L.
93
Demnach müsste für alle n ∈ N ein Zustand bn ∈ Areg existieren mit runA(a)(xn) = bn
und β A(runA(bn)(y n)) = 1. Da A endlich ist, gäbe es i, j ∈ N mit i 6= j und bi = bj .
Daraus würde jedoch
unfold A(xiy j ) = β A(runA(a)(xiy j )) = β A(runA(runA(a)(xi))(y j )) = β A(runA(bi)(y j ))
= β A(runA(bj )(y j )) = 1
folgen, im Widerspruch dazu, dass xiy j nicht zu L gehört. Also ist L nicht regulär.
3.6 Termsubstitution
Sei Σ = (S, I, F ) eine Signatur. Belegungen von Variablen durch Σ-Terme heißen Substitutionen und werden üblicherweise mit kleinen griechischen Buchstaben bezeichnet.
Im Gegensatz zu den Belegungen von Kapitel 2 beschränken wir den Wertebereich von
Substitutionen σ : V → TΣ(V ) nicht auf die Trägermenge der Algebra TΣ(V ), also auf
Terme ohne λ und ite.
Um ungewollte Bindungen von Variablen zu vermeiden, muss die Fortsetzung
σ ∗ : TΣ(V ) → TΣ(V )
von σ auf Terme anders als die Auswertung g ∗ : TΣ(V ) → A einer Belegung g : V → A
von Kapitel 2 definiert werden:
94
• Für alle X ∈
( BS, e ∈ T (S, I), x ∈ VX und t ∈ TΣ(V )e,
λx0.σ[x0/x]∗(t) falls x ∈ var(σ(free(t) \ {x})),
∗
σ (λx.t) =
λx.σ[x/x]∗(t) sonst.
• Für alle e ∈ T (S, I) t ∈ TΣ(V )2 und u, v ∈ TΣ(V )e,
σ ∗(ite(t, u, v)) = ite(σ ∗(t), σ ∗(u), σ ∗(v)).
In den restlichen Fällen ist σ ∗ genauso definiert wie g ∗:
• Für alle x ∈ V , σ ∗(x) = σ(x).
• Für alle x ∈ X ∈ BS, σ ∗(x) = x.
• Für alle n > 1 und t1, . . . , tn ∈ TΣ(V ), σ ∗(t1, . . . , tn) = (σ ∗(t1), . . . , σ ∗(tn)).
• Für alle f : e → e0 ∈ F und t ∈ TΣ(V )e, σ ∗(f t) = f σ ∗(t).
• Für alle X ∈ BS, e ∈ T (S, I), t ∈ TΣ(V )eX und u ∈ TΣ(V )X ,
σ ∗(t(u)) = σ ∗(t)(σ ∗(u)).
σ ∗(t) heißt σ-Instanz von t und Grundinstanz, falls σ alle Variablen von t auf Grundterme abbildet.
Man schreibt häufig tσ anstelle von σ ∗(t) sowie {t1/x1, . . . , tn/xn} für die Substitution σ
mit σ(xi) = ti für alle 1 ≤ i ≤ n und σ(x) = x für alle x ∈ V \ {x1, . . . , xn}.
95
Satz 3.7 (Auswertung und Substitution)
(1) Für alle Belegungen g : V → A und Σ-Homomorphismen h : A → B gilt:
(h ◦ g)∗ = h ◦ g ∗.
(2) Für alle Substitutionen σ, τ : V → TΣ(V ) gilt:
(σ ∗ ◦ τ )∗ = σ ∗ ◦ τ ∗.
Beweis. (1) Induktion über den Aufbau von Σ-Termen. (2) folgt aus (1), weil σ ∗ ein ΣHomomorphismus ist.
o
Termäquivalenz und Normalformen
Sei Σ = (S, I, F ) eine konstruktive Signatur. In diesem Abschnitt geht es um Σ-Algebren
A, die eine gegebene Menge E von Σ-Gleichungen (s.o.) erfüllen, d.h. für die Folgendes
gilt:
• für alle t = t0 ∈ E und g : V → A gilt g ∗(t) = g ∗(t0).
AlgΣ,E bezeichnet die Klasse aller Σ-Algebren, die E erfüllen.
96
Aus Satz 3.7 (1) folgt sofort, dass AlgΣ,E unter Σ-homomorphen Bildern abgeschlossen, d.h.
für alle Σ-Homomorphismen h : A → B gilt:
A ∈ AlgΣ,E
⇒
h(A) ∈ AlgΣ,E .
Die Elemente der Menge
Inst(E ) =def {(tσ, t0σ) | (t, t0) ∈ E, σ : V → TΣ(V )}
(6)
heißen Instanzen von E.
Die E-Äquivalenz ≡E ist definiert als kleinste Σ-Kongruenz, die Inst(E) enthält.
Aufgabe Zeigen Sie, dass der Quotient TΣ(V )/≡E E erfüllt.
o
Satz 3.8 Für alle Belegungen g : V → A in eine Σ-Algebra A, die E erfüllt, faktorisiert g ∗ : TΣ(V ) → A durch TΣ(V )/≡E , d.h. es gibt einen Σ-Homomorphismus
h : TΣ(V )/≡E → A mit h ◦ nat≡E = g ∗.
97
V
incV
g
TΣ(V )
g
∗
nat≡E
TΣ(V )/≡E
(3)
h
g≺
A
Beweis. Da der Kern von g ∗ eine Σ-Kongruenz ist, die Inst(E ) enthält (siehe [29], Satz
GLK), ≡E aber die kleinste derartige Relation auf TΣ(V ) ist, ist letztere im Kern von g ∗
enthalten. Folglich ist h : TΣ(V )/≡E → A mit h([t]≡E ) = h(t) für alle t ∈ TΣ(V ) wohldefiniert. (3) kommutiert, was zusammen mit der Surjektivität und Σ-Homomorphie von
nat≡E impliziert, dass auch h Σ-homomorph ist.
o
Die durch den gestrichelten Pfeil angedeutete Eigenschaft von h, der einzige Σ-Homomorphismus zu sein, der (3) kommutativ macht, folgt ebenfalls aus der Surjektivität und Σ-Homomorphie von nat≡E .
Zusammen mit TΣ/≡E ∈ AlgΣ,E impliziert Satz 3.8, dass TΣ/≡E initial in AlgΣ,E ist, dass
es also für alle A ∈ AlgΣ,E genau einen Σ-Homomorphismus h : TΣ/≡E → A gibt.
98
Da g ∗ im Fall V = ∅ mit fold A übereinstimmt, reduziert sich das obige Diagramm zu
folgendem:
nat≡E
A
TΣ
fold A
(3)
h
TΣ/≡E
Beispiel 3.9 Sei Σ = Mon, x, y, z ∈ V und
E = {mul(one, x) = x, mul(x, one) = x, mul(mul(x, y), z) = mul(x, mul(y, z))}.
Die freie Mon-Algebra TMon (V ) ist kein Monoid, wohl aber ihr Quotient TMon (V )/ ≡E .
Man nennt ihn das freie Monoid (über V ).
Aufgabe Zeigen Sie, dass das freie Monoid tatsächlich ein Monoid ist, also E erfüllt, und
Mon-isomorph zu V ∗ ist.
o
Bei der Implementierung von Quotienten werden isomorphe Darstellungen bevorzugt, deren
Elemente keine Äquivalenzklassen sind, diese aber eindeutig repräsentieren. Z.B. sind die
Wörter über V eindeutige Repräsentanten der Äquivalenzklassen von TMon (V )/≡E .
99
Bei der Berechnung äquivalenter Normalformen beschränkt man sich meist auf die folgende
Teilrelation von ≡E , die nur “orientierte” Anwendungen der Gleichungen von E zulässt:
Die E-Reduktionsrelation →E besteht aus allen Paaren
(u{t/x}, u{t0/x})
mit t = t0 ∈ E, u ∈ TΣ(V ) und σ : V → TΣ(V ).
+
Die kleinste transitive Relation auf TΣ(V ), die →E enthält, wird mit →E bezeichnet.
+
Aufgabe Zeigen Sie, dass →E die kleinste transitive und mit Σ verträgliche Relation auf
+
TΣ(V ) ist, die Inst(E ) enthält. Folgern Sie daraus, dass →E eine Teilmenge von ≡E ist.o
+
Sei t ∈ TΣ(V ). u ∈ TΣ(V ) heißt E-Normalform von t, wenn t →E u gilt und u zu
einer vorgegebenen Teilmenge von TΣ(V ) gehört.
Eine Funktion reduce : TΣ(V ) → TΣ(V ) heißt E-Reduktionsfunktion, wenn für alle
t ∈ TΣ(V ), reduce(t) eine E-Normalform von t ist.
Sei A ∈ AlgΣ,E und g : V → A. Wegen
+
→E ⊆ ≡E ⊆ ker(g ∗)
(siehe obige Aufgabe und den Beweis von Satz 3.8) gilt g ∗(t) = g ∗(reduce(t)) für alle
100
t ∈ TΣ(V ), also kurz:
g ∗ ◦ reduce = g ∗.
(3)
Allgemeine Methoden zur Berechnung von Normalformen werden in [29], §5.2 behandelt.
Beispiel 3.10 Normalformen regulärer Ausdrücke
E bestehe aus folgenden Reg(BS)-Gleichungen:
f (f (x, y), z) = f (x, f (y, z))
par(x, y) = par(y, x)
seq(x, par(y, z)) = par(seq(x, y), seq(x, z))
seq(par(x, y), z) = par(seq(x, z), seq(y, z))
par(x, x) = x
par(mt, x) = x
par(x, mt) = x
seq(eps, x) = x
seq(x, eps) = x
seq(mt, x) = mt
seq(x, mt) = mt
f (ite(x, y, z), z 0) = ite(x, f (y, z 0), f (z, z 0))
f (z 0, ite(x, y, z)) = ite(x, f (z 0, y), f (z 0, z))
(Assoziativität von f ∈ {par, seq})
(Kommutativität von par)
(Linksdistributivität von seq über par)
(Rechtsdistributivität von seq über par)
(Idempotenz von par)
(Neutralität von mt bzgl. par)
(Neutralität von eps bzgl. seq)
(Annihilation)
(f ∈ {par, seq})
(f ∈ {par, seq})
Demnach entfernt eine E-Reduktionsfunktion mt und Mehrfachkopien von Summanden
aus Summen sowie eps aus Produkten, ersetzt alle Produkte, die mt enthalten, durch mt,
101
distribuiert seq über par und linearisiert geschachtelte Summen und Produkte rechtsassoziativ. Angewendet auf Reg(BS)-Grundterme entspricht sie deren Faltung in der Reg(BS)Algebra regNorm (siehe Compiler.hs).
S
Sei X = BS. Da Lang(X) E erfüllt, gilt (3) für A = Lang(X).
Algebren, die E erfüllen, heißen idempotente Semiringe. Da E weder den Sternoperator
iter : reg → reg noch die Konstanten B : 1 → reg enthält, brauchen diese in einem
Semiring nicht definiert zu sein. Auch die Idempotenz von par ist keine Anforderung an
Semiringe.
Kleene-Algebren sind Semiringe, auf denen ein Sternoperator definiert ist und die neben E
weitere (den Sternoperator betreffende) Gleichungen erfüllen. Die Elemente von Beh(X, Y )
(siehe 2.7) heißen formale Potenzreihen, wenn Y ein Semiring ist (siehe [37], Kapitel 9).
3.11 Die Brzozowski-Gleichungen
Die folgende Menge BRE von – Brzozowski-Gleichungen genannten – Reg(BS)Gleichungen hat in TReg(BS) genau eine Lösung, d.h. es gibt genau eine Erweiterung von
TReg(BS) zur Acc(X)-Algebra, die BRE erfüllt (siehe Beispiel 17.4).
In Abschnitt 2.7 wurde diese Lösung unter dem Namen Bro(BS) eingeführt. Existenz und
Eindeutigkeit folgen aus Satz 17.1 und werden dort aus dem in Satz 3.2 (3) eingeführten
102
Induktionsprinzip abgeleitet.
δ(eps)
δ(mt)
δ(B)
δ(par(t, u))
δ(seq(t, u))
δ(iter(t))
β(eps)
β(mt)
β(B)
β(par(t, u))
β(seq(t, u))
β(iter(t))
=
=
=
=
=
=
=
=
=
=
=
=
λx.mt
λx.mt
λx.ite(x ∈ B, eps, mt)
λx.par(δ(t)(x), δ(u)(x))
λx.par(seq(δ(t)(x), u), ite(β(t), δ(u)(x), mt)),
λx.seq(δ(t)(x), iter(t))
1
0
0
max{β(t), β(u)}
β(t) ∗ β(u)
1
t und u sind hier Variablen der Sorte reg.
Nach obiger Lesart definiert BRE Destruktoren (δ und β) auf der Basis von Konstruktoren
von Reg(BS). Umgekehrt lässt sich BRE auch als Definition der Konstruktoren auf der
Basis der Destruktoren δ und β auffassen: Nach Satz 17.7 hat BRE nämlich in der finalen
Acc(X)-Algebra Pow (X) genau eine coinduktive Lösung, d.h. es gibt genau eine Erweiterung von Lang(X) zur Reg(BS)-Algebra, die BRE erfüllt (siehe Beispiel 17.10).
o
103
3.12 Optimierter Brzozowski-Automat
Der Erkenner Bro(BS) regulärer Sprachen (siehe Abschnitt 2.11) benötigt viel Platz, weil
die wiederholten Aufrufe von δ Bro(BS) aus t immer größere Ausdrücke erzeugen. Um das zu
vermeiden, ersetzen wir Bro(BS) durch die Acc(X)-Algebra Norm(BS) (norm in Compiler.hs), die bis auf die Interpretation von δ mit Bro(BS) übereinstimmt. δ Norm(BS) normalisiert die von δ Bro(BS) berechneten Folgezustände mit der in Beispiel 3.10 beschriebenen
Reduktionsfunktion
reduce : TReg(BS)(V ) → TReg(BS)(V ).
Für alle t ∈ TReg(BS),
δ Norm(BS)(t) =def reduce ◦ δ Bro(BS)(t).
(1)
fold Lang(X) ◦ reduce = fold Lang(X).
(2)
Gemäß Beispiel 3.10 gilt
104
Es bleibt zu zeigen, dass für alle t ∈ TReg(BS) die initialen Automaten (Bro(BS), t) und
(Norm(BS), t) dieselben Sprachen erkennen, d.h.
unfold Norm(BS)(t) = unfold Bro(BS)(t).
(3)
Wir beweisen (3) durch Induktion über die Länge der Wörter über X.
β Norm(BS)(runNorm(BS)(t)()) = β Norm(BS)(t) = β Bro(BS)(t) = β Bro(BS)(runBro(BS)(t)()).
Für alle x ∈ X und w ∈ X ∗,
β Norm(BS)(runNorm(BS)(t)(xw)) = β Norm(BS)(runNorm(BS)(δ Norm(BS)(t)(x))(w))
= unfold Norm(BS)(δ Norm(BS)(t)(x))(w)
ind. hyp.
=
unfold Bro(BS)(δ Norm(BS)(t)(x))(w)
(1)
= unfold Bro(BS)(reduce(δ Bro(BS)(t))(x))(w) = fold Lang(X)(reduce(δ Bro(BS)(t))(x))(w)
(2)
= fold Lang(X)(δ Bro(BS)(t)(x))(w) = unfold Bro(BS)(δ Bro(BS)(t)(x))(w)
= β Bro(BS)(runBro(BS)(δ Bro(BS)(t)(x))(w)) = β Bro(BS)(runBro(BS)(t)(xw)).
Also gilt (3):
unfold Norm(BS)(t) = {w ∈ X ∗ | β Norm(BS)(runNorm(BS)(t)(w)) = 1}
= {w ∈ X ∗ | β Bro(BS)(runBro(BS)(t)(w)) = 1} = unfold Bro(BS)(t).
Im Haskell-Modul Compiler.hs entspricht der Erkenner unfold Norm(BS)(t) dem Aufruf regToAl
"" w 4, wobei w die Wortdarstellung von t ist.
o
105
3.13 Erkenner regulärer Sprachen sind endlich
Aus der Gültigkeit von BRE in Pow (X) lassen sich die folgenden Gleichungen für
runPow (X) : P(X ∗) → P(X ∗)X
∗
ableiten (siehe 2.9): Für alle w ∈ X ∗, B ∈ BS und L, L0 ⊆ P(X ∗),
(
1 falls w = ,
runPow (X)(epsLang(X))(w)
=
∅ sonst,
runPow (X)(mtLang(X))(w)
= 
∅,

 C falls w = ,
Lang(X)
runPow (X)(B
)(w)
=
1 falls w ∈ C,

 ∅ sonst,
runPow (X)(parLang(X)(L, L0))(w) = runPow (X)(L)(w) ∪ runPow (X)(L0)(w),
runPow (X)(seq Lang(X)(L, L0))(w) = {uv | u ∈ runPow (X)(L)(w), v ∈ L0}
S
∪ uv=w (if ∈ runPow (X)(L)(u)
then runPow (X)(L0)(v) else ∅),
(4)
(5)
(6)
(7)
(8)
106
runPow (X)(iterLang(X)(L))(w) = {uv | u ∈ runPow (X)(L)(w), v ∈ iterPow (X)(L)}
S
T
∪ u1...unv=w (if ∈ ni=1 runPow (X)(L)(ui)
then {uv 0 | u ∈ runPow (X)(L)(v),
v 0 ∈ iterPow (X)(L)}
else ∅)
(9)
(siehe z.B. [37], Theorem 10.1).
Wir erinnern an Satz 2.24 (iii), aus dem folgt, dass für alle L ⊆ X ∗ der Unterautomat
(hLi, L) von (Pow (X), L) ein minimaler Erkenner von L ist. Ist L die Sprache eines regulären Ausdrucks t, ist also L = fold Lang(X)(t), dann ist
hLi = {runPow (X)(fold Lang(X)(t))(w) | w ∈ X ∗}.
Daraus folgt durch Induktion über den Aufbau von t, dass hLi endlich ist:
Im Fall t ∈ {eps, mt} ∪ {B | B ∈ BS} besteht hLi wegen (4), (5) und (6) aus zwei, einem
bzw. drei Zuständen.
Im Fall t = par(t0, t00) folgt |hLi| ≤ |hfold Lang(X)(t0)i| ∗ |hfold Lang(X)(t00)i| aus (7).
Im Fall t = seq(t0, t00) folgt |hLi| ≤ |hfold Lang(X)(t0)i| ∗ 2|hfold
Im Fall t = iter(t0) folgt |hLi| ≤ 2|hfold
Lang(X) (t0 )i|
Lang(X) (t00 )i|
aus (8).
aus (9).
107
Damit ist ohne den üblichen Umweg über Potenzautomaten (siehe Beispiel 12.3) gezeigt,
dass reguläre Sprachen von endlichen Automaten erkannt werden.
108
4 Kontextfreie Grammatiken (CFGs)
Sie ordnen einer konstruktiven Signatur Σ eine konkrete Syntax und damit eine vom Compiler verstehbare Quellsprache zu, so dass er diese in eine als Σ-Algebra formulierte Zielsprache
übersetzen kann. Auch wenn das zu die Quellsprache bereits als konstruktive Signatur Σ
und die Zielsprache als Σ-Algebra gegeben sind, benötigt der Compiler eine kontextfreie
Grammatik, um Zeichenfolgen in Elemente der Algebra zu übersetzen.
Eine kontextfreie Grammatik (CFG)
G = (S, BS, R)
besteht aus
• einer endlichen Menge S von Sorten, die auch Nichtterminale oder Variablen genannt
werden,
• einer endlichen Menge BS nichtleerer Basismengen, deren Singletons Terminale
genannt und mit ihrem jeweiligen einzigen Element gleichgesetzt werden.
• einer endlichen Menge R von Regeln s → w mit s ∈ S und w ∈ (S ∪ BS)∗, die auch
Produktionen genannt werden.
n > 0 Regeln s → w1, . . . , s → wn mit derselben linken Seite s werden oft zu der einen
Regel s → w1| . . . |wn zusammengefasst.
109
Beispiel 4.1
Sei BS eine endliche Menge von Basismengen, die ∅ und 1 enthält, syms(BS) wie in 2.14
definiert und
R = {reg → reg + reg, reg → reg reg, reg → reg ∗, reg → (reg)}
∪ {reg → B | B ∈ BS}.
REG =def ({reg}, syms(BS), R) ist eine CFG.
o
Oft genügt es, bei der Definition einer CFG nur deren Regeln und Basismengen anzugeben.
Automatisch bilden dann die Symbole, die auf der linken Seite einer Regel vorkommen, die
Menge S der Sorten, während alle anderen Wörter und Symbole (außer dem senkrechten
Strich |; s.o.) auf der rechten Seite einer Regel Terminale oder Namen mehrelementiger
Basismengen sind.
Beispiel 4.2 Die Regeln der CFG JavaLight für imperative Programme mit Konditionalen und Schleifen lauten wie folgt:
110
Commands →
Command →
Sum
Sumsect
Prod
Prodsect
Factor
Disjunct
Conjunct
Literal
→
→
→
→
→
→
→
→
Command Commands | Command
{Commands} | String = Sum; |
if Disjunct Command else Command |
if Disjunct Command | while Disjunct Command
Prod Sumsect
{+, −} Prod Sumsect | Factor Prodsect
{∗, /} Factor Prodsect | Z | String | (Sum)
Conjunct || Disjunct | Conjunct
Literal && Conjunct | Literal
!Literal | Sum Rel Sum | 2 | (Disjunct)
String, {+, −}, {∗, /}, Z, Rel und 2 sind die mehrelementigen Basismengen von JavaLight.
String bezeichnet die Menge aller Zeichenfolgen außer den in Regeln von JavaLight vorkommenden Symbolen und Wörtern außer String.
Rel bezeichnet eine Menge nicht näher spezifizierter binärer Relationen auf Z.
Ein aus der Sorte Commands ableitbares JavaLight-Programm ist z.B.
fact = 1; while x > 1 {fact = fact*x; x = x-1;}
o
111
Beispiel 4.3 XMLstore (siehe [19], Abschnitt 2)
Store
→
Orders
Order
P erson
Emails
Email
Items
Item
Stock
ItemS
Suppliers
Id
→
→
→
→
→
→
→
→
→
→
→
hstorei hstocki Stock h/stocki h/storei |
hstorei Orders hstocki Stock h/stocki h/storei
Order Orders | Order
horderi hcustomeri P erson h/customeri Items h/orderi
hnamei String h/namei | hnamei String h/namei Emails
Email Emails | hemaili String h/emaili
Item Items | Item
hitemi Id hpricei String h/pricei h/itemi
ItemS Stock | ItemS
hitemi Id hquantityi Z h/quantityi Suppliers h/itemi
hsupplieri P erson h/supplieri | Stock
hidi String h/idi
112
Die Sprache von XMLstore beschreibt XML-Dokumente wie z.B. das folgende:
<store> <order> <customer> <name> John Mitchell </name>
<email> [email protected] </email>
</customer>
<item> <id> I18F </id> <price> 100 </price> </item>
</order>
<stock>
<item> <id> IG8 </id> <quantity> 10 </quantity>
<supplier> <name> Al Jones </name>
<email> [email protected] </email>
<email> [email protected] </email>
</supplier>
</item>
<item> <id> J38H </id> <quantity> 30 </quantity>
<item> <id> J38H1 </id> <quantity> 10 </quantity>
<supplier> <name> Richard Bird </name> </supplier>
</item>
<item> <id> J38H2 </id> <quantity> 20 </quantity>
<supplier> <name> Mick Taylor </name> </supplier>
</item>
</item>
</stock>
</store>
113
Nicht-linksrekursive CFGs
Sei G = (S, BS, R) eine CFG und X =
S
BS.
X ist die Menge der Eingabesymbole, die Compiler für G verarbeiten. Demgegenüber tauchen auf den rechten Seiten der Regeln von G nur (Namen für) komplette Basismengen auf!
Die klassische Definition einer linksrekursiven Grammatik verwendet die (Links-)Ableitungsrelation
→G = {(vsw, vαw) | s → α ∈ R, v, w ∈ (S ∪ BS)∗}.
+
∗
→G und →G bezeichnen den transitiven bzw. reflexiv-transitiven Abschluss von →G.
+
G heißt linksrekursiv, falls es s ∈ S und w ∈ (S ∪ BS)∗ mit s →G sw gibt.
Z.B. ist REG linksrekursiv, JavaLight und XMLstore jedoch nicht (siehe Beispiele 4.1-4.3).
114
Linksassoziative Auswertung erzwingt keine Linksrekursion
Sind alle Regeln, deren abstrakte Syntax binäre Operationen darstellen, dann werden aus
diesen Operationen bestehende Syntaxbäume rechtsassoziativ ausgewertet. Bei ungeklammerten Ausdrücke wie x+y−z−z 0 oder x/y∗z/z 0 führt aber nur die linkassoziative Faltung
zum gewünschten Ergebnis. Die gegebene Grammatik muss deshalb um Sorten und Regeln
erweitert werden, die bewirken, dass aus der linkassoziativen eine semantisch äquivalente
rechtsassoziative Faltung wird.
Im Fall von JavaLight bewirken das die Sorten Sumsect und Prodsect und deren Regeln.
Die Namen der Sorten weisen auf deren Interpretation als Mengen von Sektionen hin,
also von Funktionen wie z.B. (∗5) : Z → Z. Angewendet auf eine Zahl x, liefert (∗5) das
Fünffache von x. Die linkassoziative Faltung ((x+y)−z)−z 0 bzw. ((x/y)∗z)/z 0 der obigen
Ausdrücke entspricht daher der – von den Regeln für Sumsect und Prodsect bewirkten –
rechtsassoziativen Faltung ((−z 0) ◦ ((−z) ◦ (+y)))(x) bzw. ((/z 0) ◦ ((∗z) ◦ (/y)))(x).
Die Sektionssorten von JavaLight würden automatisch eingeführt werden, wenn man die
folgende Entrekursivierung auf eine linksrekursive Variante der Grammatik anwendet.
115
Verfahren zur Eliminierung von Linksrekursion
Sei G = (S, BS, R) eine CFG und S = {s1, . . . , sn}.
Führe für alle 1 ≤ i ≤ n die beiden folgenden Schritte in der angegebenen Reihenfolge
durch:
• Für alle 1 ≤ j < i und Regelpaare (si → sj v, sj → w) ersetze die Regel si → sj v
durch die neue Regel si → wv.
• Falls vorhanden, streiche die Regel si → si.
• Für alle Regelpaare (si → siw, si → ev) mit e ∈ (S ∪ BS) \ {si} ersetze die Regel
o
si → siw durch die drei neuen Regeln si → evs0i, s0i → ws0i und s0i → w.
Die Verwendung von jeweils drei Sorten für arithmetische bzw. Boolesche Ausdrücke (Sum,
Prod und Factor bzw. Disjunct, Conjunct und Literal ) bewirkt ebenfalls eine bestimmte
Auswertungsreihenfolge und zwar diejenige, die sich aus den üblichen Prioritäten arithmetischer bzw. Boolescher Operationen ergibt.
116
Abstrakte Syntax
Sei G = (S, BS, R) eine CFG und Z = {B ∈ BS | |B| = 1} (Terminale von G).
Die konstruktive Signatur Σ(G) = (S, BS, F (G)) mit
F (G) = {fr : 1 → s | r = (s → w) ∈ R, w ∈ Z ∗} ∪
{fr : e1 × · · · × en → s | r = (s → w0e1w1 . . . enwn) ∈ R, n > 0,
w0 . . . wn ∈ Z ∗, e1, . . . , en ∈ S ∪ BS \ Z}
heißt abstrakte Syntax von G.
Beispiel 4.4
Die Signatur Reg(BS) ist eine Teilsignatur der abstrakten Syntax der CFG REG von
Beispiel 4.1.
Aufgabe
Zu welcher Regel von REG fehlt in Reg(BS) der entsprechende Konstruktor?
o
117
Die Konstruktoren von Σ(G) lassen sich i.d.R. direkt aus den Terminalen von G basteln. So
wird manchmal für den aus der Regel r = (s → w0e1w1 . . . enwn) mit wi ∈ Z ∗ entstandenen
Konstruktor fr die (Mixfix-)Darstellung w0 w1 . . . wn gewählt.
So könnte beispielsweise der Konstruktor f : Disjunct × Command × Command für die
JavaLight-Regel
Commands → if Disjunct Command else Command
(siehe Beispiel 4.2) if else genannt werden. Um Verwechslungen zwischen Terminalen
und Konstruktoren vorzubeugen, werden wir hier jedoch keine Terminale als Namen für
Konstruktoren verwenden.
Umgekehrt kann jede endlich verzweigende konstruktive Signatur Σ = (S, I, F ) in eine
CFG
G(Σ) = (S, BS ∪ {(, ), , }, R)
mit Σ(G(Σ)) = Σ überführt werden, wobei
R = {s → f (e1, . . . , en) | f : e1 × · · · × en → s ∈ F }.
Σ(G)-Grundterme werden Syntaxbäume von G genannt. Alle ihre Knoten sind mit
Operationssymbolen markiert.
118
Beispiel 4.5 SAB
Die Grammatik SAB besteht aus den Sorten S, A, B, den Terminalen a, b und den Regeln
r1 = S → aB,
r2 = S → bA,
r3 = S → ,
r4 = A → aS,
r5 = A → bAA,
r6 = B → bS,
r7 = B → aBB.
Demnach lauten die Konstruktoren der abstrakten Syntax von SAB wie folgt:
f1 : B → S,
f2 : A → S,
f3 : 1 → S,
f4 : S → A,
f5 : A × A → A,
f6 : S → B,
f7 : B × B → B.
f1
f7
f6
f6
f1
f3
f6
ε
f3
ε
Syntaxbaum des Eingabewortes aababb
Mit markierte Blätter eines Syntaxbaums werden künftig weglassen.
119
Die folgende SAB-Algebra SABcount berechnet die Anzahl #a(w) bzw. #b(w) der Vorkommen von a bzw. b im Eingabewort w:
SABcount S = SABcount A = SABcount B = N2,
f1SABcount = f4SABcount = λ(i, j).(i + 1, j),
f2SABcount = f6SABcount = λ(i, j).(i, j + 1),
f3SABcount
= (0, 0),
f5SABcount
= λ((i, j), (k, l)).(i + k, j + l + 1),
f7SABcount
= λ((i, j), (k, l)).(i + k + 1, j + l).
o
120
Beispiel 4.6 Σ(JavaLight) = (S, I, F )
S = { Commands, Command , Sum, Sumsect, Prod , Prodsect, Factor ,
Disjunct, Conjunct, Literal }
I = { 1, 2, {+, −}, {∗, /}, Z, String, Rel }
F = { seq : Command × Commands → Commands,
embed : Command → Commands,
block : Commands → Command ,
assign : String × Sum → Command ,
cond : Disjunct × Command × Command → Command ,
cond1, loop : Disjunct × Command → Command ,
sum : Prod × Sumsect → Sum,
sumsect : {+, −} × Prod × Sumsect → Sumsect,
nilS : 1 → Sumsect,
prod : Factor × Prodsect → Prod ,
prodsect : {∗, /} × Factor × Prodsect → Prodsect,
nilP : 1 → Prodsect,
embedI : Z → Factor ,
var : String → Factor ,
encloseS : Sum → Factor ,
121
disjunct : Conjunct × Disjunct → Disjunct,
embedC : Conjunct → Disjunct,
conjunct : Literal × Conjunct → Conjunct,
embedL : Literal → Conjunct,
not : Literal → Literal ,
atom : Sum × Rel × Sum → Literal ,
embedB : 2 → Literal ,
encloseD : Disjunct → Literal }
Z.B. hat das JavaLight-Programm
fact = 1; while x > 1 {fact = fact ∗ x; x = x − 1; }
folgenden Syntaxbaum:
122
Seq
Assign
fact
Embed
Sum
Prod
EmbedI
Loop
NilS
NilP
1
EmbedC
Block
EmbedL
Seq
Atom
Sum
Prod
Var
x
>
Sum
NilS
NilP
Assign
Prod
EmbedI
1
fact
NilS
NilP
Sum
Prod
Var
fact
Embed
Assign
NilS
x
Prodsect
*
Var
x
NilP
Sum
Prod
Var
x
NilP
Sumsect
-
Prod
EmbedI
NilS
NilP
1
javaToAlg "prog" 1 (siehe Java.hs) übersetzt das JavaLight-Programm in der Datei
prog in den zugehörigen Syntaxbaum und schreibt diesen in die Datei Pix/javaterm.svg.
Mit https://cloudconvert.org kann diese schnell in ein anderes Format konvertiert werden.
123
Beispiel 4.7 Σ(XMLstore) = (S , I, ∅, F )
S =
=
I =
F =
{
{
{
{
Store, Orders, Order, P erson, Emails, Email, Items, Item, Stock,
ItemS, Suppliers, Id }
1, String, Id , Z }
store
: Stock → Store,
storeO : Orders × Stock → Store,
orders : P erson × Items × Orders → Orders,
embedP : P erson × Items → Orders,
person : String → P erson,
personE : String × Emails → P erson,
emails : Email × Emails → Emails,
none
: 1 → Emails,
email
: String → Email,
items
: Id × String × Items → Items,
embedI : Id × String → Items,
stock
: Id × Z × Supplier × Stock → Stock,
embedS : Id × Z × Supplier → Stock,
supplier : P erson → Suppliers,
parts
: Stock → Suppliers,
id
: String → Id }
124
StoreO
EmbedP
PersonE
Stock
EmbedI
John Mitchell Emails
Id 10 Supplier
Id 100 IG8
Email None I18F
[email protected]
EmbedS
PersonE
Id 30 Parts
Al Jones Emails
Email
J38H
Emails
Id 10 Supplier
[email protected] Email None J38H1
[email protected]
Stock
PersonE
EmbedS
Id 20 Supplier
Richard Bird None J38H2
PersonE
Mick Taylor None
Syntaxbaum des XML-Dokumentes von Beispiel 4.3
xmlToAlg "xmldoc" 1 (siehe Compiler.hs) übersetzt das XMLstore-Dokument in der Datei xmldoc in den zugehörigen Syntaxbaum übersetzt und schreibt diesen in die Datei
Pix/xmlterm.svg.
xmlToAlg "xmldoc" 2 (siehe Compiler.hs) übersetzt xmldoc in eine Listen-ProdukteSummen-Darstellung und schreibt diese in die Datei Pix/xmllist.svg. Für Beispiel 4.3
sieht sie folgendermaßen aus:
125
()
[]
[]
()
()
John Mitchell []
[email protected]
[]
()
()
IG8 10 One
J38H 30 Two
()
Al Jones []
[]
I18F 100 [email protected] [email protected] ()
J38H1 10 One
()
J38H2 20 One
Richard Bird []
Sei G = (S, BS, R) eine CFG, X =
S
Mick Taylor []
BS und Z = {B ∈ BS | |B| = 1}.
Wort- und Ableitungsbaumalgebra
Neben TΣ(G) lassen sich auch die Menge der Wörter über X und die Menge der Ableitungsbäume von G zu Σ(G)-Algebren erweitern.
126
Word (G), die Wortalgebra von G
• Für alle s ∈ S, Word (G)s =def X ∗.
Word (G)
• Für alle w ∈ Z ∗ und r = (s → w) ∈ R, fr
() =def w.
• Für alle n > 0, w0 . . . wn ∈ Z ∗, e1, . . . , en ∈ S ∪ BS \ Z,
r = (s → w0e1w1 . . . enwn) ∈ R und (v1, . . . , vn) ∈ (X ∗)n,
frWord (G)(v1, . . . , vn) =def w0v1w1 . . . vnwn.
Beispiel 4.8 Nochmal die Regeln von SAB (siehe Beispiel 4.5):
r1 = S → aB, r2 = S → bA, r3 = S → ,
r4 = A → aS r5 = A → bAA, r6 = B → bS, r7 = B → aBB.
Alle drei Trägermengen der Wortalgebra Word (SAB) sind durch {a, b}∗ gegeben.
Die Konstruktoren von Σ(SAB) werden in Word (SAB) wie folgt interpretiert: Für alle
v, w ∈ {a, b}∗,
Word (SAB)
Word (SAB)
(w)
= f4
(w) = aw,
f1
Word (SAB)
Word (SAB)
f2
(w)
= f6
(w) = bw,
Word (SAB)
f3
= ,
Word (SAB)
f5
(v, w) = bvw,
Word (SAB)
f7
(v, w) = avw.
o
127
t ∈ TΣ(G) heißt G-Syntaxbaum für w ∈ X ∗, falls fold Word (G)(t) = w gilt.
G ist eindeutig, wenn fold Word (G) injektiv ist.
Die Sprache L(G) von G ist die Menge der Wörter über X, die sich aus der Faltung
eines Syntaxbaums in Word (G) ergeben:
L(G) =def fold Word (G)(TΣ(G)).
Z.B. ist L(REG) (siehe 4.1) die Menge aller Wörter über syms(BS) (siehe 2.14), die
reguläre Ausdrücke über BS darstellen, formal:
L(REG) = {w ∈ syms(BS)∗ | ∃ t ∈ TReg(BS) : fold Regword (BS)(t)(0) = w}.
Zwei Grammatiken G und G0 heißen äquivalent, wenn ihre Sprachen übereinstimmen.
Die Theorie formaler Sprachen liefert eine äquivalente Definition von L(G), die die oben
definierte Ableitungsrelation →G verwendet: Für alle s ∈ S,
+
L(G)s = {w ∈ BS ∗ | s →G w}.
128
javaToAlg "prog" 2 (siehe Java.hs) übersetzt das JavaLight-Programm in der Datei
prog in die Interpretation des zugehörigen Syntaxbaums in Word (JavaLight) und schreibt
diese in die Datei javasource. Die Inhalte von prog und javasource stimmen also miteinander überein!
Abl(G), die Ableitungsbaumalgebra von G
Für alle s ∈ S ist Abl(G) ist das kleinste (S ∪BS)-Tupel von Bäumen, deren innere Knoten
mit Sorten und deren Blätter mit Basismengen markiert sind, und die folgende Eigenschaft
haben:
• Für alle B ∈ BS, Abl(G)B = {B}.
• Für alle r = (s → (e1, . . . , en)) ∈ R und (t1, . . . , tn) ∈ Abl(G)e1 × . . . × Abl(G)en ,
s(t1, . . . , tn) ∈ Abl(G)s.
s(t1, . . . , tn) repräsentiert den Baum mit Wurzelmarkierung s und maximalen Unterbäumen
t1, . . . , tn.
Abl(G) interpretiert die Konstruktoren von Σ(G) wie folgt:
Abl(G)
• Für alle w ∈ Z ∗ und r = (s → w) ∈ R, fr
() =def s(w).
129
• Für alle n > 0, w0 . . . wn ∈ Z ∗, e1, . . . , en ∈ S ∪ BS \ Z,
r = (s → w0e1w1 . . . enwn) ∈ R und (t1, . . . , tn) ∈ Abl(G)e1 × . . . × Abl(G)en ,
frAbl(G)(t1, . . . , tn) =def s(w0, t1, w1, . . . , tn, wn).
Commands
Command
Commands
fact = Sum ;
Command
Prod Sumsect while Disjunct
Factor Prodsect
Command
Conjunct
1
Commands
Literal
Sum
>
Sum
Prod Sumsect
Factor Prodsect
x
Command
fact = Sum ;
Prod Sumsect
Factor Prodsect
1
Commands
Command
Prod Sumsect
Factor
fact
x = Sum ;
Prodsect
Prod
* Factor Prodsect Factor Prodsect
x
x
Sumsect
- Prod Sumsect
Factor Prodsect
1
Ableitungsbaum von fact = 1; while x > 1 {fact = fact*x; x = x-1;}
130
javaToAlg "prog" 3 (siehe Java.hs) übersetzt das JavaLight-Programm in der Datei
prog in den zugehörigen Ableitungsbaum und schreibt diesen in die Datei
Pix/javaderi.svg.
Beispiel 4.9 Zwei Modelle der Ausdrücke von JavaLight
Wir interpretieren hier zunächst nur die Sorten und Operationen von Σ(JavaLight), die mit
Ausdrücken zu tun haben, also alle außer Command und Commands. Der erste Modell
A = (A, Op) ignoriert darüberhinaus Programmvariablen (Strings), d.h. der Konstruktor
var : String → Factor bleibt uninterpretiert.
ASum = AProd = AFactor = Z
ASumsect = AProdsect = Z → Z
ADisjunct = AConjunct = ALiteral = 2
embedDA : AConjunct
embedLA : ALiteral
encloseS A : ASum
encloseDA : ADisjunct
embedI A : Z
embedB A : 2
x
→
→
→
→
→
→
7→
ADisjunct
AConjunct
AFactor
ALiteral
AFactor
ALiteral
x
131
sumA : AProd × ASumsect → ASum
prodA : AFactor × AProdsect → AProd
(x, f ) 7→ f (x)
sumsectA : {+, −} × AProd × ASumsect → ASumsect
prodsectA : {∗, /} × AFactor × AProdsect → AProdsect
(op, x, f ) 7→ λy.f (y op x)
nilS A : 1 → ASumsect
nilP A : 1 → AProdsect
7→ λx.x
disjunctA : AConjunct × ADisjunct → ADisjunct
(x, y) 7→ x ∨ y
conjunctA : ALiteral × AConjunct → AConjunct
(x, y) 7→ x ∧ y
notA : ALiteral → ALiteral
x 7→ ¬x
132
atomA : ASum × Rel × ASum → ALiteral
(x, rel, y) 7→ x rel y
Sei Store = String → Z (Menge der Belegungen von Programmvariablen durch ganze
Zahlen).
Das zweite Modell B = (B, Op) liftet jede Trägermenge M ∈ {Z, Z → Z, 2} von A
zur Funktionsmenge Store → M :
BSum = BProd = BFactor = Store → Z
BSumsect = BProdsect = Store → (Z → Z)
BDisjunct = BConjunct = BLiteral = Store → 2
B interpretiert embedD, embedL, encloseS und encloseD wie A als Identitäten.
varB : String → BFactor
x 7→ λstore.store(x)
Die restlichen Operationen von Σ(JavaLight) werden zustandsabhängig gemacht:
embedI B : Z → BFactor
embedB B : 2 → BLiteral
a 7→ λstore.a
133
sumB : BProd × BSumsect → BSum
prodB : BFactor × BProdsect → BProd
(f, g) 7→ λstore.g(store)(f (store))
sumsectB : {+, −} × BProd × BSumsect → BSumsect
prodsectB : {∗, /} × BFactor × AProdsect → BProdsect
(op, f, g) 7→ λstore.λx.g(store)(x op f (store))
nilS B : 1 → BSumsect
nilP B : 1 → BProdsect
7→ λstore.λx.x
disjunctB : BConjunct × BDisjunct → BDisjunct
(f, g) 7→ λstore.(f (store) ∨ g(store))
conjunctB : BLiteral × BConjunct → BConjunct
(f, g) 7→ λstore.(f (store) ∧ g(store))
notB : BLiteral → BLiteral
f 7→ λstore.¬f (store)
134
atomB : BSum × Rel × BSum → BLiteral
(f, rel, g) 7→ λstore.(f (store) rel g(store))
In Abschnitt 11.5 wird B als Teil der JavaLight-Algebra javaState in Haskell implementiert.
135
5 Parser und Compiler für CFGs
Sei G = (S, BS, R) eine CFG, X =
X übersetzt werden sollen.
S
BS und A eine Σ(G)-Algebra, in die Wörter über
In Anlehnung an den klassischen Begriff eines semantisch korrekten Compilers [22, 41] verlangen wir, dass die Semantiken Sem und Mach seiner Quell- bzw. Zielsprache in der durch
das folgende Funktionsdiagramm ausgedrückten Weise miteinander zusammenhängen:
TΣ(G)
fold Sem
g
Sem
fold A
(1)
encode
A
evaluate
g
Mach
Hierbei sind
• Sem die ebenfalls als Σ(G)-Algebra gegebene Semantik der Quellsprache L(G),
• Mach ein in der Regel unabhängig von Σ(G) definiertes Modell der Zielsprache, meist
in Form einer abstrakten Maschine,
• evaluate ein Interpreter, der Zielprogramme in der abstrakten Maschine Mach ausführt,
136
• encode eine Funktion, die Sem auf Mach abbildet und die gewünschte Arbeitsweise des
Compilers auf semantischer Ebene reflektiert.
Die Initialität der Termalgebra TΣ(G) erlaubt es uns, den Beweis der Kommutativität von
(1) auf die Erweiterung von encode und evaluate zu Σ(G)-Homomorphismen zu reduzieren. Dazu muss zunächst Mach zu einer Σ(G)-Algebra gemacht werden, was z.B. bedeuten
kann, die elementaren Funktionen der Zielsprache so in einer Signatur Σ0 zusammenzufassen, dass TΣ0 mit A übereinstimmt, jeder Konstruktor von Σ(G) einem Σ0-Term entspricht,
Sem eine Σ0-Algebra ist und evaluate Σ0-Terme in Sem faltet. Die Darstellung von Σ(G)Konstruktoren durch Σ0-Terme bestimmt dann möglicherweise eine Definition von encode,
die sowohl encode als auch evaluate Σ(G)-homomorph macht. So wurde z.B. in [41] die
Korrektheit eines Compilers gezeigt, der imperative Programme in Datenflussgraphen übersetzt.
Damit sind alle vier Abbildungen in Diagramm (1), also auch und die beiden Kompositionen
evaluate ◦ fold A und encode ◦ fold Mach Σ(G)-homomorph. Da TΣ(G) eine initiale Σ(G)Algebra ist, sind beide Kompositionen gleich. Also kommutiert (1).
Während fold A Syntaxbäume in der Zielsprache auswertet, ordnet fold Word (G) (siehe Kapitel 4) einem Syntaxbaum t das Wort der Quellsprache zu, aus dem ein Parser für G t
berechnen soll.
137
In der Theorie formaler Sprachen ist der Begriff Parser auf Entscheidungsalgorithmen beschränkt, die anstelle von Syntaxbäumen lediglich einen Booleschen Wert liefern, der angibt,
ob ein Eingabewort zur Sprache der jeweiligen Grammatik gehört oder nicht.
Demgegenüber definieren wir einen Parser für G als eine S-sortige Funktion
parseG : X ∗ → M (TΣ(G)),
die entweder Syntaxbäume oder, falls das Eingabewort nicht zur Sprache von G gehört,
Fehlermeldungen erzeugt. Welche Syntaxbäume bzw. Fehlermeldungen ausgegeben werden
sollen, wird durch eine Monade M festgelegt.
5.1 Funktoren und Monaden
Funktoren, natürlichen Transformationen und Monaden sind kategorientheoretische Grundbegriffe, die heutzutage jeder Softwaredesigner kennen sollte, da sie sich in den Konstruktionsund Transformationsmustern jedes denkbaren statischen, dynamischen oder hybriden Systemmodells wiederfinden. Die hier benötigten kategorientheoretischen Definitionen lauten
wie folgt:
Eine Kategorie K besteht aus
• einer – ebenfalls mit K bezeichneten – Klasse von K-Objekten,
• für alle A, B ∈ K einer Menge K(A, B) von K-Morphismen,
138
• einer assoziativen Komposition
◦ : K(A, B) × K(B, C) → K(A, C)
(f, g) 7−→ g ◦ f,
• einer Identität idA ∈ K(A, A), die bzgl. ◦ neutral ist, d.h. für alle B ∈ K und
f ∈ K(A, B) gilt f ◦ idA = f = idB ◦ f .
Im Kontext einer festen Kategorie K schreibt man meist f : A → B
anstelle von f ∈ K(A, B).
Wir haben hier mit vier Kategorien zu tun: Set, Set2 und SetS , deren Objekte alle Mengen,
alle Mengenpaare bzw. alle S-sortigen Mengen und deren Morphismen alle Funktionen, alle
Funktionspaare bzw. alle S-sortigen Mengen sind, sowie die Unterkategorie AlgΣ von SetS ,
deren Objekte alle Σ-Algebren und deren Morphismen alle Σ-Homomorphismen sind.
Seien K, L Kategorien. Ein Funktor F : K → L ist eine Funktion, die jedem K-Objekt
ein L-Objekt und jedem K-Morphismus f : A → B einen L-Morphismus
F (f ) : F (A) → F (B) zuordnet sowie folgende Gleichungen erfüllt:
• Für alle K-Objekte A, F (idA) = idF (A),
(2)
• Für alle K-Morphismen f : A → B and g : B → C, F (g ◦ f ) = F (g) ◦ F (f ).
(3)
139
Zwei Funktoren F : K → L und G : L → M kann man wie andere Funktionen sequentiell zu weiteren Funktoren komponieren: Für alle K-Objekte und -Morphismen A,
(GF )(A) =def G(F (A)).
Beispiele
Sei B ∈ L. Der konstante Funktor const(B) : K → L ordnet jedem K-Objekt das
L-Objekt B zu und jedem K-Morphismus die Identität auf B.
Der Identitätsfunktor Id K : K → K ordnet jedem K-Objekt und jedem K-Morphismus
sich selbst zu.
Der Diagonalfunktor ∆K : K → K2 ordnet jedem K-Objekt A das Objektpaar (A, A)
und jedem K-Morphismus f das Morphismenpaar (f, f ) zu.
Der Produktfunktor × : Set2 → Set ordnet jedem Mengenpaar (A, B) die Menge
A × B und jedem Funktionspaar (f : A → B, g : C → D) die Funktion f × g =def
λ(a, c).(f (a), g(c)) zu.
140
Der Listenfunktor Star : Set → Set ordnet jeder Menge A die Menge A∗ der Wörter
über A zu und jeder Funktion f : A → B die Funktion
Star (f ) : A∗ → B ∗
7→ (a1, . . . , an) 7→ (f (a1), . . . , f (an))
Der Mengenfunktor P : Set → Set ordnen jeder Menge A die Potenzmenge P(A) zu
und jeder Funktion f : A → B die Funktion
P(f ) : P(A) → P(B)
C 7→ {f (c) | c ∈ C}
Sei E eine Menge von Fehlermeldungen, Ausnahmewerten o.ä. Der Ausnahmefunktor
+ E : Set → Set ordnet jeder Menge A die Menge A + E zu (siehe Kapitel 2) und jeder
Funktion f : A → B die Funktion
f +E :A+E → B+E
(a, 1) 7→ (f (a), 1)
(e, 2) 7→ (e, 2)
Sei S eine Menge.
141
Der Potenz- oder Leserfunktor S : Set → Set über S ordnet jeder Menge A die
Menge S → A zu und jeder Funktion f : A → B die Funktion
f S : AS → B S
g 7→ f ◦ g
Der Copotenz- oder Schreiberfunktor × S : Set → Set über S ordnet jeder Menge
A die Menge A × S zu und jeder Funktion f : A → B die Funktion
f ×S :A×S → B×S
(a, s) 7→ (f (a), s)
Der Transitionsfunktor ( × S)S : Set → Set komponiert den Leser- mit dem Schreiberfunktor über S: Er ordnet also jeder Menge A die Menge (A×S)S zu und jeder Funktion
f : A → B die Funktion
(f × S)S : (A × S)S → (B × S)S
g 7→ λs.(f (π1(g(s))), π2(g(s)))
Aufgabe Zeigen Sie, dass die hier definierten Funktionen tatsächlich Funktoren sind, also
(2) und (3) erfüllen, und dass sie sich von Set auf SetS fortsetzen lassen.
o
142
Seien F, G : K → L Funktoren. Eine natürliche Transformation τ : F → G ordnet
jedem K-Objekt A einen L-Morphismus τA : F (A) → G(A) derart, dass für alle KMorphismen f : A → B folgendes Diagramm kommutiert:
τA
F (A)
G(A)
F (f )
g
F (B)
G(f )
τB
g
G(B)
Ein Funktor M : K → K heißt Monade, wenn es zwei natürliche Transformationen
η : Id K → M (Einheit) und µ : M ◦ M → M (Multiplikation) gibt, die für alle
A ∈ K das folgende Diagramm kommutativ machen:
M (A)
ηM (A)
M (ηA)
M (M (A)) ≺
M (A)
(4)
M (idA)
µA
g ≺
M (A)
(5)
M (idA)
M (M (M (A)))
M (µA)
g
M (M (A))
µM (A)
M (M (A))
(6)
µA
µA
g
M (A)
143
Beispiele
Viele der o.g. Funktoren sind Monaden. Einheit bzw. Multiplikation sind wie folgt definiert:
Seien A, E, S Mengen.
• Listenfunktor:
ηA : A → A∗
a 7→ (a)
µA : (A∗)∗ → A∗
(w1, . . . , wn) 7→ w1 · . . . · wn
• Mengenfunktor:
ηA : A → P(A)
a 7→ {a}
µA : P(P(A)) → P(A)
S
S 7→
S
• Ausnahmefunktor:
ηA : A → A + E
a 7→ (a, 1)
µA : (A + E) + E → A + E
((a, 1), 1) 7→ (a, 1)
((e, 2), 1) 7→ (e, 2)
(e, 2) 7→ (e, 2)
144
• Leserfunktor:
ηA : A → A S
µA : (AS )S → AS
a 7→ λs.a
f 7→ λs.f (s)(s)
• Schreiberfunktor: Hier muss S die Trägermenge eines Monoids M = (S, ∗, e) sein.
ηA : A → A × S
µA : (A × S) × S → A × S
a 7→ (a, e)
((a, s), s0) 7→ (a, s ∗ s0)
• Transitionsfunktor:
ηA : A → (A × S)S
µA : ((A × S)S × S)S → (A × S)S
a 7→ λs.(a, s)
f 7→ λs.π1(f (s))(π2(f (s)))
Aufgabe Zeigen Sie, dass für diese Beispiele (4)-(6) gilt.
o
Seien A und B Mengen. Der bind-Operator
= : M (A) × (A → M (B)) → M (B)
wird wie folgt aus M und der Multiplikation von M abgeleitet:
145
Für alle A ∈ K und f : A → M (B),
(= f ) = µB ◦ M (f ).
(7)
Intuitiv stellt man sich ein monadisches Objekt m ∈ M (A) als Berechnung vor, die eine
– evtl. leere – Menge von Werten in A erzeugt. Ein Ausdruck der Form m = f wird
dann wie folgt ausgewertet: Die von m berechneten Werte a ∈ A werden als Eingabe an
die Berechnung f übergeben und von f (a) verarbeitet.
Die Betrachtung der Elemente von M (A) als Berechnungen, die eine Ausgabe in A produzieren, lässt sich besonders gut am Beispiel der Transitionsmonade begründen.
Aus der Multiplikation der Transitionsmonade und der allgemeinen Definition des bindOperators ergibt sich nämlich die folgende Charakterisierung des bind-Operators der Transitionsmonade:
Für alle g : S → A × S und f : A → (S → B × S),
g = f = µB (M (f )(g)) = µB ((f × S)S (g)) = µB (λs.(f (π1(g(s))), π2(g(s))))
= λs.π1((λs.(f (π1(g(s))), π2(g(s))))(s))(π2((λs.(f (π1(g(s))), π2(g(s))))(s)))
= λs.π1(f (π1(g(s))), π2(g(s)))(π2(f (π1(g(s))), π2(g(s))))
= λs.f (π1(g(s)))(π2(g(s))).
146
Die Anwendung der Funktion (g = f ) : S → B ×S auf den Zustand s besteht demnach
• in der Anwendung von g auf s, die die Ausgabe a = π1(g(s)) und den Folgezustand
s0 = π2(g(s)) liefert,
• und der darauffolgenden Anwendung von f (a) auf s0.
Sei A, B, C Mengen, a ∈ A, m ∈ M (A), m0 ∈ M (M (A)), f : A → M (B), g : B → M (C)
und h : A → B. Aus (4)-(7) erhält man die folgenden Eigenschaften von =:
m = ηA
ηA(a) = f
(m = f ) = g
=
=
=
m,
f (a),
m = λa.f (a) = g.
(8)
(9)
(10)
(2)-(4) und (7) implizieren die folgenden Charakterisierungen von M (h) bzw. µA:
M (h)(m) = m = ηB ◦ h,
(11)
µA(m0) = m0 = idM (A).
Mit dem bind-Operator werden monadische Objekte sequentiell verknüpft. Plusmonaden
haben zusätzlich eine parallele Komposition, die – analog zu η und µ – für Monaden
M : Set → Set als natürliche Transformation ⊕ von M × M = × ◦ ∆ ◦ M nach M
definiert wird. Mit ⊕ lässt sich z.B. das Backtracking bzw. der Nichtdeterminismus eines
monadischen Compilers realisieren (siehe Kapitel 6).
147
Die Komposition von Monaden führt zu weiteren Monaden:
Sei M eine Monade mit Einheit η, Multiplikation µ und paralleler Komposition ⊕ und S
eine Menge.
• Lesermonade über (M, ⊕):
M 0 =def λA.M (A)S
Die Morphismenabbildung von M , η, µ und ⊕ werden wie folgt auf M 0 fortgesetzt: Sei
h : A → B.
M 0(h) : M 0(A) → M 0(B)
f 7→ M (h) ◦ f
ηA : A → M 0(A)
µA : M 0(M 0(A)) → M 0(A)
f 7→ λs.f (s) = λg.g(s)
⊕ : M 0(A) × M 0(A) → M 0(A)
(f, g) 7→ λs.f (s) ⊕ g(s)
148
• Transitionsmonade über (M, ⊕):
M 0 =def λA.M (A × S)S
Die Morphismenabbildung von M , η, µ und ⊕ werden wie folgt auf M 0 fortgesetzt:
Sei h : A → B.
M 0(h) : M 0(A) → M 0(B)
f 7→ λs.f (s) = λ(a, s).η(h(a), s)
ηA : A → M 0(A)
a 7→ λs.η(a, s)
µA : M 0(M 0(A)) → M 0(A)
f 7→ λs.f (s) = λ(g, s).g(s)
⊕ : M 0(A) × M 0(A) → M 0(A)
(f, g) 7→ λs.f (s) ⊕ g(s)
Die Implementierung von Monaden in Haskell wird in Kapitel 15 behandelt, monadische
Compiler in Kapitel 16. Letztere sind Transitionsmonaden über Ausnahmemonaden, wobei
S die Menge X ∗ der zu verarbeitenden Wörter ist.
Weitere Monadenbeispiele finden sich z.B. in [28], Kapitel 7.
149
Compilermonaden
Sei S eine Menge, M : SetS → SetS eine Monade mit Einheit η, bind-Operator =,
natürliche Transformationen ⊕ : M × M → M und set : M → P (wobei P hier den
Mengenfunktor für S-sortige Mengen bezeichnet) und
E = {m ∈ M (A) | setA(m) = ∅, A ∈ SetS }
(“Menge der Ausnahmewerte”).
M heißt Compilermonade, wenn für alle Mengen A, B, m, m0, m00 ∈ M (A), e ∈ E,
f : A → M (B), h : A → B und a ∈ A Folgendes gilt:
(m ⊕ m0) ⊕ m00 = m ⊕ (m0 ⊕ m00),
M (h)(e) = e,
M (h)(m ⊕ m0) = M (h)(m) ⊕ M (h)(m0),
setA(m ⊕ m0) = setA(m) ∪ setA(m0),
(CM 1)
(CM 2)
(CM 3)
setA(ηA(a)) = {a},
S
setB (m = f ) = {setB (f (a)) | a ∈ setA(m)}.
150
Nach Definition der Einheit η P und des bind-Operators =P der Mengenmonade P machen
die letzten beiden Gleichungen set zum Monadenmorphismus. Aus ihnen folgt nämlich:
setA ◦ ηA
=
ηAP ,
setB (m = f )
=
setA(m) =P setB ◦ f.
Außerdem erhält man für alle S-sortigen Mengen A, S-sortigen Funktionen h : A → B
und m ∈ M (A):
S
setB (M (h)(m)) = setB (m = ηB ◦ h) = {setB (ηB (h(a)) | a ∈ setA(m)}
S
= {{h(a)} | a ∈ setA(m)} = h(setA(m)).
Satz 5.2
Listen-, Mengen- und Ausnahmefunktoren sind Compilermonaden.
Beweis. Sei A eine Menge, ⊕ = · und setA : A∗ → P(A) definiert durch setA() = ∅ und
setA(s) = {a1, . . . , an} für alle s = (a1, . . . , an) ∈ A+. Damit ist Star eine Compilermonade.
Sei A eine Menge, ⊕ = ∪ und setA : P(A) → P(A) definiert durch idP(A). Damit ist P
eine Compilermonade.
151
Seien A und E disjunkte Mengen, ⊕ = λ(m, m0).m und setA : A + E → P(A) definiert
durch setA(e) = ∅ für alle e ∈ E und setA(a) = {a} für alle a ∈ A. Damit ist λA.A + E
eine Compilermonade.
o
Monadenbasierte Parser und Compiler
S
Sei G = (S, BS, R) eine CFG, X = BS, Σ(G) = (S, I, F ) und M : SetS → SetS eine
Compilermonade.
Wie bereits in Kapitel 1 informell beschrieben wurde, soll ein Compiler für G in die als
Σ(G)-Algebra A formulierten Zielsprache einen Parser für G mit der Faltung in A der vom
Parser erzeugten Syntaxbäume komponieren:
compileA
G
∗ parseG
M (fold A )
= X −→ M (TΣ(G)) −→ M (A).
(12)
152
Gilt (12), dann überträgt sich die Kommutativität von Diagramm (1) wie folgt auf die
Kommutativität des folgenden Diagramms:
X
compileA
G
∗
compileSem
G
M (A)
M (evaluate)
(13)
g
M (Sem)
M (encode)
g
M (Mach)
(12)
A
M (evaluate) ◦ compileA
G = M (evaluate) ◦ M (fold ) ◦ parseG
(1)
M ist Funktor
=
M (evaluate ◦ fold A) ◦ parseG = M (encode ◦ fold Sem ) ◦ parseG
M ist Funktor
M (encode) ◦ M (fold Sem ) ◦ parseG = M (encode) ◦ compileSem
G .
=
(12)
Da TΣ(G) initial ist, stimmt fold TΣ(G) mit der Identität auf TΣ(G) überein. Also gilt:
parseG = idM (TΣ(G)) ◦ parseG = M (idTΣ(G) ) ◦ parseG = M (fold TΣ(G) ) ◦ parseG
(12)
T
= compileGΣ(G) .
153
Da TΣ(G) eine Σ(G)-Algebra und fold A Σ(G)-homomorph ist, ist (12) ein Spezialfall folgender Bedingung:
Für alle Σ(G)-Homomorphismen h : A → B,
B
M (h) ◦ compileA
G = compileG .
(14)
T
Sei parseG = compileGΣ(G) und unparseG =def fold Word (G).
Die Korrektheit von parseG lässt sich wie folgt formalisieren: Für alle w ∈ X ∗,
P(unparseG)(setTΣ(G) (parseG(w))) ⊆ {w},
(15)
in Worten: Die Faltung jedes von parseG(w) berechneten Syntaxbaums in der Algebra
Word (G) stimmt mit w überein. Wegen
P(unparseG) ◦ setTΣ(G) ◦ parseG = setWord (G) ◦ M (unparseG) ◦ parseG
Word (G)
= setWord (G) ◦ compileG
ist (15) äquivalent zu
Word (G)
setWord (G)(compileG
(w)) ⊆ {w}.
(16)
(15) erlaubt die Leerheit von setTΣ(G) (parseG(w)). Der Parser parseG ist vollständig,
wenn er jedes Wort w ∈ L(G) in mindestens einen Syntaxbaum für w übersetzt, d.h. er
erfüllt folgende Bedingung:
Word (G)
∀ w ∈ L(G) : setWord (G)(compileG
(w)) 6= ∅.
(17)
154
Zusammenfassend ergibt sich folgende Definition:
Ein generischer Compiler für G ist eine AlgΣ(G)-sortige Menge
∗
compileG = (compileA
G : X → M (A))A∈AlgΣ(G) ,
die (14), (16) und (17) erfüllt.
Sei W = Word (G). (16) und (17) machen setW ◦ compileW
G zu der Summenextension
(siehe Kapitel 2)
[λw.{w}, λw.∅] : L(G) + X ∗ \ L(G) → P(X ∗).
L(G)
incL(G)
X∗ ≺
incX ∗\L(G)
X ∗ \ L(G)
setW ◦ compileW
G
λw.{w}
λw.∅
g ≺
P(X ∗)
U.a. ist dann die Einschränkung von parseG auf L(G) rechtsinvers zu unparseG:
setW ◦ M (unparseG) ◦ parseG ◦ incL(G) = setW ◦ M (fold W ) ◦ parseG ◦ incL(G)
(12)
= setW ◦ compileW
G ◦ incL(G) = λw.{w}.
155
Schließlich gilt (14) genau dann, wenn
compileG : const(X ∗) → M ◦ U
eine natürliche Transformation ist, wobei
const(X ∗), U : AlgΣ(G) → SetS
die Funktoren bezeichnen, die
• jeder Σ(G)-Algebra A die S-sortige Menge (X ∗)s∈S bzw. die Trägermenge von A und
• jedem Σ(G)-Homorphismus h die S-sortige Funktion (idX ∗ )s∈S bzw. die h zugrundeliegende S-sortige Funktion
zuordnen.
156
6 LL-Compiler
Der in diesem Kapitel definierte generische Compiler überträgt die Arbeitsweise eines klassischen LL-Parsers auf Compiler mit Backtracking. Das erste L steht für seine Verarbeitung
des Eingabewortes von links nach rechts, das zweite L für seine – implizite – Konstruktion
einer Linksableitung. Da diese Arbeitsweise dem mit der Wurzel beginnenden schrittweisen Aufbau eines Syntaxbaums entspricht, werden LL-Parser auch top-down-Parser
genannt.
S
Sei G = (S, BS, R) eine nicht-linksrekursive CFG, X = BS, Z = {B ∈ BS | |B| = 1},
M eine Compilermonade, errmsg : X ∗ → E, A eine Σ(G)-Algebra und s ∈ S. compileG
heißt LL-Compiler, wenn
∗
compileA
G,s : X → M (As )
wie folgt definiert ist: Für alle w ∈ X ∗,
A
(1)
compileA
G,s (w) = transs (w) = λ(a, w).if w = then ηA (a) else errmsg(w),
wobei für alle für alle s ∈ S ∪ BS
∗
∗
transA
s : X → M (As × X )
wie folgt definiert ist:
157
Fall 1: s ∈ BS. Für alle x ∈ X und w ∈ X ∗,
transA
s (xw) = if x ∈ s then ηA×X ∗ (x, w) else errmsg(xw)
transA
s ()
= errmsg()
Fall 2: s ∈ S. Für alle w ∈ X ∗,
transA
s (w)
=
M
tryrA(w).
(2)
r=(s→e)∈R
Für alle r = (s → (e1, . . . , en)) ∈ R und w ∈ X ∗,


transA

e1 (w) = λ(a1 , w1 ).



 transA (w ) = λ(a , w ).
1
2
2
e2
tryrA(w) =
...





 transA (w ) = λ(a , w ).η
A
n−1
n
n
A×X ∗ (fr (ai1 , . . . , aik ), wn ),
en
(3)
wobei {i1, . . . , ik } = {1 ≤ i ≤ n | ei ∈ S ∪ BS \ Z}.
Während alle Summanden von (2) auf das gesamte Eingabewort w angewendet werden,
wird es von tryrA Symbol für Symbol verarbeitet: Zunächst wird transA
e1 auf w angewendet,
A
A
dann transA
e2 auf das von transe1 nicht verarbeitete Suffix w1 von w, transe3 auf das von
transA
e2 nicht verarbeitete Suffix w2 von w1 , usw.
158
Gibt es 1 ≤ j ≤ n derart, dass transA
ej (w) scheitert, d.h. existiert kein aus ej ableitbares
Präfix von w, dann übergibt tryrA das gesamte Eingabewort zur Parsierung an den nächsten
Teilcompiler tryrA0 der Argumentliste von ⊕ in (2) (Backtracking).
A
Ist transA
ei (w) jedoch für alle 1 ≤ i ≤ n erfolgreich, dann schließt der Aufruf von tryr mit
der Anwendung von frA auf die Zwischenergebnisse ai1 , . . . , aik ∈ A.
Klassische LL-Parser sind deterministisch, d.h. sie erlauben kein Backtracking, sondern
setzen voraus, dass die ersten k Symbole des Eingabewortes w bestimmen, welcher der
A
Teilcompiler trys→e
aufgerufen werden muss, um zu erkennen, dass w zu L(G)s gehört.
CFGs, die diese Voraussetzung erfüllen, heißen LL(k)-Grammatiken.
Im Gegensatz zur Nicht-Linksrekursivität lässt sich die LL(k)-Eigenschaft nicht immer
durch eine Transformation der Grammatik erzwingen, auch dann nicht, wenn sie eine von
einem deterministischen Kellerautomaten erkennbare Sprache erzeugt!
Monadische Ausdrücke werden in Haskell oft in der do-Notation wiedergegeben. Sie verdeutlicht die Korrespondenz zwischen monadischen Berechnungen einerseits und imperativen Programmen andererseits. Die Rückübersetzung der do-Notation in die ursprünglichen
monadischen Ausdrücke ist induktiv wie folgt definiert:
159
Sei m ∈ M (A), a ∈ A und m0 ∈ M (B).
do m; a ← m0 = m = λa. do m0
do m; m0
= m do m0
Gleichung (10) von Kapitel 5 impliziert die Assoziativität des Sequentialisierungsoperators
(;), d.h. do m; m0; m00 ist gleichbedeutend mit do (do m; m0); m00 und do m; (do m0; m00).
Beispiel 6.1 SAB (siehe Beispiel 4.5)
Die Regeln
r1 = S → aB,
r2 = S → bA,
r3 = S → ,
r4 = A → aS,
r5 = A → bAA,
r6 = B → bS,
r7 = B → aBB
von SAB liefern nach obigem Schema die folgende Transitionsfunktion des LL-Compilers in
eine beliebige SAB-Algebra alg:
Für alle x, z ∈ {a, b} und w ∈ {a, b}∗,
trans_z(xw) = if x = z then η(z,w) else errmsg(xw)
trans_z() = errmsg()
160
trans_S(w) = try_r1(w) ⊕ try_r2(w) ⊕ try_r3(w)
trans_A(w) = try_r4(w) ⊕ try_r5(w)
trans_B(w) = try_r6(w) ⊕ try_r7(w)
try_r1(w)
try_r2(w)
try_r3(w)
try_r4(w)
try_r5(w)
try_r6(w)
try_r7(w)
= do (x,w) <- trans_a(w);
(c,w) <- trans_B(w);
= do (x,w) <- trans_b(w);
(c,w) <- trans_A(w);
= η(f_r3^alg,w)
= do (x,w) <- trans_a(w);
(c,w) <- trans_S(w);
= do (x,w) <- trans_b(w);
(c,w) <- trans_A(w);
(d,w) <- trans_A(w);
= do (x,w) <- trans_b(w);
(c,w) <- trans_S(w);
= do (x,w) <- trans_a(w);
(c,w) <- trans_B(w);
(d,w) <- trans_B(w);
η(f_r1^alg(c),w)
η(f_r2^alg(c),w)
η(f_r4^alg(c),w)
η(f_r5^alg(c,d),w)
η(f_r6^alg(c),w)
η(f_r7^alg(c,d),w)
o
161
Sei W = Word (G). Um später zeigen zu können, dass der LL-Compiler vollständig ist,
setzen wir voraus, dass
• für alle s ∈ S, v ∈ L(G)s und w ∈ X ∗
r = (s → (e1, . . . , en)) ∈ R
und für alle 1 ≤ i ≤ n vi ∈ L(G)ei mit v1 . . . vn = v und
setW 2 (tryrW (w)) ⊆ setW 2 (transW
s (w))
existieren.
(LLC )
Gibt es keine Anordnung der Regeln in (2), die diese Bedingung erfüllt, dann müssen ggf.
neue Sorten für die Vorkommen einer Sorte in bestimmten Kontexten eingeführt und die
Regeln von R entsprechend modifiziert werden.
Beispiel 6.2
Ein solcher Fall würde z.B. in JavaLight+ auftreten (siehe Kapitel 13), wenn wir dort
anstelle der drei Sorten ExpSemi , ExpBrac und ExpComm die zunächst naheliegende eine
Sorte Exp und die folgenden Regeln verwenden würden:
162
Command
Exp
Sum
Sumsect
Prod
Prodsect
Factor
Disjunct
Conjunct
Literal
Actuals
Actuals0
→
→
→
→
→
→
→
→
→
→
→
→
String = Exp; | write Exp; | . . .
Sum | Disjunct
Prod Sumsect
... | Factor Prodsect
... | String | String Actuals | . . .
Conjunct | . . .
Literal | . . .
String | String Actuals | . . .
() | (Actuals0
Exp) | Exp, Actuals0
(A)
(B)
(C)
Ein LL-Compiler für Command würde z.B. die Eingabe z = x<=11; als syntaktisch inkorrekt betrachten, weil der Teilcompiler für Exp zunächst nach dem längsten aus Sum
ableitbaren Präfix von x<=11 sucht, x als solches erkennt und deshalb das Wort x<=11
nicht bis zum Ende liest.
Die Ersetzung von Regel (B) durch Exp → Disjunct|Sum löst das Problem nicht, denn
nun würde der Compiler z.B. die Eingabe z = x+11; als syntaktisch inkorrekt betrachten,
weil der Teilcompiler für Exp zunächst nach dem längsten aus Disjunct ableitbaren Präfix
163
von x+11 sucht, x als solches erkennt und deshalb das Wort x+11 nicht bis zum Ende liest.
Ersetzt man hingegen (A)-(C) durch
Command
ExpSemi
ExpBrac
ExpComm
Actuals0
→
→
→
→
→
String = ExpSemi | write ExpSemi | . . .
Sum; | Disjunct;
Sum) | Disjunct)
Sum, | Disjunct,
ExpBrac | ExpComm Actuals0
dann wird alles gut: Jetzt erzwingt das auf Sum oder Disjunct folgende Terminal (Semikolon, schließende Klammer bzw. Komma), dass die Compiler für ExpSemi , ExpBrac und
ExpComm die jeweilige Resteingabe stets bis zu diesem Terminal verarbeiten.
Natürlich genügt ein Compiler für alle drei Sorten. Man muss ihn lediglich mit dem jeweiligen Terminal parametrisieren (siehe Java2.hs).
o
Satz 6.4 transA ist wohldefiniert.
Beweis durch Noethersche Induktion. Da S endlich und G nicht linksrekursiv ist, ist
die folgende Ordnung > auf S ∪ BS Noethersch, d.h., es gibt keine unendlichen Ketten
s1 > s2 > s3 > · · · :
s > s0
⇔def
+
∃ w ∈ (S ∪ BS)∗ : s →G s0w.
164
Wir erweitern > zu einer ebenfalls Noetherschen Ordnung auf X ∗ × (S ∪ BS):
(v, s) (w, s0)
⇔def
|v| > |w| oder (|v| = |w| und s > s0).
Nach Definition von transs(w) gibt es r = (s → e1 . . . en) ∈ R mit
A
A
A
transA
s (w) = . . . transe1 (w) . . . transe2 (w1 ) . . . transen (wn−1 ) . . .
Da w1, . . . , wn echte Suffixe von w sind, gilt s > e1 und |w| > |wi| für alle 1 ≤ i < n,
also (w, s) (w, e1) und (w, s) (wi−1, ei) für alle 1 < i ≤ n. Die rekursiven Aufrufe von
transA haben also bzgl. kleinere Argumente.
Folglich terminiert jeder Aufruf von transA, m.a.W.: transA ist wohldefiniert.
o
Der Beweis, dass compileG Gleichung (15) von Kapitel 5 erfüllt, verwendet die folgenden
Zusammenhänge zwischen einer Monade M und ihrem bind-Operator:
Lemma 6.5
Sei M : SetS → SetS eine Monade und h : A → B eine S-sortige Funktion. Für alle
m ∈ M (A), f : A → M (A) und g : B → M (B),
M (h)(m = f ) = m = M (h) ◦ f,
M (h)(m) = g = m = g ◦ h.
(4)
(5)
165
Sei Σ eine Signatur, m1, . . . , mn ∈ M (A), h : A → B Σ-homomorph, für alle 1 ≤ i ≤ n,
fi : A → · · · → A → M (A),
gi : B → · · · → B → M (B),
und t ∈ TΣ(V ) mit var(t) = {x1, . . . , xn},
fi(a1) . . . (ai−1) = mi fi+1(a1) . . . (ai−1),
fn+1(a1) . . . (an) = ηA(fa∗(t))
für alle a = (a1, . . . , an) ∈ A und
gi(b1) . . . (bi−1) = M (h)(mi) gi+1(b1) . . . (bi−1),
gn+1(b1) . . . (bn) = ηB (gb∗(t))
für alle b = (b1, . . . , bn) ∈ B, wobei fa : V → A und gb : V → B durch fa(xi) = ai und
gb(xi) = bi für alle 1 ≤ i ≤ n definiert sind. Dann gilt
M (h)(f1) = g1.
(6)
Beweis von (4).
M (h)(m = f )
(10) in Kap. 5
=
=
(m = f ) = ηB ◦ h
m = λa.f (a) = ηB ◦ h
(11) in Kap. 5
=
(11) in Kap. 5
m = λa.M (h)(f (a)) = m = M (h) ◦ f.
166
Beweis von (5).
M (h)(m) = g = (m = ηB ◦ h) = g
(9) in Kap. 5
=
(10) in Kap. 5
=
m = λa.ηB (h(a)) = g
m = λa.g(h(a)) = m = g ◦ h.
Beweis von (6).
(4)
M (h)(f1) = M (h)(m1 f2) = m1 = M (h) ◦ f2 = m1 = λa1.M (h)(f2(a1))
(4)
= m1 = λa1.M (h)(m2 = f3(a1)) = m1 = λa1.m2 = M (h) ◦ f3(a1)
= m1 = λa1.m2 = λa2.M (h)(f3(a1)(a2))
= · · · = m1 = λa1.m2 = · · · = λan.M (h)(fn+1(a1) . . . (an))
= m1 = λa1.m2 = · · · = λan.M (h)(ηA(fa∗(t)))
= m1 = λa1.m2 = · · · = λan.ηB (h(fa∗(t)))
= m1 = λa1.m2 = · · · = λan.ηB ((h ◦ fa)∗(t))
h◦fa =gh(a)
=
∗
m1 = λa1.m2 = · · · = λan.ηB (gh(a)
(t))
(7)
167
(5)
g1 = M (h)(m1) = g2 = m1 = g2 ◦ h = m1 = λa1.g2(h(a1))
= m1 = λa1.M (h)(m2) = g3(h(a1)) = m1 = λa1.m2 = g3(h(a1)) ◦ h
= m1 = λa1.m2 = λa2.g3(h(a1))(h(a2))
= · · · = m1 = λa1.m2 = · · · = λan.gn+1(h(a1)) . . . , (h(an))
∗
= m1 = λa1.m2 = · · · = λan.ηB (gh(a)
(t))
(8)
o
Aus (7) und (8) folgt (6).
Satz 6.6
Sei h : A → B ein Σ(G)-Homomorphismus. Der oben definierte LL-Compiler erfüllt Gleichung (14) aus Kapitel 5, also
A
compileB
G = M (h) ◦ compileG .
(9)
Beweis. Sei
transB = M (h × X ∗) ◦ transA.
(10)
Dann gilt (9): Für alle w ∈ X ∗,
(1)
B
compileB
G (w) = trans (w) = λ(b, w).if w = then ηB (b) else errmsg(w)
168
(10)
= M (h × X ∗)(transA(w)) = λ(b, w).if w = then ηB (b) else errmsg(w)
(11) in Kap. 5
=
(transA(w) = (ηB×X ∗ ◦ (h × X ∗)))
= λ(b, w).if w = then ηB (b) else errmsg(w)
= (transA(w) = λ(a, w).ηB×X ∗ (h(a), w))
= λ(b, w).if w = then ηB (b) else errmsg(w)
(10) in Kap. 5
=
transA(w) = λ(a, w).ηB×X ∗ (h(a), w)
= λ(b, w).if w = then ηB (b) else errmsg(w)
(9) in Kap. 5
=
transA(w) = λ(a, w).if w = then ηB (h(a)) else errmsg(w)
(CM 1)
= transA(w) = λ(a, w).if w = then ηB (h(a)) else M (h)(errmsg(w))
(9),(11) in Kap. 5
=
transA(w) = λ(a, w).if w = then ηA(a) = ηB ◦ h
else errmsg(w) = ηB ◦ h
= transA(w) = λ(a, w).(if w = then ηA(a) else errmsg(w)) = ηB ◦ h
(10) in Kap. 5
=
(transA(w) = λ(a, w).if w = then ηA(a) else errmsg(w)) = ηB ◦ h
= compileA
G (w) = ηB ◦ h
(11) in Kap. 5
=
M (h)(compileA
G (w)).
169
Beweis von (10) durch Noethersche Induktion bzgl. der im Beweis von Satz 6.4 definierten
Ordnung :
Fall 1: s ∈ BS. Für alle x ∈ X und w ∈ X ∗,
∗
M (h × X ∗)(transA
s (xw)) = if x ∈ s then M (h × X )(ηA×X ∗ (x, w))
else M (h × X ∗)(errmsg(xw))
η ist nat. Transformation, (CM 1 )
=
(h×X ∗ )(x,w)=(h(x),w)=(x,w)
=
if x ∈ s then ηB×X ∗ ((h × X ∗)(x, w)) else errmsg(xw)
if x ∈ s then ηB×X ∗ (x, w) else errmsg(xw) = transB
s (xw),
(CM 1)
∗
B
M (h × X ∗)(transA
s ()) = M (h × X )(errmsg()) = errmsg() = transs ().
Fall 2: s ∈ S. Sei r = (s → w0) ∈ R. Für alle w ∈ X ∗,
M (h × X ∗)(tryrA(w))


A





 transe1 (w) = λ(a1, w1).
(3)
.
)
= M (h × X ∗)( ..





 transA (w ) = λ(a , w ).η
A
∗
(f
(a
,
.
.
.
,
a
),
w
)
n−1
n
n
A×X
j
j
n
en
r
1
k


A




 transe1 (w) = λx1.

.
∗
= M (h × X )( ..
)




 transA (w ) = λx .η

A
∗ (f (π (x ), . . . , π (x )), π (x ))
en
n−1
n
A×X
r
1
j1
1
jk
2
n
170




M (h × X ∗)(transA


e1 (w)) = λx1 .






.
 ..

6.5 (6)
=
)
∗
A


M
(h
×
X
)(trans
(w
))


n−1
en







B
∗
= λxn.ηB×X (fr (π1(xj1 ), . . . , π1(xjk )), π2(xn)) 


∗
A




 M (h × X )(transe1 (w)) = λ(b1, w1).

.
= ..




 M (h × X ∗)(transA (w )) = λ(b , w ).η

B
∗
(f
(b
,
.
.
.
,
b
),
w
)
n−1
n
n
B×X
j1
jk
n
r
en


B





 transe1 (w) = λ(b1, w1).
ind. hyp.
.
..
=




 transB (w ) = λ(b , w ).η

B
∗ (f (b , . . . , b ), w )
en
n−1
n
n
B×X
r
j1
jk
(3)
= tryrB (w).
n
(11)
Daraus folgt (10):
(2)
∗
A
M (h × X ∗)(transA
s (w)) = M (h × X )(⊕r=(s→e)∈R tryr (w))
(CM 2)
(11)
(2)
= ⊕r=(s→e)∈R M (h × X ∗)(tryrA(w)) = ⊕r=(s→e)∈R tryrB (w) = transB
s (w). o
171
Lemma 6.7
Sei M : SetS → SetS eine Compilermonade, f : AN → A und m1, . . . , mn ∈ M (A).
setA(m1 = λa1. . . . mn = λan.ηA(f (a1, . . . , an)))
V
= {f (a1, . . . , an) | ni=1 ai ∈ setA(mi)}.
Beweis.
setA(m1 = λa1. . . . mn = λan.ηA(f (a1, . . . , an)))
S
= {setA(m2 = λa2. . . . mn = λan.ηA(f (a1, . . . , an))) | a1 ∈ setA(m1)}
S S
= { {setA(m3 = λa3. . . . mn = λan.ηA(f (a1, . . . , an))) | a2 ∈ setA(m2)}
| a1 ∈ setA(m1)}
=
S
{setA(m3 = λa3. . . . mn = λan.ηA(f (a1, . . . , an)))
| a2 ∈ setA(m2), a1 ∈ setA(m1)}
= ...
S
V
= {setA(ηA(f (a1, . . . , an))) | ni=1 ai ∈ setA(mi)}
V
S
= {{f (a1, . . . , an)} | ni=1 ai ∈ setA(mi)}
V
= {f (a1, . . . , an) | ni=1 ai ∈ setA(mi)}.
o
172
Satz 6.8
Der oben definierte LL-Compiler ist korrekt, d.h. für alle w ∈ X ∗ gilt:
Word (G)
setWord (G)(compileG
(w)) ⊆ {w}
(12)
(siehe (16) in Kapitel 5).
Beweis. Sei W = Word (G). Für alle w ∈ X ∗ und s ∈ S ∪ BS gelte folgende Bedingung:
0
∀ (v, v 0) ∈ setW 2 (transW
s (w)) : vv = w.
(13)
Dann gilt auch (14):
setW (compileW
G,s (w))
(1)
0
0
0
= setW (transW
s (w) = λ(v, v ).if v = then ηW (v) else errmsg(v ))
S
= {setW (if v 0 = then ηW (v) else errmsg(v 0)) | (v, v 0) ∈ setW 2 (transW
s (w))}
(13) S
⊆ {setW (if v 0 = then ηW (v) else errmsg(v 0)) | vv 0 = w}
S
= {if v 0 = then setW (ηW (v)) else setW (errmsg(v 0)) | vv 0 = w}
S
= {if v 0 = then {v} else ∅ | vv 0 = w} = {w}.
Beweis von (15) durch Noethersche Induktion bzgl. der im Beweis von Satz 6.4 definierten Ordnung .
173
Fall 1: s ∈ BS. Sei x ∈ X, w ∈ X ∗ und (v, v 0) ∈ setW 2 (transW
s (xw)). Dann gilt
(v, v 0) ∈ setW 2 (if x ∈ s then ηW 2 (x, w) else errmsg(xw))
= if x ∈ s then setW 2 (ηW 2 (x, w)) else setW 2 (errmsg(xw))
= if x ∈ s then {(x, w)} else ∅,
also vv 0 = xw.
Fall 2: s ∈ S. Sei w ∈ X ∗ und (v, v 0) ∈ setW 2 (transW
s (w)). Dann gilt
L
(2)
W
(v, v 0) ∈ setW 2 (transW
(w)))
=
set
(
2
W
s
r=(s→e)∈R tryr (w))
(CM 3) S
W
⊆
r=(s→e)∈R setW 2 (tryr (w)).
Also gibt es r = (s → e1 . . . en) ∈ R mit (v, v 0) ∈ setW 2 (tryrW (w)). Sei w0 = w. Dann gilt
(v, v 0) ∈ setW 2 (tryrW (w0))
(3)
= setW 2 (transW
e1 (w0 ) = λ(v1 , w1 ).
...
W
transW
en (wn−1 ) = λ(vn , wn ).ηW 2 (fr (vj1 , . . . , vjk ), wn ))
V
Lemma 6 .7
=
{(frW (vj1 , . . . , vjk ), wn) | ni=1(vi, wi) ∈ setW 2 (transW
ei (wi−1 ))}
V
= {(v1 . . . vn, wn) | ni=1(vi, wi) ∈ setW 2 (transW
(14)
ei (wi−1 ))}.
174
V
Also gibt es v1, . . . , vn, w1, . . . , wn ∈ X ∗ mit ni=1(vi, wi) ∈ setW 2 (transW
ei (wi−1 )),
V
n
v = v1 . . . vn und v 0 = wn. Nach Induktionsvoraussetzung folgt i=1 viwi = wi−1, also
vv 0 = v1 . . . vnwn = w0 = w.
o
Satz 6.9 Der oben definierte LL-Compiler ist vollständig, d.h. für alle w ∈ L(G) gilt:
Word (G)
setW (compileG
(w)) 6= ∅.
(15)
Beweis. Sei W = Word (G). Für alle s ∈ S ∪ BS, v ∈ L(G)s und w ∈ X ∗ gelte:
(v, w) ∈ setW 2 (transW
s (vw)).
(16)
Dann gilt auch (15): Sei w ∈ L(G).
Word (G)
setW (compileG,s
(w))
(1)
0
0
0
= setW (transW
s (w) = λ(v, v ).if v = then ηW (v) else errmsg(v ))
S
= {setW (if v 0 = then ηW (v) else errmsg(v 0)) | (v, v 0) ∈ setW 2 (transW
s (w))}
S
= {setW (ηW (v)) | (v, ) ∈ setW 2 (transW
s (w))} ∪
S
0
{setW (errmsg(v 0)) | (v, v 0) ∈ setW 2 (transW
s (w)), v 6= }
S
S
0
= {{v} | (v, ) ∈ setW 2 (transW
(w))}
∪
{∅ | (v, v 0) ∈ setW 2 (transW
s
s (w)), v 6= }
= {v | (v, ) ∈
setW 2 (transW
s (w))}
(16)
6= ∅.
175
Beweis von (16) durch Noethersche Induktion bzgl. der im Beweis von Satz 6.4 definierten Ordnung .
Fall 1: s ∈ BS. Dann ist v ∈ X und damit
setW 2 (transW
s (vw)) = setW 2 (ηW 2 (v, w)) = {(v, w)}.
Fall 2: s ∈ S. Wegen (LLC ) gibt es r = (s → (e1, . . . , en)) ∈ R und für alle 1 ≤ i ≤ n
vi ∈ L(G)ei mit v1 . . . vn = v und
setW 2 (tryrW (w)) ⊆ setW 2 (transW
s (w)).
(17)
Nach Induktionsvoraussetzung folgt
n
^
(vi, vi+1 . . . vnw) ∈ setW 2 (transW
ei (vi . . . vn w)).
(18)
i=1
Mit wi =def vi+1 . . . vnw für alle 0 ≤ i ≤ n ist (18) äquivalent zu:
n
^
(vi, wi) ∈ setW 2 (transW
ei (wi−1 )).
(19)
i=1
Aus (14), (17) und (19) folgt schließlich
(v, w) = (v1 . . . vn, wn) ∈ setW 2 (tryrW (w0)) = setW 2 (tryrW (w)) ⊆ setW 2 (transW
s (w)). o
176
7 LR-Compiler
LR-Compiler lesen ein Wort w wie LL-Compiler von links nach rechts, konstruieren dabei
jedoch eine Rechtsreduktion von w, d.h. die Umkehrung einer Rechtsableitung. Da dies
dem schrittweisen, mit den Blättern beginnenden Aufbau eines Syntaxbaums entspricht,
werden LR-Compiler auch bottom-up-Compiler genannt.
Im Gegensatz zum LL-Compiler ist die entsprechende Übersetzungsfunktion eines LRCompilers iterativ. Die Umkehrung der Ableitungsrichtung (Reduktion statt Ableitung)
macht es möglich, die Compilation durch einen Automaten, also eine iterativ definierte
Funktion, zu steuern.
Während LL-Compiler keine linksrekursiven CFGs verarbeiten können, sind LR-Compiler
auf LR(k)-Grammatiken beschränkt (s.u.).
Sei G = (S, BS, R) eine CFG und X =
S
BS.
Wir setzen jetzt voraus, dass S das Symbol start enthält und dieses nicht auf der rechten
Seite einer Regel von R auftritt.
177
Ablauf der LR-Übersetzung auf Ableitungsbäumen
start
φ
ε
vw
Ableitungsbäume Eingabe
v
w
Ableitungsbäume Eingabe
vw
ε
Ableitungsbaum Eingabe
G heißt LR(k)-Grammatik, falls das Vorauslesen von k noch nicht verarbeiteten Eingabesymbolen genügt, um zu entscheiden, ob ein weiteres Zeichen gelesen oder eine Reduktion
durchgeführt werden muss, und, wenn ja, welche. Außerdem müssen die Basismengen von
BS paarweise disjunkt sein.
Beispiel 7.1 Die CFG G = ({S, A}, {∗, b}, {S → A, A → A ∗ A, A → b}) ist für kein
k eine LR(k)-Grammatik.
178
Nach der Reduktion des Präfixes b ∗ b des Eingabewortes b ∗ b ∗ b zu A ∗ A liegt ein shiftreduce-Konflikt vor. Soll man A ∗ A mit der Regel A → A ∗ A zu A reduzieren oder
erst die Resteingabe ∗b lesen und dann b mit A → b zu A reduzieren? Die Resteingabe
determiniert liefert keine Antwort.
o
first- und follow-Wortmengen
Sei CS = BS ∪ Z, k > 0, α ∈ (S ∪ BS)∗ und s ∈ S.
∗
∗
first k (α) = {β ∈ CS k | ∃ γ ∈ CS ∗ : α →G βγ} ∪ {β ∈ CS <k | α →G β}
∗
follow k (s) = {β ∈ CS k | ∃ α, γ ∈ CS ∗ : start →G αsβγ} ∪
∗
{β ∈ CS <k | ∃ α ∈ CS ∗ : start →G αsβ}
first(α) = first 1(α)
follow (s) = follow 1(s)
Simultane induktive Definition der first-Mengen
first() = {}
C ∈ CS ∧ α ∈ (S ∪ BS)∗ ⇒ first(Cα) = {C}
(s → α) ∈ R ∧ β ∈ (S ∪ BS)∗ ∧ C ∈ first(αβ) ⇒ C ∈ first(sβ)
(1)
(2)
(3)
179
Sei G eine CFG. Wir definieren den LR(1)-Compiler für G in mehreren Schritten. Zunächst
definieren wir einen Erkenner für die Sprache L(G)start. Während Erkenner für reguläre Sprachen reguläre Ausdrücke auf Funktionen des Typs X ∗ → 2 abbilden (siehe
Kapitel 2 und 3), bildet der folgende Erkenner (recognizer) Wörter über den Sorten und
Basismengen von G auf jene Funktionen ab:
recog1 : (S ∪ BS)∗ → 2X
∗
formuliert. Sei γ, α, ϕ ∈ (S ∪ BS)∗, x ∈ X und w ∈ X ∗.
recog1(γα)(xw) = recog1(γαx)(w) falls ∃ s → αβ ∈ R, v ∈ (S ∪ BS)∗ :
∗
start →G γsv, β 6= , x ∈ first(β) ∩ Z
shift (read)
recog1(γα)(xw) = recog1(γαB)(w) falls ∃ s → αβ ∈ R, B ∈ BS, v ∈ (S ∪ BS)∗ :
∗
start →G γsv, β 6= , x ∈ B ∈ first(β)
shift (read)
180
recog1(γα)(w) = recog1(γs)(w) falls ∃ s → α ∈ R, B ∈ BS, v ∈ (S ∪ BS)∗ :
∗
start →G γsv, s 6= start,
w = ∈ follow (s) ∨
head(w) ∈ follow (s) ∩ Z ∨
head(w) ∈ B ∈ follow (s)
reduce
recog1(ϕ)()
= 1
falls start → ϕ ∈ R
accept
recog1(ϕ)(w)
= 0
sonst
reject
recog1 ist genau dann wohldefiniert, wenn G eine LR(1)-Grammatik ist.
recog1 ist korrekt bzgl. L(G)start, d.h. für alle w ∈ X ∗ gilt:
recog1() = χ(L(G)start).
(1)
Die Funktion recog1 ist iterativ definiert. Um (1) benötigt man daher eine Invariante. Sie
lautet wie folgt: Für alle w ∈ X ∗,
∗
recog1(ϕ)(w) = 1 ⇐⇒ start →G ϕw.
(2)
(2) zeigt man durch Induktion über |w|. Aus (2) folgt sofort (1).
181
Im zweiten Schritt wird das erste Argument von recog1 durch den Inhalt eines Zustandskellers ersetzt. Die zugrundeliegende endliche (!) Zustandsmenge Q ist die Bildmenge der
Funktion
state : (S ∪ BS)∗ → P(Quad)
∗
ϕ 7→ {(s, α, β, u) ∈ Quad | ∃ γ, v : start →G γsv, ϕ = γα,
u = v = ∨ u = head(v) ∈ BS ∪ Z}
wobei Quad die Menge aller Quadrupel (s, α, β, u) ∈ S × (S ∪ BS)∗ × (S ∪ BS)∗ × (BS ∪
Z ∪ 1) mit s → αβ ∈ R ist.
Die Bedingungen in der Definition von recog1 werden durch Abfragen der Form
(s, α, β, x) ∈ state(ϕ) ersetzt:
recog1(γα)(xw) = recog1(γαx)(w) falls ∃ (s, α, β, ) ∈ state(γα) :
β 6= , x ∈ first(β) ∩ Z
recog1(γα)(xw) = recog1(γαB)(w) falls ∃ (s, α, β, ) ∈ state(γα), B ∈ BS :
β 6= , x ∈ B ∈ first(β)
recog1(γα)(w)
= recog1(γs)(w)
falls ∃ (s, α, , u) ∈ state(γα) : s 6= start,
w = = u ∨ head(w) = u ∈ Z ∨
head(w) ∈ u ∈ BS
182
recog1(ϕ)()
= 1 falls (start, ϕ, , ) ∈ state(ϕ)
recog1(ϕ)(w) = 0 sonst
Der LR-Automat für G hat die Eingabemenge S ∪ BS, die Zustandsmenge
QG = {state(ϕ) | ϕ ∈ (S ∪ BS)∗}
und die (goto-Tabelle genannte) partielle (!) Transitionsfunktion
δG : QG (→ QS∪BS
G
state(ϕ)
7→
λs.state(ϕs)
Simultane induktive Definition von QG und δG
start → α ∈ R ⇒ (start, , α, ) ∈ q0
(s, α, s0β, u) ∈ q ∧ s0 → γ ∈ R ∧ v ∈ first(βu) ⇒ (s0, , γ, v) ∈ q
(s, α, s0β, u) ∈ q, s0 ∈ S ∪ BS ⇒ (s, αs0, β, u) ∈ δG(q)(s0)
(3)
(4)
(5)
In der Definition von recog1 wird jedes Wort von (S ∪ BS)∗ durch eine gleichlange Liste
von Zuständen des LR-Automaten ersetzt:
183
h : (S ∪ BS)∗ → Q∗G
7→ q0 =def state()
(s1, . . . , sn) 7→ (state(s1 . . . sn), state(s1 . . . sn−1), . . . , state(s1), state())
Im Folgenden benutzen wir gelegentlich die Haskell-Funktionen (:) und (++), um auf
Wörtern zu operieren (siehe Kapitel 9).
∗
Aus recog1 wird recog2 : Q∗G → 2X :
Für alle q, qi ∈ QG, qs ∈ Q∗G, x ∈ X und w ∈ X ∗,
recog2(q : qs)(xw) = recog2(δG(q)(x) : q : qs, w)
falls ∃ (s, α, β, ) ∈ q : β 6= , x ∈ first(β) ∩ Z
recog2(q : qs)(xw) = recog2(δG(q)(B) : q : qs, w)
falls ∃ (s, α, β, ) ∈ q, B ∈ BS :
β 6= , x ∈ B ∈ first(β)
184
recog2(q1 : · · · : q|α| : q : qs)(w) = recog2(δG(q)(s) : q : qs)(w)
falls ∃ (s, α, , u) ∈ q1 : s 6= start,
w = = u ∨ head(w) = u ∈ Z ∨
head(w) ∈ u ∈ BS
recog2(q : qs)()
= 1 falls ∃ ϕ : (start, ϕ, , ) ∈ q
recog2(qs)(w)
= 0 sonst
Offenbar gilt recog1 = recog2 ◦ h, also insbesondere
recog2(q0) = χ(L(G)start)
(6)
wegen (1).
Um die Suche nach bestimmten Elementen eines Zustands in den Iterationsschritten von
recog2 zu vermeiden, verwenden wir die – Aktionstabelle für G genannte – Funktion
actG : QG × (BS ∪ Z ∪ 1) → R ∪ {shift, error },
die wie folgt definiert ist:
185
Für alle u ∈ BS ∪ Z ∪ 1,



 shift falls ∃ (s, α, β, ) ∈ q : β 6= , u ∈ first(β),
actG(q, u) =
s → α falls ∃ (s, α, , u) ∈ q,


 error sonst.
Unter Verwendung von δG und actG erhält man eine kompakte Definition von recog2:
Für alle q, qi ∈ QG, qs ∈ Q∗G, x ∈ X und w ∈ X ∗,
recog2(q : qs)(xw)
= recog2(δG(q)(x) : q : qs)(w)
falls x ∈ Z, actG(q, x) = shift
recog2(q : qs)(xw)
= recog2(δG(q)(B) : q : qs)(w)
falls ∃ B ∈ BS : x ∈ B, actG(q, B) = shift
recog2(q1 : · · · : q|α| : q : qs)(w) = recog2(δG(q)(s) : q : qs)(w)
falls ∃ u ∈ BS ∪ Z ∪ 1 : actG(q1, u) = s → α,
w = = u ∨ head(w) = u ∈ Z ∨
head(w) ∈ u ∈ BS
186
recog2(q : qs)() = 1 falls actG(q, ) = start → α
recog2(qs)(w)
= 0 sonst
Beispiel 7.2 SAB2
G = ({S, A, B}, {c, d, ∗}, R)
R = {S → A, A → A ∗ B, A → B, B → c, B → d}
LR-Automat für G
q0 = {(S, , A, ), (A, , A ∗ B, ), (A, , B, ), (A, , A ∗ B, ∗), (A, , B, ∗),
(B, , c, ), (B, , d, ), (B, , c, ∗), (B, , d, ∗)}
q1 = δG(q0)(A) = {(S, A, , ), (A, A, ∗B, ), (A, A, ∗B, ∗)}
q2 = δG(q0)(B) = {(A, B, , ), (A, B, , ∗)}
q3 = δG(q0)(c)
= {(B, c, , ), (B, c, , ∗)} = δG(q5)(c)
q4 = δG(q0)(d)
= {(B, d, , ), (B, d, , ∗)} = δG(q5)(d)
q5 = δG(q1)(∗)
= {(A, A∗, B, ), (A, A∗, B, ∗),
(B, , c, ), (B, , d, ), (B, , c, ∗), (B, , d, ∗)}
q6 = δG(q5)(B) = {(A, A ∗ B, , ), (A, A ∗ B, , ∗)}
187
goto-Tabelle für G
A B c
d ∗
q0 q1 q2 q3 q4
q1
q5
q5
q6 q3 q4
Aktionstabelle für G
q0
q1
q2
q3
q4
q5
q6
c shift
error
error
error
error
shift
error
d shift
error
error
error
error
shift
error
∗ error
shift
A → B B → c B → d error A → A ∗ B
error S → A A → B B → c B → d error A → A ∗ B
188
Ein Erkennerlauf
recog2(q0)(c ∗ d)
= recog2(q3q0)(∗d)
wegen actG(q0, c) = shift
und δG(q0)(c) = q3
= recog2(q2q0)(∗d)
wegen actG(q3, ∗) = B → c
und δG(q0)(B) = q2
= recog2(q1q0)(∗d)
wegen actG(q2, ∗) = A → B
und δG(q0)(A) = q1
= recog2(q5q1q0)(d)
wegen actG(q1, ∗) = shift
und δG(q1)(∗) = q5
= recog2(q4q5q1q0)() wegen actG(q5, d) = shift
und δG(q5)(d) = q4
= recog2(q6q5q1q0)() wegen actG(q4, ) = B → d
und δG(q5)(B) = q6
= recog2(q1q0)()
wegen actG(q6, ) = A → A ∗ B und δG(q0)(A) = q1
= 1
wegen actG(q1, ) = S → A
o
Für den dritten und letzten Schritt zum LR(1)-Compiler benötigen wir die abstrakte Syntax
Σ(G) von G (siehe Kapitel 4).
Sei A eine Σ(G)-Algebra. Aus recog2 wird der generische Compiler
∗
∗
∗
X
compileA
G : QG × A → M (A) .
189
Er startet mit q0 = state() im ansonsten leeren Zustandskeller. Die Bedeutung der Liste von A-Elementen im Definitionsbereich von compileA
G entnimmt man am besten der
folgenden Formalisierung der Korrektheit von compileG:
• Für alle w ∈ X ∗ und t ∈ TΣ(G),start,
A
Word (G)
(t) = w,
compileA
G (q0 , )(w) = η(fold (t)) ⇒ fold
compileA
G (q0 , )(w) = error(w) ⇒ w 6∈ L(G)start .
(1)
(2)
Die Invariante von compileG, aus der (1) folgt, ist komplizierter die des Erkenners recog1
(s.o.):
• Für alle α = w0s1w1 . . . snwn mit wi ∈ Z ∗ und si ∈ S ∪ BS, qs ∈ Q∗G, ai ∈ A,
w ∈ X ∗, und t ∈ TΣ(G)({x1, . . . , xn}),
∗
∗
compileA
G (state(α) : qs, [a1 , . . . , an ])(w) = η(gA (t)) ⇒ gW (t) = αw,
(3)
wobei gA : {x1, . . . , xn} → A und gW : {x1, . . . , xn} → Word (G) xi auf ai bzw. si
abbilden.
Aus (3) ergibt sich die folgende iterative Definition von compileA
G:
190
Für alle n ∈ N, q, qi ∈ QG, qs ∈ Q∗G, ai ∈ A, as ∈ A∗, x ∈ X und w ∈ X ∗,
A
compileA
G (q : qs, as)(xw) = compileG (δG (q)(x) : q : qs, as)(w)
falls x ∈ Z, actG(q, x) = shift
A
compileA
G (q : qs, as)(xw) = compileG (δG (q)(B) : q : qs, as ++[x])(w)
falls ∃ B ∈ BS : x ∈ B, actG(q, B) = shift
compileA
G (q1 : · · · : qn : q : qs, as ++[ai1 , . . . , aik ])(w) =
A
compileA
G (δG (q)(s) : q : qs, as ++[fr (ai1 , . . . , aik )])(w)
falls ∃ u ∈ BS ∪ Z ∪ 1 : actG(q1, u) = r = (s → e1 . . . en),
{i1, . . . , ik } = {1 ≤ i ≤ n | ei ∈ S ∪ BS},
w = = u ∨ head(w) = u ∈ Z ∨ head(w) ∈ u ∈ BS
A
compileA
G (q : qs, [ai1 , . . . , aik ])() = η(fr (ai1 , . . . , aik ))
falls actG(q, ) = r = (start → e1 . . . en),
{i1, . . . , ik } = {1 ≤ i ≤ n | ei ∈ S ∪ BS}
compileA
G (qs, as)(w) = errmsg(w)
sonst
191
Im Fall A = TΣ(G) bestehen die Listen as, [ai1 , . . . , aik ] und [frA(ai1 , . . . , aik )] aus Syntaxbäumen. Die zweite Gleichung zeigt ihren schrittweisen Aufbau:
ai
ai
1
k
as
fr
ai
1
ai
k
as
192
Beispiel 7.3 SAB2
Hier ist der um die schrittweise Konstruktion des zugehörigen Syntaxbaums erweiterte
Erkennerlauf von Beispiel 7.2:
T (G)
([0], [])(c ∗ d)
T (G)
([3, 0], [])(∗d)
T (G)
([2, 0], [fB→c])(∗d)
T (G)
([1, 0], [fA→B (fB→c)])(∗d)
T (G)
([5, 1, 0], [fA→B (fB→c)])(d)
T (G)
([4, 5, 1, 0], [fA→B (fB→c)])()
T (G)
([6, 5, 1, 0], [fA→B (fB→c), fB→d])()
T (G)
([1, 0], [fA→A∗B (fA→B (fB→c), fB→d)])()
compileGΣ
= compileGΣ
= compileGΣ
= compileGΣ
= compileGΣ
= compileGΣ
= compileGΣ
= compileGΣ
= η(fS→A(fA→A∗B (fA→B (fB→c), fB→d)))
In der graphischen Darstellung dieses Parserlaufs sind die Knoten der Syntaxbäume nicht
mit Konstruktoren, sondern mit den Regeln markiert, aus denen sie hervorgehen, wobei
der Pfeil zwischen der linken und rechten Seite einer Regel durch den Unterstrich ersetzt
wurde.
o
193
Beispiel 7.4 Auf den folgenden Seiten steht die vom C-Compiler-Generator yacc (“yet
another compiler-compiler”) aus der folgenden LR(1)-Grammatik G erzeugte Zustandsmenge – deren Elemente hier nur aus den ersten drei Komponenten der o.g. Quadrupel
bestehen – zusammen mit der auf die einzelnen Zustände verteilten Einträge der goto- und
Aktionstabelle:
$accept
prog
statems
assign
exp
->
->
->
->
->
prog $end
statems exp .
statems assign ; | ID : = exp
exp + exp | exp * exp | (exp) | NUMBER | ID
So besteht z.B. der Zustand 0 aus den beiden Tripeln ($accept, , $end) und (statems, , ).
Die beim Lesen eines Punktes im Zustand 0 ausgeführte Aktion ist eine Reduktion mit Regel
3, also mit statems → . Der Folgezustand von 0 ist bei Eingabe von prog bzw. statems
der Zustand 1 bzw. 2.
Link zur graphischen Darstellung des Parserlaufs auf dem Eingabewort
ID : = NUM ; ID : = ID + NUM ; NUM * ID + ID .
Den Sonderzeichen entsprechen in der input-Spalte des Parserlaufs und den u.a. Tabellen
passende Wortsymbole. Die Knoten der Syntaxbäume sind wie in Beispiel 7.3 mit Regeln
markiert.
194
195
196
Im Folgenden sind die goto- bzw. Aktionstabelle noch einmal getrennt wiedergegeben. op
(open) und cl (close) bezeichnen eine öffnende bzw. schließende Klammer. end steht für
das leere Wort . Wieder wird auf den Pfeil zwischen linker und rechter Seite einer Regel
verzichtet.
197
198
199
Im Folgenden werden die Regeln von G um C-Code erweitert, der eine Σ(G)-Algebra implementiert, die dem Zustandsmodell von JavaLight (siehe 11.5) ähnelt.
200
o
201
LR(k)
LALR(k)
SLR(k)
LL(k)
eindeutige kf.
Gramm.
A
aAa|a
nicht LR(k)
S
B
E
C
D
aB|eE
Cc|Dd
Cd|Dc
b
b
LR(1),
nicht LALR(1)
S
B
C
D
Ba|aCd
Cc|Dd
b
b
LALR(1),
nicht SLR(k)
S
A
B
C
D
E
aA|bB
Ca|Db
Cc|Da
E
E
ε
LL(1),
nicht LALR(k)
Hierarchie der CFG-Klassen
Zur Definition von LL(k)-Grammatiken verweisen wir auf die einschlägige Literatur. Kurz
gesagt, sind das diejenigen nicht-linksrekursiven CFGs, deren LL-Parser ohne Backtracking
auskommen.
202
eindeutige kontextfreie
det. kf. Sprachen
=LR(1)-Spr.
=SLR(1)-Spr.
=LALR(1)-Spr.
LL(k)Spr.
Sprachen
Hierarchie der entsprechenden Sprachklassen
Für jeden Grammatiktyp T bedeutet die Formulierung
L ist eine T -Sprache
lediglich, dass eine T -Grammatik existiert, die L erzeugt.
203
Während der LL-Compiler von Kapitel 6 – nach Beseitigung von Linksrekursion – jede kontextfreie Grammatik verarbeitet, selbst dann, wenn sie mehrdeutig ist, zeigt die obige Grafik,
dass die Forderung, dabei ohne Backtracking auszukommen, die Klasse der kompilierbaren
Sprachen erheblich einschränkt: Unter dieser Bedingung ist die bottom-up-Übersetzung offenbar mächtiger als die top-down-Compilation.
Umgekehrt wäre es den Versuch wert (z.B. in Form einer Bachelorarbeit), in Anlehnung an
den obigen Compiler für LR(1)-Grammatiken einen bottom-up-Compiler mit Backtracking
zu entwickeln. Da die Determinismusforderung wegfiele, bräuchten wir keinen Lookahead
beim Verarbeiten der Eingabe, womit die Zustände generell nur aus Tripeln bestünden –
wie im Beispiel 7.4.
204
8 Haskell: Typen und Funktionen
Die Menge 1 wird in Haskell unit-Typ genannt und mit () bezeichnet. () steht auch für
das einzige Element von 1.
Die Menge 2 entspricht dem Haskell-Typ Bool . Ihre beiden Elemente 0 und 1 werden mit
False bzw. True bezeichnet.
Alle Typen von Haskell sind aus Standardtypen wie Bool , Int oder F loat, Typvariablen
sowie Typkonstruktoren wie × (Produkt), + (Summe oder disjunkte Vereinigung) oder
→ (Funktionsmenge) aufgebaut.
Jeder Typ bezeichnet eine Menge, jeder Typkonstruktor eine Funktion auf Mengen von
Mengen.
Typnamen beginnen stets mit einem Großbuchstaben, Typvariablen mit einem Kleinbuchstaben. Typvariablen stehen für Mengen, Individuenvariablen für Elemente einer Menge. Beide müssen mit einem Kleinbuchstaben beginnen.
Das (kartesische) Produkt A1 × · · · × An von Mengen A1, . . . , An wird in Haskell wie seine
Elemente, die n-Tupel (a1, . . . , an), mit runden Klammern und Kommas notiert:
(A1, . . . , An).
205
Die Summe oder disjunkte Vereinigung SumAn von Mengen A1, . . . , An wird durch einen
(Haskell-)Datentyp implementiert (siehe Kapitel 10).
Funktionen werden benannt und durch rekursive Gleichungen definiert (s.u.) oder als λAbstraktion λp.e (Haskell-Notation: \p -> e) dargestellt, wobei p ein Muster für die
möglichen Argumente (Parameter) von λp.e ist, z.B. p = (x, (y, z)). Muster bestehen aus
Individuenvariablen und (Individuen-)Konstruktoren. Jede Variable kommt in einem Muster
höchstens einmal vor. (Die λ-Abstraktionen von Kapitel 2 haben nur Variablen als Muster.)
Der “Rumpf” e der λ-Abstraktion λp.e ist ein aus beliebigen Funktionen und Variablen
zusammengesetzter Ausdruck.
Ein Ausdruck der Form f (e) heißt Funktionsapplikation (auch: Funktionsanwendung
oder -aufruf). Ist f eine λ-Abstraktion, dann nennt man f (e) eine λ-Applikation. Die
λ-Applikation (λp.e)(e0) is auswertbar, wenn der Ausdruck e0 das Muster p matcht. Beim
Matching werden die Variablen von p in e durch Teilausdrücke von e0 ersetzt.
Die Auswertung einer Applikation von e wie z.B. (λ(x, y).x ∗ y + 5 + x)(7, 8), besteht in
der Bildung der Instanz 7 ∗ 8 + 5 + 7 der λ-Abstraktion λ(x, y).x ∗ y + 5 + x und deren
anschließender Auswertung:
(λ(x, y).x ∗ y + 5 + x)(7, 8) ; 7 ∗ 8 + 5 + 7 ; 56 + 5 + 7 ; 68
206
Die ghci-Befehle
:type \(x,y)->x*y+5+x
bzw.
:type (\(x,y)->x*y+5+x)(7,8)
liefern die Typen
Num a => (a,a) -> a
bzw.
Num a => a
der λ-Abstraktion λ(x, y).x ∗ y + 5 + x bzw. λ-Applikation (λ(x, y).x ∗ y + 5 + x)(7, 8).
Backslash (\) und Pfeil (->) sind offenbar die Haskell-Notationen des Symbols λ bzw. des
Punktes einer λ-Abstraktion. Num ist die Typklasse für numerische Typen. Allgemein werden
Typklassen in Kapitel 5 behandelt.
Link zur schrittweisen Auswertung der λ-Applikation (λ(x, y).x ∗ y + 5 + x)(7, 8)
Der Redex, d.i. der Teilausdruck, der im jeweils nächsten Schritt ersetzt wird, ist rot gefärbt.
Das Redukt, d.i. der Teilausdruck, durch den der Redex ersetzt wird, ist grün gefärbt.
Link zur schrittweisen Auswertung der λ-Applikation
(λx.x(true, 4, 77) + x(false, 4, 77))(λ(x, y, z).if x then y + 5 else z ∗ 6)
207
Eine geschachelte λ-Abstraktion λp1.λp2. . . . .λpn.e kann durch λp1 p2 . . . pn.e abgekürzt
werden. Sie hat einen Typ der Form t1 → (t2 → . . . (tn → t) . . . ), wobei auf diese Klammerung verzichtet werden kann, weil Haskell Funktionstypen automatisch rechtsassoziativ
klammert.
Anstelle der Angabe eines λ-Ausdrucks kann eine Funktion benannt und dann mit f mit
Hilfe von Gleichungen definiert werden:
f = \p -> e
ist äquivalent zu
f p = e (applikative Definition)
Funktionen, die andere Funktionen als Argumente oder Werte haben, heißen Funktionen
höherer Ordnung. Der Typkonstruktor → ist rechtsassoziativ. Also ist die Deklaration
(+) :: Int -> (Int -> Int)
ist äquivalent zu
(+) :: Int -> Int -> Int
Die Applikation einer Funktion ist linksassoziativ. Also ist
((+) 5) 6
ist äquivalent zu
(+) 5 6
(+) ist zwar als Präfixfunktion deklariert, kann aber auch infix verwendet werden. Dann
entfallen die runden Klammern um den Infixoperator:
5 + 6
ist äquivalent zu
(+) 5 6
208
Das Gleiche gilt für jede Funktion f eines Typs der Form A → B → C. Besteht f
aus Sonderzeichen, dann wird f bei der Präfixverwendung in runde Klammern gesetzt,
bei der Infixverwendung nicht. Beginnt f mit einem Kleinbuchstaben und enthält f keine
Sonderzeichen, dann wird f bei der Infixverwendung in Akzentzeichen gesetzt, bei der
Präfixverwendung nicht:
mod :: Int -> Int -> Int
mod 9 5
ist äquivalent zu
9 `mod` 5
Die Infixnotation wird auch verwendet, um die in f enthaltenen Sektionen (Teilfunktionen) des Typs A → C bzw. B → C zu benennen. Z.B. sind die folgenden Sektionen
Funktionen des Typs Int -> Int, während (+) und mod den Typ Int -> Int -> Int
haben.
(+5)
(9`mod`)
ist äquivalent zu
ist äquivalent zu
(+) 5
mod 9
ist äquivalent zu
ist äquivalent zu
\x -> x+5
\x -> 9`mod`x
Eine Sektion wird stets in runde Klammern eingeschlossen. Die Klammern gehören zum
Namen der jeweiligen Funktion.
209
Der Applikationsoperator
($) :: (a -> b) -> a -> b
f $ a = f a
führt die Anwendung einer gegebenen Funktion auf ein gegebenes Argument durch. Sie ist
rechtsassoziativ und hat unter allen Operationen die niedrigste Priorität. Daher kann durch
Benutzung von $ manch schließende Klammer vermieden werden:
f1 $ f2 $ ... $ fn a
;
f1 (f2 (...(fn a)...)))
Demgegenüber ist der Kompositionsoperator
(.) :: (b -> c) -> (a -> b) -> a -> c
(g . f) a = g (f a)
zwar auch rechtsassoziativ, hat aber – nach den Präfixoperationen – die höchste Priorität.
(f1 . f2 . ... . fn) a
;
f1 (f2 (...(fn a)...)))
210
U.a. benutzt man den Kompositionsoperator, um in einer applikativen Definition Argumentvariablen einzusparen. So sind die folgenden drei Definitionen einer Funktion f : a → b → c
äquivalent zueinander:
f a b = g (h a) b
f a
= g (h a)
f
= g . h
Welchen Typ hat hier g bzw. h?
Auch die folgenden drei Definitionen einer Funktion f : a → b sind äquivalent zueinander:
f a = g . h a
f a = (g.) (h a)
f
= (g.) . h
Welchen Typ hat hier g bzw. h?
211
Monomorphe und polymorphe Typen
Ein Typ ohne Typvariablen heißt monomorph.
Ein Typ mit Typvariablen wie z.B. a -> Int -> b heißt polymorph. Eine Funktion mit
monomorphem oder polymorphem Typ heißt monomorphe bzw. polymorphe Funktion.
Eine Funktion heißt mono- bzw. polymorph, wenn ihr Typ mono- bzw. polymorph ist.
Ein Typ u heißt Instanz eines Typs t, wenn u durch Ersetzung der Typvariablen von t
aus t entsteht.
Typvariablen stehen für Mengen, Individuenvariablen stehen für Elemente einer Menge. Sie müssen ebenfalls mit einem Kleinbuchstaben beginnen.
Eine besondere Individuenvariable ist der Unterstrich (auch Wildcard genannt). Er darf
nur auf der linken Seite einer Funktionsdefinition vorkommen, was zur Folge hat, dass er
für einen Teil eines Argumentmusters der gerade definierten Funktion steht, der ihre Werte
an Stellen, die das Muster matchen, nicht beeinflusst (siehe Beispiele im nächsten Kapitel).
Die oben definierten Funktionen ($) und (.) sind demnach polymorph.
212
Weitere polymorphe Funktionen höherer Ordnung
Identität
id :: a -> a
id a = a
id
= \a -> a
äquivalente Definition
const :: a -> b -> a
const a b = a
const a
= \b -> a
const
= \a -> \b -> a
const
= \a b -> a
konstante Funktion
äquivalente Definitionen
update :: Eq a => (a -> b) -> a -> b -> a -> b
update f a b a' = if a == a' then b else f a'
update f
:: a -> b -> a -> b
update f a
:: b -> a -> b
Funktionsupdate
(nicht im Prelude)
äquivalente Schreibweisen
update(f)
update(f)(a)
(update f) a
213
update f a b
:: a -> b
update(f)(a)(b)
((update f) a) b
update f a b a' :: b
update(f)(a)(b)(a')
(((update f) a) b) a'
flip :: (a -> b -> c) -> b -> a -> c
flip f b a = f a b
Vertauschung der Argumente
flip mod 11 ist äquivalent zu
(`mod` 11) (s.o.)
curry :: ((a,b) -> c) -> a -> b -> c
curry f a b = f (a,b)
Kaskadierung
(Currying)
uncurry :: (a -> b -> c) -> (a,b) -> c
uncurry f (a,b) = f a b
Dekaskadierung
214
9 Haskell: Listen
Sei A eine Menge. Die Menge A∗ (siehe Kapitel 2) wird in Haskell mit eckigen Klammern
notiert, also durch [A] — ebenso wie ihre Elemente: Für (a1, . . . , an) ∈ A∗ schreibt man
[a1, . . . , an].
Eine n-elementige Liste kann extensional oder als funktionaler Ausdruck dargestellt werden:
[a1, . . . , an]
ist äquivalent zu
a1 : (a2 : (. . . (an : []) . . . ))
Die Konstante [] (leere Liste) vom Typ [A] und die Funktion (:) (Anfügen eines Elementes
von A ans linke Ende einer Liste) vom Typ A → [A] → [A] heißen Konstruktoren, weil
sie nicht wie andere Funktionen Werte berechnen, sondern dazu dienen, die Elemente eines
Typs aufzubauen. Auch werden sie benötigt, um die Zugehörigkeit eines Elementes eines
Summentyps zu einem bestimmten Summanden wiederzugeben (siehe Kapitel 4).
Die Klammern in a1 : (a2 : (. . . (an : []) . . . )) können weggelassen werden, weil der Typ von
(:) keine andere Klammerung zulässt.
Die durch mehrere Gleichungen ausgedrückten Fallunterscheidungen bei den folgenden Definitionen von Funktionen auf Listen ergeben sich aus verschiedenen Mustern der Funktionsargumente bzw. Bedingungen an die Argumente (Boolesche Ausdrücke hinter |).
215
Seien x, y, s Individuenvariablen. s ist ein Muster für alle Listen, [] das Muster für die leere
Liste, [x] ein Muster für alle einelementigen Listen, x : s ein Muster für alle nichtleeren
Listen, x : y : s ein Muster für alle mindestens zweielementigen Listen, usw.
length :: [a] -> Int
length (_:s) = length s+1
length _
= 0
length [3,44,-5,222,29] ; 5
Link zur schrittweisen Auswertung von length [3,44,-5,222,29]
head :: [a] -> a
head (a:_) = a
head [3,2,8,4] ; 3
tail :: [a] -> [a]
tail (_:s) = s
tail [3,2,8,4] ; [2,8,4]
(++) :: [a] -> [a] -> [a]
(a:s)++s' = a:(s++s')
_++s
= s
[3,2,4]++[8,4,5] ; [3,2,4,8,4,5]
216
(!!) :: [a] -> Int -> a
(a:_)!!0
= a
(_:s)!!n | n > 0 = s!!(n-1)
[3,2,4]!!1 ; 2
init :: [a] -> [a]
init [_]
= []
init (a:s) = a:init s
init [3,2,8,4] ; [3,2,8]
last :: [a] -> a
last [a]
= a
last (_:s) = last s
last [3,2,8,4] ; 4
take
take
take
take
:: Int -> [a] -> [a]
take 3 [3,2,4,8,4,5] ; [3,2,4]
0 _
= []
n (a:s) | n > 0 = a:take (n-1) s
_ []
= []
drop
drop
drop
drop
:: Int -> [a] -> [a]
drop 4 [3,2,4,8,4,5] ; [4,5]
0 s
= s
n (_:s) | n > 0 = drop (n-1) s
_ []
= []
217
Funktionslifting auf Listen
map :: (a -> b) -> [a] -> [b]
map f (a:s) = f a:map f s
map _ _
= []
map (+1) [3,2,8,4]
; [4,3,9,5]
map ($ 7) [(+1),(+2),(*5)] ; [8,9,35]
map ($ a) [f1,f2,...,fn]
; [f1 a,f2 a,...,fn a]
Link zur schrittweisen Auswertung von map(+3)[2..9]
zipWith :: (a -> b -> c) -> [a] -> [b] -> [c]
zipWith f (a:s) (b:s') = f a b:zipWith f s s'
zipWith _ _ _
= []
zipWith (+) [3,2,8,4] [8,9,35] ; [11,11,43]
zip :: [a] -> [b] -> [(a,b)]
zip = zipWith (,)
zip [3,2,8,4] [8,9,35] ; [(3,8),(2,9),(8,35)]
218
Strings sind Listen von Zeichen
Strings werden als Listen von Zeichen betrachtet, d.h. die Typen String und [Char] sind
identisch.
Z.B. haben die folgenden Booleschen Ausdrücke den Wert True:
"" == []
"H" == ['H']
"Hallo" == ['H','a','l','l','o']
Also sind alle Listenfunktionen auf Strings anwendbar.
words :: String -> [String] und unwords :: [String] -> String zerlegen bzw.
konkatenieren Strings, wobei Leerzeichen, Zeilenumbrüche ('\n') und Tabulatoren ('\t')
als Trennsymbole fungieren.
unwords fügt Leerzeichen zwischen die zu konkatenierenden Strings.
lines :: String -> [String] und unlines :: [String] -> String zerlegen bzw.
konkatenieren Strings, wobei nur Zeilenumbrüche als Trennsymbole fungieren.
unlines fügt '\n' zwischen die zu konkatenierenden Strings.
219
Listenfaltung
Faltung einer Liste von links her
f
f
foldl f state [a ,a ,a ,a ,a ]
1 2 3 4 5
Endzustand
f
f
f
state
Anfangszustand
a1
a2
a3
a4
a5
Eingaben
foldl :: (a -> b -> a) -> a -> [b] -> a
foldl f a (b:s) = foldl f (f a b) s
foldl _ a _
= a
a ist Zustand, b ist Eingabe
f ist Zustandsüberführung
Link zur schrittweisen Auswertung von foldl(+)(0)[1..5]
foldl1 :: (a -> a -> a) -> [a] -> a
foldl1 f (a:s) = foldl f a s
sum
= foldl (+) 0
and
= foldl (&&) True
minimum = foldl1 min
product
or
maximum
= foldl (*) 1
= foldl (||) False
= foldl1 max
220
concat
= foldl (++) []
concatMap :: (a -> [b]) -> [a] -> [b]
concatMap f = concat . map f
Parallele Faltung zweier Listen von links her
fold2 f state [a1,a2,a3,a4,a5] [b1,b2,b3,b4,b5]
f
f
f
f
f
state a1
b1
a2
b2
a3
b3
a4
b4
a5
b5
fold2 :: (a -> b -> c -> a) -> a -> [b] -> [c] -> a
fold2 f a (b:s) (c:s') = fold2 f (f a b c) s s'
fold2 _ a _ _
= a
221
listsToFun :: Eq a => b -> [a] -> [b] -> a -> b
listsToFun = fold2 update . const
Beginnend mit const b, erzeugt listsToFun b schrittweise aus einer Argumentliste as
und einer Werteliste bs die entsprechende Funktion:
(
bs!!i falls i = max{k | as!!k = a, k < length(bs)},
listsToFun b as bs a =
b
sonst.
Faltung einer Liste von rechts her
entspricht der Auswertung ihrer Konstruktordarstellung in einer Algebra A, die die Konstruktoren (:) :: b -> [b] -> [b] und [] :: [b] als Funktion (:)A bzw. Konstante
[]A interpretiert (siehe Kapitel 2).
(:)A
:
foldr f a [b1,b2,b3,b4,b5]
f
f
(:)A
:
f
f
(:)A
:
(:)A
:
f
b1
b2
b3
b4
b5
a
b1
b2
b3
b4
b5
(:)A
:
[] []A
222
foldr :: (b -> a -> a) -> a -> [b] -> a
foldr f a (b:s) = f b $ foldr f a s
foldr _ a _
= a
Der Applikationsoperator $ als Parameter von Listenfunktionen
foldr ($) a [f1,f2,f3,f4]
;
f1 $ f2 $ f3 $ f4 a
foldl (flip ($)) a [f4,f3,f2,f1]
;
f1 $ f2 $ f3 $ f4 a
map f [a1,a2,a3,a4]
map ($a) [f1,f2,f3,f4]
;
;
[f a1,f a2,f a3,f a4]
[f1 a,f2 a,f3 a,f4 a]
zipWith ($) [f1,f2,f3,f4] [a1,a2,a3,a4]
; [f1 a1,f2 a2,f3 a3,f4 a4]
223
Listenlogik
any :: (a -> Bool) -> [a] -> Bool
any f = or . map f
any (>4) [3,2,8,4] ; True
all :: (a -> Bool) -> [a] -> Bool
all f = and . map f
all (>2) [3,2,8,4] ; False
elem :: Eq a => a -> [a] -> Bool
elem a = any (a ==)
elem 2 [3,2,8,4] ; True
notElem :: Eq a => a -> [a] -> Bool
notElem a = all (a /=)
notElem 9 [3,2,8,4] ; True
filter :: (a -> Bool) -> [a] -> [a] filter (<8) [3,2,8,4] ; [3,2,4]
filter f (a:s) = if f a then a:filter f s else filter f s
filter f _
= []
224
Jeder Aufruf von map, zipWith, filter oder einer Komposition dieser Funktionen entspricht einer Listenkomprehension:
map f s
= [f a | a <- s]
zipWith f s s' = [f a b | (a,b) <- zip s s']
filter f s
= [a | a <- s, f a]
zip(s)(s’) ist nicht das kartesische Produkt von s und s’. Dieses entspricht der Komprehension [(a,b) | a <- s, b <- s’].
Allgemeines Schema von Listenkomprehensionen:
[e(x1, . . . , xn) | x1 ← s1, . . . , xn ← sn, be(x1, . . . , xn)] :: [a]
• x1, . . . , xn sind Variablen,
• s1, . . . , sn sind Listen,
• e(x1, . . . , xn) ist ein Ausdruck des Typs a,
• xi ← si heißt Generator und steht für xi ∈ si,
• be(x1, . . . , xn) heißt Guard und ist ein Boolescher Ausdruck.
Jede endlichstellige Relation lässt sich als Listenkomprehension implementieren, z.B. die
Menge aller Tripel (a, b, c) ∈ A1 × A2 × A3, die ein Prädikat p : A1 × A2 × A3 → Bool
erfüllen, durch [(a,b,c) | a <- a1, b <- a2, c <- a3, p(a,b,c)].
225
10 Haskell: Datentypen und Typklassen
Zunächst das allgemeine Schema einer Datentypdefinition:
data DT x_1 ... x_m = C_1 typ_11 ... typ_1n_1 | ... |
C_k typ_k1 ... typ_kn_k
typ11, . . . , typknk sind beliebige Typen, die außer x1, . . . , xm keine Typvariablen enthalten.
DT heißt rekursiv, wenn DT in mindestens einem dieser Typen vorkommt.
Die durch DT implementierte Menge besteht aus allen Ausdrücken der Form
C_i e_1 ... e_n_i,
wobei 1 ≤ i ≤ n und für alle 1 ≤ j ≤ ni ej ein Element des Typs typij ist. Als Funktion
hat Ci den Typ
typ_i1 -> ... -> typ_in_i -> DT a_1 ... a_m.
Alle mit einem Großbuchstaben beginnenden Funktionssymbole und alle mit einem Doppelpunkt beginnenden Folgen von Sonderzeichen werden vom Haskell-Compiler als Konstruktoren eines Datentyps aufgefasst und müssen deshalb irgendwo im Programm in einer
Datentypdefinition vorkommen.
226
Die Unterschiede in der Schreibweise von Konstruktoren bei Infix- bzw. Präfixverwendung
sind dieselben wie bei anderen Funktionen.
Beispiel 10.1 Der Haskell-Standardtyp für Listen
data [a] = [] | a : [a]
Für jede Menge A besteht [A] aus
• allen endlichen Ausdrücken a1 : . . . : an : [] mit a1, . . . , an ∈ A und
• allen unendlichen Ausdrücken a1 : a2 : a3 : . . . mit {ai | i ∈ N} ⊆ A.
[A] ist die größte Lösung der Gleichung
M = {[]} ∪ {a : s | a ∈ A, s ∈ M }
(1)
in der Mengenvariablen M und damit [A] eine Haskell-Implementierung der Menge CTList(A)
aller List(A)-Grundterme.
Als Elemente von [Int] lauten die List(Z)-Grundterme blink und blink’ wie folgt:
blink,blink' :: [Int]
blink = 0:blink'
blink' = 1:blink
227
Die endlichen Ausdrücke von [A] bilden die kleinste Lösung von (1) und damit eine HaskellImplementierung der Menge TList(A) der List(A)-Grundterme endlicher Tiefe (siehe 2.13). o
Beispiel 10.2 (siehe 2.13) TReg(BS) ist isomorph zur kleinsten und CTReg(BS) zur größten
Lösung der Gleichung
M = {par(t, u) | t, u ∈ M } ∪ {seq(t, u) | t, u ∈ M } ∪ {iter(t) | t ∈ M } ∪
{base(B) | B ∈ BS}
(2)
in der Mengenvariablen M . Beide Mengen werden deshalb durch folgenden Datentyp implementiert:
data RegT bs = Par (RegT bs) (RegT bs) | Seq (RegT bs) (RegT bs) |
Iter (RegT bs) | Base bs
o
Summentypen
Die Summe oder disjunkte Vereinigung
A1 + · · · + An =def {(a, 1) | a ∈ A1} ∪ · · · ∪ {(a, n) | a ∈ An}
von n > 1 Mengen A1, . . . , An (siehe Kapitel 2) wird durch einen nicht-rekursiven Datentyp mit n Konstruktoren C1, . . . , Cn implementiert, welche die Summanden voneinander
228
trennen, m.a.W.: es gibt eine Bijektion zwischen A1 + · · · + An und der Menge
{C1(a) | a ∈ A1} ∪ · · · ∪ {Cn(a) | a ∈ An}.
Z.B. implementiert der Standardtyp
data Maybe a = Nothing | Just a
die binäre Summe 1 + A, die A um ein Element zur Darstellung “undefinierter” Werte
erweitert. Damit lassen sich partielle Funktionen totalisieren wie die Funktion lookup, mit
der der Graph einer Funktion von A nach B modifiziert wird (siehe Kapitel 2):
lookup :: Eq a => a -> [(a,b)] -> Maybe b
lookup a ((a',b):r) = if a == a' then Just b else lookup a r
lookup _ _
= Nothing
Der Standardtyp Either implementiert beliebige binäre Summen:
data Either a b = Left a | Right b
Die Menge aller ganzen Zahlen kann als dreistellige Summe implementiert werden:
data INT = Zero | Plus PosNat | Minus PosNat
data PosNat = One | Succ PosNat
229
PosNat implementiert die Menge aller positiven natürlichen Zahlen (und ist rekursiv).
Datentypen mit Destruktoren
Um auf die Argumente eines Konstruktors zugreifen zu können, ordnet man ihnen Namen
zu, die field labels genannt werden und Destruktoren im Sinne von Kapitel 2 wiedergeben.
In obiger Definition von DT wird
C_i typ_i1 ... typ_in_i
erweitert zu:
C_i {d_i1 :: typ_i1, ..., d_in_i :: typ_in_i}
Wie Ci , so ist auch dij eine Funktion. Als solche hat sie den Typ
DT a1 ... am -> typ_ij
Destruktoren sind invers zu Konstruktoren. Z.B. hat der folgende Ausdruck den Wert ej :
d_ij (C_i e_1 ... e_ni)
Destruktoren nennt man in OO-Sprachen Attribute, wenn ihre Wertebereiche aus Standardtypen zusammengesetzt ist, bzw. Methoden (Zustandstransformationen), wenn der
230
Wertebereich mindestens einen Datentyp mit Destruktoren enthält. Außerdem schreibt man
dort in der Regel x.d ij anstelle von d ij(x).
Mit Destruktoren lautet das allgemeine Schema einer Datentypdefinition also wie folgt:
data DT a_1 ... a_m = C_1 {d_11 :: typ_11,..., d_1n_1 :: typ_1n_1} |
... |
C_k {d_k1 :: typ_k1,..., d_kn_k :: typ_kn_k}
Elemente von DT können mit oder ohne Destruktoren definiert werden:
obj = C_i e_i1 ... e_in_i
ist äquivalent zu
obj = C_i {d_i1 = e_i1,..., d_in_i = e_in_i}
Die Werte einzelner Destruktoren von obj können wie folgt verändert werden:
obj' = obj {d_ij_1 = e_1',..., d_ij_m = e_m'}
obj 0 unterscheidet sich von obj dadurch, dass den Destruktoren d ij 1,...,d ij m neue
Werte, nämlich e 1’,...,e m’zugewiesen wurden.
231
Destruktoren dürfen nicht rekursiv definiert werden. Folglich deutet der Haskell-Compiler
jedes Vorkommen von attrij auf der rechten Seite einer Definitionsgleichung als eine vom
gleichnamigen Destruktor verschiedene Funktion und sucht nach deren Definition.
Dies kann man nutzen, um dij doch rekursiv zu definieren, indem in der zweiten Definition
von obj (s.o.) die Gleichung d ij = ej durch d ij = d ij ersetzt und d ij lokal definiert
wird:
obj = C_i {d_i1 = e_1,..., d ij = d_ij,..., d_in_i = en_i}
where d_ij ... = ...
Ein Konstruktor darf nicht zu mehreren Datentypen gehören.
Ein Destruktor darf nicht zu mehreren Konstruktoren unterschiedlicher Datentypen gehören.
Beispiel Listen mit Destruktoren
könnten in Haskell durch folgenden Datentyp definiert werden:
data ListD a = Nil | Cons {hd :: a, tl :: ListD a}
Man beachte, dass die Destrukturen
hd :: ListD a -> a
und
tl :: ListD a -> ListD a,
232
die den Kopf bzw. Rest einer nichtleeren Liste liefern, partielle Funktionen sind, weil sie
nur auf mit dem Konstruktor Cons gebildete Elemente von ListD(A) anwendbar sind. o
Die Destruktoren eines Datentyps DT sind immer dann totale Funktionen, wenn DT genau
einen Konstruktor hat. Wie das folgende Beispiel nahelegt, ist das aus semantischer Sicht
keine Einschränkung: Jeder Datentyp ohne Destruktoren ist isomorph zu einem Datentyp
mit genau einem Konstruktor und einem Destruktor.
Beispiel 10.3 Listen mit totalen Destruktoren (siehe 2.13)
DTcoList(A) ist isomorph zur größten Lösung der Gleichung
M = {coListC(Nothing)} ∪ {coListC(Just(a, s)) | a ∈ A, s ∈ M }
(3)
in der Mengenvariablen M und wird deshalb durch folgenden Datentyp implementiert:
data ColistC a = ColistC {split :: Maybe (a,ColistC a)}
Wie man leicht sieht, ist die größte Lösung von (3) isomorph zur größten Lösung von (1),
also zu CTList(A).
Die Einschränkung von CTList(A) auf unendliche Listen ist isomorph zu DTStream(A) und
wird daher durch folgenden Datentyp implementiert:
233
data StreamC a = (:<) {hd :: a, tl :: StreamC a}
Als Elemente von StreamC(Int) lauten die Stream(Z)-Coterme blink und blink’ wie folgt:
blink,blink' :: StreamC Int
blink = 0:<blink'
blink' = 1:<blink
o
Beispiel 10.4 (siehe 2.13) DTDAut(X,Y ) ist isomorph zur größten Lösung der Gleichung
M = {DA(f )(y) | f : X → M , y ∈ Y }
in der Mengenvariablen M und wird deshalb durch folgenden Datentyp implementiert:
data DAutC x y = DA {deltaC :: x -> DAutC x y, betaC :: y}
Als Elemente von DAutC (Int) lauten die Acc(Z)-Coterme esum und osum wie folgt:
esum,osum :: DAutC Int Bool
esum = DA {deltaC = \x -> if even x then esum else osum, betaC = True}
osum = DA {deltaC = \x -> if odd x then esum else osum, betaC = False}
o
234
Typklassen
stellen Bedingungen an die Instanzen einer Typvariablen. Die Bedingungen bestehen in der
Existenz bestimmter Funktionen, z.B.
class Eq a where (==), (/=) :: a -> a -> Bool
(/=) = (not .) . (==)
Eine Instanz einer Typklasse besteht aus den Instanzen ihrer Typvariablen sowie Definitionen der in ihr deklarierten Funktionen.
instance Eq (Int,Bool) where (x,b) == (y,c) = x == y && b == c
instance Eq a => Eq [a]
where s == s' = length s == length s' && and $ zipWith (==) s s'
Auch (/=) könnte hier definiert werden. Die Definition von (/=) in der Klasse Eq als
Negation von (==) ist nur ein Default!
Der Typ jeder Funktion einer Typklasse muss die - in der Regel eine - Typvariable der Typklasse mindestens einmal enthalten. Sonst wäre die Funktion ja gar nicht von (der jeweiligen
Instanz) der Typvariable abhängig.
235
10.5 Mengenoperationen auf Listen
insert :: Eq a => a -> [a] -> [a]
insert a s@(b:s') = if a == b then s else b:insert a s'
insert a _
= [a]
union :: Eq a => [a] -> [a] -> [a]
union = foldl $ flip insert
Mengenvereinigung
unionMap :: Eq a => (a -> [b]) -> [a] -> [b]
unionMap f = foldl union [] . map f
concatMap für Mengen
subset :: Eq a => [a] -> [a] -> Bool
s `subset` s' = all (`elem` s') s
Mengeninklusion
Unterklassen
Typklassen können wie Objektklassen andere Typklassen erben. Die jeweiligen Oberklassen
werden vor dem Erben vor dem Pfeil => aufgelistet.
class Eq a => Ord a where (<=), (<), (>=), (>) :: a -> a -> Bool
236
max, min :: a -> a -> a
a < b
= a <= b && a /= b
a >= b = b <= a
a > b
= b < a
max x y = if x >= y then x else y
min x y = if x <= y then x else y
Ausgeben
Vor der Ausgabe von Daten eines Typs T wird automatisch die T-Instanz der Funktion
show aufgerufen, die zur Typklasse Show a gehört.
class Show a where
show :: a -> String
show x = shows x ""
shows :: a -> String -> String
shows = showsPrec 0
showsPrec :: Int -> a -> String -> String
237
Das String-Argument von showsPrec wird an die Ausgabe des Argumentes vom Typ a
angefügt.
Steht deriving Show am Ende der Definition eines Datentyps, dann werden dessen Elemente in der Darstellung ausgegeben, in der sie im Programmen vorkommen.
Für andere Ausgabeformate müssen entsprechende Instanzen von show oder showsPrec
definiert werden.
Einlesen
Bei der Eingabe von Daten eines Typs T wird automatisch die T-Instanz der Funktion read
aufgerufen, die zur Typklasse Read a gehört:
class Read a where
readsPrec :: Int -> String -> [(a,String)]
reads :: String -> [(a,String)]
reads = readsPrec 0
read :: String -> a
read s = case [x | (x,t) <- reads s, ("","") <- lex t] of
[x] -> x
238
[]
_
-> error "PreludeText.read: no parse"
-> error "PreludeText.read: ambiguous parse"
reads s liefert eine Liste von Paaren, bestehend aus dem als Element vom Typ a erkannten
Präfix von s und der jeweiligen Resteingabe (= Suffix von s).
lex :: String -> [(a,String)] ist eine Standardfunktion, die ein evtl. aus mehreren
Zeichen bestehendes Symbol erkennt, vom Eingabestring abspaltet und sowohl das Symbol
als auch die Resteingabe ausgibt.
Der Generator ("","") <- lex t in der obigen Definition von read s bewirkt, dass nur
die Paare (x,t) von reads s berücksichtigt werden, bei denen die Resteingabe t aus
Leerzeichen, Zeilenumbrüchen und Tabulatoren besteht.
Steht deriving Read am Ende der Definition eines Datentyps, dann werden dessen Elemente in der Darstellung erkannt, in der sie in Programmen vorkommen.
Für andere Eingabeformate müssen entsprechende Instanzen von readsPrec definiert werden. Jede Instanz von readsPrec ist – im Sinne von Kapitel 5 – ein Parser in die Listenmonade. Da die Implementierung von Parsern und Compilern in Haskell erst in späteren
Kapiteln behandelt wird, geben wir an dieser Stelle keine Beispiele für readsPrec an.
239
11 Algebren in Haskell
Sei Σ = (S, I, F ) eine Signatur, I = {x1, . . . , xk }, S = {s1, . . . , sm} und
F = {f1 : e1 → e01, . . . , fn : en → e0n}.
Jede Σ-Algebra entspricht einem Element des folgenden polymorphen Datentyps:
data Sigma x1 ... xk s1 ... sm = Sigma {f1 :: e1 -> e1',...,
fn :: en -> en'}
Die Sorten und Operationssymbole von Σ werden durch Typvariablen bzw. Destruktoren
wiedergegeben und durch die Trägermengen bzw. (kaskadierten) Funktionen der jeweiligen
Algebra instanziiert.
Um eine Signatur Σ in Haskell zu implementieren, genügt es daher, den Datentyp ihrer
Algebren nach obigem Schema zu formulieren.
Der Datentyp Sigma(x1) . . . (xk ) repräsentiert im Gegensatz zu den Datentypen der Beispiele 10.1-10.4, die Trägermengen einzelner Algebren implementieren, die Klasse aller ΣAlgebren.
240
11.1 Erweiterung von Term- und anderen Trägermengen zu Algebren
(siehe 2.13 und 2.14)
data Nat nat = Nat {zero :: nat, succ :: nat -> nat}
natT implementiert TN at, listT implementiert TList(X):
natT :: Nat Int
natT = Nat {zero = 0, succ = (+1)}
data List x list = List {nil :: list, cons :: x -> list -> list}
listT implementiert TList(X) (siehe 10.1):
listT :: List x [x]
listT = List {nil = [], cons = (:)}
Die folgende Funktion implementiert die Faltung fold alg : TList(X) → alg von List(X)Grundtermen endlicher Tiefe in einer beliebigen List(X)-Algebra alg:
foldList :: List x list -> [x] -> list
foldList alg []
= nil alg
foldList alg (x:s) = cons alg x $ foldList alg s
241
data Reg bs reg = Reg {par,seq :: reg -> reg -> reg,
iter :: reg -> reg,
base :: bs -> reg,}
regT implementiert TReg(BS), regWord implementiert Regword (BS) (siehe 2.14):
regT :: bs -> Reg bs (RegT bs)
(siehe 10.2)
regT = Reg {par = Par, seq = Seq, iter = Iter, base = Base}
regWord :: Show bs => bs -> Reg bs (Int -> String)
regWord bs = Reg {par = \f g n -> enclose (n > 0) $ f 0 ++ '+':g 0,
seq = \f g n -> enclose (n > 1) $ f 1 ++ '.':g 1,
iter = \f n -> enclose (n > 2) $ f 2 ++ "*",
base = \b -> show . const b}
where enclose b w = if b then '(':w++")" else w
Die folgende Funktion implementiert die Faltung fold alg : TReg(BS) → alg von regulären
Ausdrücken (= Reg(BS)-Grundtermen) in einer beliebigen Reg(BS)-Algebra alg:
foldReg :: Reg bs reg -> RegT bs -> reg
foldReg alg t = case t of Par t u -> par alg (f t) $ f u
Seq t u -> seq alg (f t) $ f u
242
Iter t -> iter alg $ f t
Base b -> base alg b
where f = foldReg alg
instance Show bs => Show (RegT bs) where
showsPrec = flip $ foldReg regWord
data Stream x s = Stream {head :: s -> x, tail :: s -> list}
streamC :: Stream x (StreamC x)
streamC = Stream {head = hd, tail = tl}
(siehe 10.3)
Die Stream(Z)-Algebra zo (siehe 2.11) kann wie folgt implementiert werden:
data ZO = Blink | Blink'
zo :: Stream Int Blinks
zo = Stream {head = \case Blink -> 0; Blink' -> 1,
tail = \case Blink -> Blink'; Blink' -> Blink}
Die finale Stream(X)-Algebra StreamFun(X) (siehe 17.5) kann wie folgt implementiert
werden:
243
streamFun :: Stream x (Int -> x)
streamFun = Stream {head = ($0), tail = \s n -> s $ n+1}
Die folgenden Funktionen implementieren die Entfaltungen von Elementen einer beliebigen
Stream(X)-Algebra alg zu Elementen von StreamFun(X) bzw. DTStream(X):
unfoldStreamF :: Stream x list -> list -> Int -> x
unfoldStreamF alg s = \case 0 -> head_ alg s
n -> unfoldStreamF alg (tail_ alg s) $ n-1
unfoldStream :: Stream x list -> list -> StreamC x
unfoldStream alg s = head alg s :< unfoldStream alg $ tail alg s
data DAut x y state = DAut {delta :: state -> x -> state,
beta :: state -> y}
Der Datentyp DAutC(X)(Y) von Beispiel 10.4 ist eine DAut(X,Y)-Algebra:
dAutC :: DAut x y (DAutC x y)
dAutC = DAut {delta = deltaC, beta = betaC}
(siehe 10.4)
Die Acc(Z)-Algebra eo (siehe 2.11) kann wie folgt implementiert werden:
244
data EO = Esum | Osum deriving Eq
eo :: DAut Int Bool EO
eo = DAut {delta = \case Esum -> f . even; Osum -> f . odd,
beta = (== Esum)}
where f b = if b then Esum else Osum
Die finale DAut(X, Y )-Algebra Beh(X, Y ) (siehe 2.15) kann wie folgt implementiert werden:
behFun :: DAut x y ([x] -> y)
behFun = DAut {delta = \f x -> f . (x:), beta = ($ [])}
Die folgenden Funktionen implementieren die Entfaltungen von Elementen einer beliebigen
DAut(X, Y )-Algebra alg zu Elementen von Beh(X, Y ) bzw. DTDAut(X,Y ):
unfoldDAutF :: DAut x y state -> state -> [x] -> y
unfoldDAutF alg s = \case [] -> beta alg s
x:w -> unfoldDAutF alg (delta alg s x) w
unfoldDAut :: DAut x y state -> state -> StateC x y
unfoldDAut alg s = DA {deltaC = unfoldDAut alg . delta alg s,
betaC = beta alg s}
245
Nach Beispiel 2.23 realisieren die initialen Automaten (eo, Esum) und (eo, Osum) die Verhaltensfunktion even ◦ sum : Z∗ → 2 bzw. odd ◦ sum : Z∗ → 2.
Da eo Acc(Z)-isomorph zur Unteralgebra von A = DAutC(Z)(Bool ) mit der Trägermenge {esum, osum} ist, werden die beiden Funktionen auch von den initialen Automaten
(A, esum) und (A, osum) realisiert. Es gelten also folgende Gleichungen:
unfoldDAutF eo Esum = even . sum = unfoldDAutF dAutC esum
unfoldDAutF eo Osum = odd . sum = unfoldDAutF dAutC osum
o
(siehe Beispiel 4.2 und Java.hs)
data JavaLight commands command sum_ sumsect prod prodsect factor
disjunct conjunct literal =
JavaLight {seq_
:: command -> commands -> commands,
embed
:: command -> commands,
block
:: commands -> command,
assign
:: String -> sum_ -> command,
cond
:: disjunct -> command -> command -> command,
cond1,loop :: disjunct -> command -> command,
sum_
:: prod -> sumsect -> sum_,
sumsect
:: String -> prod -> sumsect -> sumsect,
nilS
:: sumsect,
246
prod
prodsect
nilP
embedI
var
encloseS
disjunct
embedC
conjunct
embedL
not_
atom
embedB
encloseD
::
::
::
::
::
::
::
::
::
::
::
::
::
::
factor -> prodsect -> prod,
String -> factor -> prodsect -> prodsect,
prodsect,
Int -> factor,
String -> factor,
sum_ -> factor,
conjunct -> disjunct -> disjunct,
conjunct -> disjunct,
literal -> conjunct -> conjunct,
literal -> conjunct,
literal -> literal,
sum_ -> String -> sum_ -> literal,
Bool -> literal,
disjunct -> literal}
11.3 Die Termalgebra von JavaLight
Zunächst wird die Menge der JavaLight-Terme analog zu den TList(X) und TReg(BS) (siehe
10.1 bzw. 10.2) durch Datentypen implementiert und zwar jeweils einen für jede Sorte von
JavaLight:
data Commands
data Command
= Seq (Command,Commands) | Embed Command deriving Show
= Block Commands | Assign (String,Sum) |
Cond (Disjunct,Command,Command) | Cond1 (Disjunct,Command) |
247
data
data
data
data
data
data
data
data
Sum
Sumsect
Prod
Prodsect
Factor
Disjunct
Conjunct
Literal
=
=
=
=
=
=
=
=
Loop (Disjunct,Command) deriving Show
Sum (Prod,Sumsect) deriving Show
Sumsect (String,Prod,Sumsect) | NilS deriving Show
Prod (Factor,Prodsect) deriving Show
Prodsect (String,Factor,Prodsect) | NilP deriving Show
EmbedI Int | Var String | EncloseS Sum deriving Show
Disjunct (Conjunct,Disjunct) | EmbedC Conjunct deriving Show
Conjunct (Literal,Conjunct) | EmbedL Literal deriving Show
Not Literal | Atom (Sum,String,Sum) | EmbedB Bool |
EncloseD Disjunct deriving Show
javaTerm :: JavaLight Commands Command Sum Sumsect Prod Prodsect Factor
Disjunct Conjunct Literal
javaTerm = JavaLight {seq_ = curry Seq, embed = Embed, block = Block,
assign = curry Assign, cond = curry3 Cond, cond1 = curry Cond1,
loop = curry Loop, sum_ = curry Sum, sumsect = curry3 Sumsect,
nilS = NilS, prod = curry Prod, prodsect = curry3 Prodsect,
nilP = NilP, embedI = EmbedI, var = Var, encloseS = EncloseS,
disjunct = curry Disjunct, embedC = EmbedC,
conjunct = curry Conjunct, embedL = EmbedL, not_ = Not,
atom = curry3 Atom, embedB = EmbedB, encloseD = EncloseD}
11.4 Die Wortalgebra von JavaLight
javaWord :: JavaLight String String String String String String String String
String String
248
javaWord = JavaLight {seq_
embed
block
assign
cond
cond1
loop
sum_
sumsect
nilS
prod
prodsect
nilP
embedI
var
encloseS
disjunct
embedC
conjunct
embedL
not_
atom
embedB
encloseD
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
(++),
id,
\cs -> " {"++cs++"}",
\x e -> x++" = "++e++"; ",
\e c c' -> "if "++e++c++" else"++c',
\e c -> "if "
++e++c,
\e c -> "while "++e++c,
(++),
\op e f -> op++e++f,
"",
(++),
\op e f -> op++e++f,
"",
show,
id,
\e -> '(':e++")",
\e e' -> e++" || "++e',
id,
\e e' -> e++" && "++e',
id,
\be -> '!':be,
\e rel e' -> e++rel++e',
show,
\e -> '(':e++")"}
249
11.5 Das Zustandsmodell von JavaLight (siehe Beispiel 4.9)
type St a = Store -> a
rel :: String -> Int -> Int -> Bool
rel str = case str of "<" -> (<)
">" -> (>)
"<=" -> (<=)
">=" -> (>=)
"==" -> (==)
"!=" -> (/=)
javaState :: JavaLight (St Store) (St Store) (St Int) (St (Int -> Int)) (St Int)
(St (Int -> Int)) (St Int) (St Bool) (St Bool) (St Bool)
javaState = JavaLight {seq_
= flip (.),
embed
= id,
block
= id,
assign
= \x e st -> update st x $ e st,
cond
= cond,
cond1
= \f g -> cond f g id,
loop
= loop,
sum_
= \f g st -> g st $ f st,
sumsect
= \str f g st i -> g st $ op str i $ f st,
nilS
= const id,
prod
= \f g st -> g st $ f st,
250
prodsect
nilP
embedI
var
encloseS
disjunct
embedC
conjunct
embedL
not_
atom
embedB
encloseD
=
=
=
=
=
=
=
=
=
=
=
=
=
\str f g st i -> g st $ op str i $ f st,
const id,
const,
flip ($),
id,
liftM2 (||),
id,
liftM2 (&&),
id,
(not .),
\f str -> liftM2 (rel str) f,
const,
id}
where cond :: St Bool -> St Store -> St Store -> St Store
cond f g h st = if f st then g st else h st
loop :: St Bool -> St Store
loop f g = cond f (loop f g
op str = case str of "+" ->
"-" ->
"*" ->
"/" ->
-> St Store
. g) id
(+)
(-)
(*)
div
251
Z.B. hat das JavaLight-Programm
prog = fact = 1; while x > 1 {fact = fact*x; x = x-1;}
die folgende Interpretation in A = javaState:
compileA
JavaLight (prog) : Store → Store
store 7→ λz.if z = x then 0
else if z = fact then store(x)! else store(z)
Ausdrücke werden hier so interpretiert wie im zweiten in Beispiel 4.9 definierten Modell von
JavaLight.
Da die Ausführung von Kommandos, zumindest von denen, die loop enthalten, manchmal
nicht terminieren, müssen sie als partielle Funktionen interpretiert werden, was die Erweiterung der Trägermengen des Zustandsmodells zu ω-CPOs (halbgeordneten Mengen mit Kettensuprema und kleinstem Element) erfordert (siehe Kapitel 19). Tatsächlich implementiert
die Haskell-Funktion loop von javaState das zu einem ω-CPO erweiterte Zustandsmodell,
dessen Details in Abschnitt 19.1 zu finden sind.
o
252
11.6 Die Ableitungsbaumalgebra von JavaLight
type TS = Tree String
javaDeri :: JavaLight TS TS TS TS TS TS TS TS TS TS
javaDeri = JavaLight {seq_
= \c c' -> F "Commands" [c,c'],
embed
= \c -> F "Commands" [c],
block
= \c -> command [c],
assign
= \x e -> command [leaf x,leaf "=",e,leaf ";"],
cond
= \e c c' -> command [leaf "if",e,c,leaf "else",c'],
cond1
= \e c -> command [leaf "if",e,c],
loop
= \e c -> command [leaf "while",e,c],
sum_
= \e f -> F "Sum" [e,f],
sumsect
= \op e f -> F "Sumsect" [leaf op,e,f],
nilS
= leaf "Sumsect",
prod
= \e f -> F "Prod" [e,f],
prodsect
= \op e f -> F "Prodsect" [leaf op,e,f],
nilP
= leaf "Prodsect",
embedI
= \i -> factor [leaf $ show i],
var
= \x -> factor [leaf x],
encloseS
= \e -> factor [leaf "(",e,leaf ")"],
disjunct
= \e e'-> F "Disjunct" [e,leaf "||",e'],
embedC
= \e -> F "Disjunct" [e],
conjunct
= \e e'-> F "Conjunct" [e,leaf "&&",e'],
embedL
= \e -> F "Conjunct" [e],
253
not_
= \be -> literal [leaf "!",be],
atom
= \e rel e' -> literal [e,leaf rel,e'],
embedB
= \b -> literal [leaf $ show b],
encloseD
= \e -> literal [leaf "(",e,leaf ")"]}
where command = F "Command"
factor = F "Factor"
literal = F "Literal"
leaf = flip F []
254
11.7 Datentyp der XMLstore-Algebren (siehe Beispiel 4.3 und Compiler.hs)
data XMLstore store orders person emails email items stock suppliers
id =
XMLstore {store
:: stock -> store,
storeO
:: orders -> stock -> store,
orders
:: person -> items -> orders -> orders,
embedO
:: person -> items -> orders,
person
:: String -> person,
personE
:: String -> emails -> person,
emails
:: email -> emails -> emails,
none
:: emails,
email
:: String -> email,
items
:: id -> String -> items -> items,
embedI
:: id -> String -> items,
stock
:: id -> Int -> suppliers -> stock -> stock,
embedS
:: id -> Int -> suppliers -> stock,
supplier :: person -> suppliers,
parts
:: stock -> suppliers,
id_
:: String -> id}
255
12 Attributierte Übersetzung
Um die Operationen der Zielalgebra einer Übersetzung induktiv definieren zu können,
müssen Trägermengen oft parametrisiert werden oder die Wertebereiche bereits parametrisierter Trägermengen um zusätzliche Komponenten erweitert werden, die zur Berechnung
von Parametern rekursiver Aufrufe des Übersetzers benötigt werden. Die Zielalgebra A hat
dann die Form
Av1 × . . . × Avm → Aa1 × . . . × Aan .
(1)
Die Indizes v1, . . . , vm und a1, . . . , an heißen vererbte Attribute (inherited attributes)
bzw. abgeleitete Attribute (derived, synthesized attributes) von s. Für alle 1 ≤ i ≤ m
und 1 ≤ j ≤ n ist Avi bzw. Aaj die Menge der möglichen Werte des Attributs vi bzw. aj .
Vererbte Attribut(wert)e sind z.B. Positionen, Adressen, Schachtelungstiefen, etc. Abgeleitete Attribut(wert)e sind das eigentliche Zielobjekt wie auch z.B. dessen Typ, Größe oder
Platzbedarf, das selbst einen Wert eines abgeleiteten Attributs bildet.
Gleichzeitig vererbte und abgeleitete Attribute heißen transient. Deren Werte bilden den
Zustandsraum, der einen Compiler zur Transitionsfunktion eines Automaten macht, dessen
Ein- und Ausgaben Quell- bzw. Zielprogramme sind.
256
Kurz gesagt, ergänzen Attribute einen Syntaxbaum um Zusatzinformation, die erforderlich
ist, um ein bestimmtes Übersetzungsproblem zu lösen. In unserem generischen Ansatz sind
sie aber nicht – wie beim klassischen Begriff einer Attributgrammatik – Dekorationen von
Syntaxbäumen, sondern Komponenten der jeweiligen Zielalgebra.
Nach der Übersetzung in eine Zielfunktion vom Typ (1) werden alle vererbten Attribute
mit bestimmten Anfangswerten initialisiert. Dann werden die Zielfunktion auf die Anfangswerte angewendet und am Schluss das Ergebnistupel abgeleiteter Attributwerte mit π1 auf
die erste Komponente, die das eigentliche Zielobjekt darstellt, projiziert.
Beispiel 12.1 Binärdarstellung rationaler Zahlen (siehe auch Compiler.hs)
konkrete Syntax G
abstrakte Syntax Σ(G)
rat → nat. | rat0 | rat1
mkRat : nat → rat
app0, app1 : rat → rat
nat → 0 | 1 | nat0 | nat1
0, 1 : 1 → nat
app0, app1 : nat → nat
257
Die Zielalgebra A
Arat enthält neben dem eigentlichen Zielobjekt ein weiteres abgeleitetes Attribut mit Wertebereich Q, welches das Inkrement liefert, um das sich der Dezimalwert einer rationalen
Zahl erhöht, wenn an die Mantisse ihrer Binärdarstellung eine 1 angefügt wird.
Anat = N
Arat = Q × Q
0A : Anat
1A : Anat
0A = 0
1A = 1
app0A : Anat → Anat
app1A : Anat → Anat
app0A(n) = n ∗ 2
app1A(n) = n ∗ 2 + 1
mkRatA : Anat → Arat
mkRatA(val) = (val, 1)
app0A : Arat → Arat
app1A : Arat → Arat
app0A(val, inc) = (val, inc/2)
app1A(val, inc)) = (val + inc/2, inc/2)
258
Beispiel 12.2 Strings mit Hoch- und Tiefstellungen
konkrete Syntax G
abstrakte Syntax Σ(G)
string → string box |
app : string × box → string
string ↑ box |
up : string × box → string
string ↓ box |
down : string × box → string
box
mkString : box → string
box → (string) |
Char
mkBox : string → box
embed : Char → box
Die Zielalgebra A
Astring und Abox enthalten
• ein vererbtes Attribut mit Wertebereich N2, das die Koordinaten der linken unteren
Ecke des Rechtecks liefert, in das der eingelesene String geschrieben wird,
• neben dem eigentlichen Zielobjekt ein weiteres abgeleitetes Attribut mit Wertebereich
N3, das die Länge sowie - auf eine feste Grundlinie bezogen - Höhe und Tiefe des
Rechtecks liefert.
Astring = Abox = Strings mit Hoch- und Tiefstellungen × N2 → N3
259
appA : Astring × Abox → Astring
appA(f, g)(x, y) = (str str0, l + l0, max h h0, max t t0)
where (str, l, h, t) = f (x, y)
(str0, l0, h0, t0) = g(x + l, y)
upA : Astring × Abox → Astring
0
upA(f, g)(x, y) = (strstr , l + l0, h + h0 − 1, max t (t0 − h + 1))
where (str, l, h, t) = f (x, y)
(str0, l0, h0, t0) = g(x + l, y + h − 1)
downA : Astring × Abox → Astring
downA(f, g)(x, y) = (strstr0 , l + l0, max h (h0 − t − 1), t + t0 − 1)
where (str, l, h, t) = f (x, y)
(str0, l0, h0, t0) = g(x + l, y − t + 1)
mkString A : Abox → Astring
mkString A(f ) = f
mkBoxA : Astring → Abox
embedA : Char → Abox
mkBoxA(f ) = f
emdedA(c)(x, y) = (c, 1, 2, 0)
260
Beispiel 12.3 Übersetzung regulärer Ausdrücke in erkennende Automaten
Die folgenden Ersetzungsregeln beschreiben die schrittweise Übersetzung eines regulären
Ausdrucks (= Reg(BS)-Grundterms) t in einen nichtdeterministischen Automaten, der die
Sprache von t erkennt (siehe 2.17). Die Regeln basieren auf [9], Abschnitt 3.2.3).
Zunächst wird die erste Regel auf t angewendet. Es entsteht ein Graph mit zwei Knoten
und einer mit t markierten Kante. Dieser wird nun mit den anderen Regeln schrittweise
verfeinert, bis an allen seinen Kanten nur noch Elemente von BS stehen. Dann stellt er den
Transitionsgraphen eines Automaten dar, der t erkennt.
261
t
0
t
1
t
s
par(t,t')
s'
s
s'
t'
s
seq(t,t')
s'
s
t'
t
s'
ε
s
iter(t)
s'
s
ε
ε
t
s'
ε
s
base(ø)
s'
s
s
base(B)
s'
s
s'
B
s'
B
BS\{ø}
262
1
0
Mit obigen Regeln aus dem regulären Ausdruck t = (aa∗ + bc(bc)∗)∗
in Wortdarstellung (siehe 2.14) konstruierter Automat,
der die Sprache von t erkennt
263
Die Zielalgebra regNDA
Jede Regel außer der ersten entspricht der Interpretation einer Operation von Reg(BS) in
folgender Reg(BS)-Algebra.
regNDAreg = (NDA × N3 → NDA × N)
Hierbei ist NDA = N → (BS → N∗) der Typ nichtdeterministischer erkennender
Automaten mit ganzzahligen Zuständen und Eingaben aus BS. regNDAreg enthält
• den Automaten als transientes Attribut, das mit der Funktion λn.λs. (Automat ohne
Zustandsübergänge) initialisiert wird,
• zwei vererbte Attribute mit Wertebereich N, die mit 0 bzw. 1 initialisiert werden und
den Start- bzw. Endzustand des Automaten bezeichnen,
• ein transientes Attribut mit Wertebereich N, das mit 2 initialisiert wird und die nächste
ganze Zahl liefert, die als Zustandsname vergeben werden kann.
Die Operationen von Reg(BS) werden von regNDA wie folgt interpretiert:
parregNDA : regNDAreg × regNDAreg → regNDAreg
parregNDA(f, g)(δ, s, s0, next) = g(δ 0, s, s0, next0)
where (δ 0, next0) = f (δ, s, s0, next)
264
seq regNDA : regNDAreg × regNDAreg → regNDAreg
seq regNDA(f, g)(δ, s, s0, next) = g(δ 0, next, s0, next0)
where (δ 0, next0) = f (δ, s, next, next + 1)
iterregNDA : regNDAreg → regNDAreg
iterregNDA(f )(δ, s, s0, next) = (addTo(δ4)(s)()(s0), next3)
where next1 = next + 1
next2 = next1 + 1
(δ1, next3) = f (δ, next, next1, next2)
δ2 = addTo(δ1)(s)()(next)
δ3 = addTo(δ2)(next1)()(next)
δ4 = addTo(δ3)(next1)()(s0)
baseregNDA : BS → regNDAreg
baseregNDA(∅)(δ, s, s0, next) = (δ, next)
∀ B ∈ BS \ {∅} : baseregNDA(B)(δ, s, s0, next) = (addTo(δ)(s)(B)(s0), next)
x
addTo(δ)(s)(x)(s0) fügt die Transition s → s0 zur Transitionsfunktion δ hinzu.
265
Der durch Auswertung eines regulären Ausdrucks (= Reg(BS)-Grundterms) in regNDA
erzeugte Automat nda ∈ NDA ist nichtdeterministisch und hat -Übergänge. Er lässt sich
wie üblich in einen deterministischen Potenzautomaten ohne -Übergänge transformieren.
Da 0 der Anfangszustand, 1 der Endzustand und auch alle anderen Zustände von nda
ganze Zahlen sind, lassen sich alle Zustände des Potenzautomaten pow(nda) als Listen
ganzer Zahlen darstellen. pow(nda) ist, zusammen mit seinem Anfangszustand, der -Hülle
des Anfangszustandes 0 von nda, eine DAut(BS, 2)-Algebra mit Zustandsmenge Z∗ (siehe
2.13):
accNDA :: NDA -> (DAut BS Bool [Int], [Int])
accNDA nda = (DAut {delta = (epsHull .) . deltaP, beta = (1 `elem`)},
epsHull [0])
where deltaP :: [Int] -> BS -> [Int]
deltaP qs x = unionMap (flip nda x) qs
epsHull :: [Int] -> [Int]
epsHull qs = if qs' `subset` qs
then qs else epsHull $ qs `union` qs'
where qs' = deltaP qs regNDA und accNDA sind im Haskell-Modul Compiler.hs implementiert.
266
12.4 Darstellung von Termen als hierarchische Listen
Mit folgender JavaLight-Algebra javaList wird z.B. der Syntaxbaum des Programms
fact = 1; while x > 1 {fact = fact*x; x = x-1;}
in die Form einer hierarchischen Liste gebracht:
267
Die Trägermengen von javaList enthalten zwei vererbte Attribute vom Typ 2 bzw. Z.
Der Boolesche Wert gibt an, ob der jeweilige Teilstring str hinter oder unter den vorangehenden zu schreiben ist. Im zweiten Fall wird str um den Wert des zweiten Attributs
eingerückt.
type BIS = Bool -> Int -> String
javaList :: JavaLight BIS BIS BIS BIS BIS BIS BIS BIS BIS BIS
javaList = JavaLight {seq_
= indent2 "commands",
embed
= indent1 "embed",
block
= indent1 "block",
assign
= \x e -> indent2 "assign" (indent0 x) e,
cond
= indent3 "cond",
cond1
= indent2 "cond1",
loop
= indent2 "loop",
sum_
= indent2 "sum",
sumsect = \op e f -> indent3 "sumsect" (indent0 op) e f,
nilS
= indent0 "nilS",
prod
= indent2 "prod",
prodsect = \op e f -> indent3 "prodsect" (indent0 op) e f,
nilP = indent0 "nilP",
embedI
= \i -> indent1 "embedI" $ indent0 $ show i,
var
= \x -> indent1 "var" $ indent0 x,
encloseS = indent1 "encloseS",
268
disjunct = indent2 "disjunct",
embedC
= indent1 "embedC",
conjunct = indent2 "conjunct",
embedL
= indent1 "embedL",
not_
= indent1 "not",
atom
= \e rel e'-> indent3 "atom" e (indent0 rel) e',
embedB
= \b -> indent1 "embedB" $ indent0 $ show b,
encloseD = indent1 "encloseD"}
where indent0 x
= blanks x []
indent1 x f
= blanks x [f]
indent2 x f g
= blanks x [f,g]
indent3 x f g h = blanks x [f,g,h]
blanks :: String -> [BIS] -> BIS
blanks x fs b n = if b then str else '\n':replicate n ' '++str
where str = case fs of f:fs -> x++' ':g True f++
concatMap (g False) fs
_ -> x
g b f = f b $ n+length x+1
269
12.5 Eine kellerbasierte Zielsprache für JavaLight
Der folgende Datentyp liefert die Befehle einer Assemblersprache, die auf einem Keller vom
Typ Z und einem Speicher vom Typ
Store = String → Z
operiert. Letzterer ist natürlich nur die Abstraktion eines realen Speichers, auf dessen Inhalt
nicht über Strings, sondern über Adressen, z.B. Kellerpositionen, zugegriffen wird. Ein solche
– realistischere – Assemblersprache wird erst im nächsten Kapitel behandelt.
data StackCom = Push Int | Pop | Load String | Save String | Add |
Sub | Mul | Div | Or_ | And_ | Inv | Cmp String |
Jump Int | JumpF Int
Diese Befehle bilden die möglichen Eingaben eines endlichen Automaten, dessen Zustände
jeweils aus einem Kellerinhalt und einer Variablenbelegung bestehen:
type State = ([Int],Store,Int)
Die Bedeutung der Befehle ergibt sich aus ihrer Verarbeitung durch eine Transitionsfunktion
executeCom:
executeCom :: StackCom -> State -> State
270
executeCom com (stack,store,n) =
case com of Push a
-> (a:stack,store,n+1)
Pop
-> (tail stack,store,n+1)
Load x
-> (store x:stack,store,n+1)
Save x
-> (stack,update store x $ head stack,n+1)
Add
-> (a+b:s,store,n+1) where a:b:s = stack
Sub
-> (b-a:s,store,n+1) where a:b:s = stack
Mul
-> (a*b:s,store,n+1) where a:b:s = stack
Div
-> (b`div`a:s,store,n+1) where a:b:s = stack
Or_
-> (max a b:s,store,n+1) where a:b:s = stack
And_
-> (a*b:s,store,n+1) where a:b:s = stack
Inv
-> ((a+1)`mod`2:s,store,n+1) where a:s = stack
Cmp str
-> (c:s,store,n+1)
where a:b:s = stack
c = if rel str a b then 1 else 0
-- siehe 11.5
Jump k
-> (stack,store,k)
JumpF k
-> (stack,store,if a == 0 then k else n+1)
where a:_ = stack
Sprungbefehle können nur im Kontext einer vollständigen Befehlsfolge cs ausgeführt werden.
271
n ist die Position des Befehls von cs, den der Interpreter als nächsten ausführt:
execute :: [StackCom] -> State -> State
execute cs state@(_,_,n) = if n >= length cs then state
else execute cs $ executeCom (cs!!n) state
Um Sprungziele, also die ganzzahligen Parameter der Sprungbefehle, berechnen zu können,
muss der Compiler die Position jedes erzeugten Befehls innerhalb des gesamten Zielprogramms kennen.
Dieses erzeugt er durch Interpretation des (abstrakten) Quellprogramms in der folgenden
JavaLight-Algebra javaStack , deren Trägermengen neben dem jeweiligen Zielcode code
ein (vererbtes) Attribut haben, das die Nummer des ersten Befehls von code wiedergibt.
Dementsprechend interpretiert javaStack alle Sorten von JavaLight durch den Funktionstyp
LCom = Int -> [StackCom]
javaStack :: JavaLight LCom LCom LCom LCom LCom LCom LCom LCom LCom LCom
javaStack = JavaLight {seq_
= seq_,
embed
= id,
block
= id,
assign
= \x e lab -> e lab++[Save x,Pop],
cond
= \e c c' lab
272
cond1
loop
sum_
sumsect
nilS
prod
prodsect
nilP
embedI
var
encloseS
disjunct
embedC
conjunct
embedL
not_
atom
embedB
encloseD
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
-> let (code,exit) = fork e c 1 lab
code' = c' exit
in code++Jump (exit+length code'):code',
\e c lab -> fst $ fork e c 0 lab,
\e c lab -> fst (fork e c 1 lab)++[Jump lab],
apply2 "",
\op -> apply2 op . apply1 op,
const [],
apply2 "",
\op -> apply2 op . apply1 op,
const [],
\i -> const [Push i],
\x -> const [Load x],
id,
apply2 "|",
id,
apply2 "&",
id,
apply1 '!',
\e rel -> apply2 rel e,
\b -> const [Push $ if b then 1 else 0],
id}
273
where apply1 :: String -> LCom -> LCom
apply1 op e lab = e lab++[case op of "!" -> Inv
"+" -> Add;
"*" -> Mul;
"-" -> Sub
"/" -> Div]
seq_ :: LCom -> LCom -> LCom
seq_ e e' lab = code++e' (lab+length code)
where code = e lab
apply2 :: String -> LCom -> LCom -> LCom
apply2 op e e' lab = code++e' (lab+length code)++
[case op of "|" -> Or_
"&" -> And_
_
-> Cmp op]
where code = e lab
fork :: LCom -> LCom -> Int -> Int -> ([StackCom],Int)
fork e c n lab = (code++JumpF exit:code',exit)
where code = e lab
lab' = lab+length code+1
code' = c lab'
exit = lab'+length code'+n
274
Beispiel 12.6 (Fakultätsfunktion) Steht das JavaLight-Programm
fact = 1; while x > 1 {fact = fact*x; x = x-1;}
in der Datei prog, dann übersetzt es javaToAlg "prog" 6 (siehe Java.hs) in ein Zielprogramm vom Typ [StackCom] und schreibt dieses in die Datei javatarget ab. Es lautet
wie folgt:
0:
1:
2:
3:
4:
5:
6:
7:
Push 1
Save "fact"
Pop
Load "x"
Push 1
Cmp ">"
JumpF 18
Load "fact"
8:
9:
10:
11:
12:
13:
14:
15:
Load
Mul
Save
Pop
Load
Push
Sub
Save
"x"
16: Pop
17: Jump 3
"fact"
"x"
1
"x"
275
13 JavaLight+ = JavaLight + I/O + Deklarationen + Prozeduren
(siehe Java2.hs)
13.1 Assemblersprache mit I/O und Kelleradressierung
Die Variablenbelegung store : String → Z im Zustandsmodell von Abschnitt Assemblerprogramme als JavaLight-Zielalgebra wird ersetzt durch den Keller stack ∈ Z∗, der jetzt
nicht nur der schrittweisen Auswertung von Ausdrücken dient, sondern auch der Ablage von
Variablenwerten unter vom Compiler berechneten Adressen. Weitere Zustandskomponenten
sind:
• der Inhalt des Registers BA für die jeweils aktuelle Basisadresse (s.u.),
• der Inhalt des Registers STP für die Basisadresse des statischen Vorgängers des jeweils
zu übersetzenden Blocks bzw. Funktionsaufrufs (s.u.),
• der schon in Abschnitt 12.5 benutzte Befehlszähler pc (program counter),
• der Ein/Ausgabestrom io, auf den Lese- bzw. Schreibbefehle zugreifen.
Der entsprechende Datentyp lautet daher wie folgt:
data State = State {stack,io :: [Int], ba,stp,pc :: Int}
276
Bei der Übersetzung eines Blocks b oder Prozeduraufrufs f (es) reserviert der Compiler
Speicherplatz für die Werte aller lokalen Variablen des Blocks b bzw. Rumpfs (der Deklaration) von f . Dieser Speicherplatz ist Teil eines b bzw. f (es) zugeordneten Kellerabschnitts
(stack frame, activation record). Dessen Anfangsadresse ist die Basisadresse ba der lokalen Variablen. Die absolute Adresse einer lokalen Variablen x ist die Kellerposition,
an welcher der jeweils aktuelle Wert von x steht. Sie ist immer die Summe von ba und
dem – Relativadresse von x genannten und vom Compiler in der Symboltabelle (s.u.)
gespeicherten – Abstand zum Beginn des Kellerabschnitts.
Wird zur Laufzeit der Block b bzw. – als Teil der Ausführung von f (es) – der Rumpf von f
betreten, dann speichert ein vom Compiler erzeugter Befehl die nächste freie Kellerposition
als aktuelle Basisadresse im Register BA.
Der statische Vorgänger von b bzw. f (es) ist der innerste Block bzw. Prozedurrumpf,
in dem b bzw. die Deklaration von f steht. Der b bzw. f (es) zugeordnete Kellerabschnitt
beginnt stets mit dem Display, das aus den – nach aufsteigender Schachtelungstiefe geordneten – Basisadressen der Kellerabschnitte aller umfassenden Blöcke und Prozedurrümpfe
besteht.
277
Der Compiler erzeugt und verwendet keine ganzzahligen, sondern nur symbolische Adressen, d.h. Registernamen (BA, STP, TOP) oder mit einer ganzen Zahl indizierte (inDexed)
Adressen der Form Dex(adr)(i):
data SymAdr = BA | STP | TOP | Dex SymAdr Int | Con Int
Der Inhalt von TOP ist immer die Adresse der ersten freien Kellerposition. Ist s der Kellerinhalt, dann entspricht die absolute Adresse adr der Kellerposition |s| − 1 − adr.
278
Die folgende Funktion baseAdr berechnet die jeweils aktuelle (symbolische) Basisadresse:
baseAdr :: Int -> Int -> SymAdr
baseAdr declDep dep = if declDep == dep then BA else Dex BA declDep
Der Compiler ruft baseAdr bei der Übersetzung jeder Verwendung einer Variable x auf.
declDep und dep bezeichnen die Deklarations- bzw. Verwendungstiefe von x. Stimmen
beide Werte überein, dann ist x eine lokale Variable und die Basisadresse von x steht (zur
Laufzeit) im Register BA.
Andernfalls ist x eine globale Variable, d.h. x wurde in einem Block bzw. Prozedurrumpf
deklariert, der denjenigen, in dem x verwendet wird, umfasst (declDep<dep). Die Basisadresse von x ist in diesem Fall nicht im Register BA, sondern unter dem Inhalt der
declDep-ten Position des dem Block bzw. Prozedurrumpf, in dem x verwendet wird, zugeordneten Kellerabschnitt zu finden (s.o.).
Die folgenden Funktionen berechnen aus symbolischen Adressen absolute Adressen bzw.
Kellerinhalte:
absAdr,contents :: State -> SymAdr -> Int
absAdr _ (Con i)
= i
absAdr state BA
= ba state
absAdr state STP
= stp state
279
absAdr state TOP
absAdr state (Dex BA i)
absAdr state (Dex STP i)
absAdr state (Dex TOP i)
absAdr state (Dex adr i)
contents state (Dex adr i)
contents state adr
=
=
=
=
=
=
length $ stack state
ba state+i
stp state+i
length (stack state)+i
contents state adr+i
s!!(k-i)
where (s,k) = stackPos state adr
= absAdr state adr
stackPos :: State -> SymAdr -> ([Int],Int)
stackPos state adr = (s,length s-1-contents state adr)
where s = stack state
updState
updState
updState
updState
:: State -> SymAdr ->
state BA x
=
state STP x
=
state (Dex adr i) x =
Int -> State
state {ba = x}
state {stp = x}
state {stack = updList s (k-i) x}
where (s,k) = stackPos state adr
Mit der Anwendung von absAdr oder contents auf eine – evtl. geschachtelte – indizierte
Adresse wird eine Folge von Kelleradressen und -inhalten durchlaufen.
280
absAdr liefert die letzte Adresse der Folge, contents den Kellerinhalt an der durch diese
Adresse beschriebenen Position.
Die Zielprogramme von Javalight+ setzen sich aus folgenden Assemblerbefehlen mit symbolischen Adressen zusammen:
data StackCom = PushA SymAdr | Push SymAdr | Pop | Save SymAdr |
Move SymAdr SymAdr | Add | Sub | Mul | Div | Or_ |
And_ | Inv | Cmp String | Jump SymAdr | JumpF Int |
Read SymAdr | Write
Analog zu Abschnitt 12.5 ist die Bedeutung der Befehle durch eine Transitionsfunktion
executeCom gegeben – die jetzt auch die Sprungbefehle verarbeitet:
executeCom :: StackCom -> State -> State
executeCom com state =
case com of
PushA adr
-> state' {stack = absAdr state adr:stack state}
Push adr
-> state' {stack = contents state adr:
stack state}
Pop
-> state' {stack = tail $ stack state}
Save adr
-> updState state' adr $ head $ stack state
281
Move adr adr' -> updState state' adr' $ contents state adr
Add
-> applyOp state' (+)
Sub
-> applyOp state' (-)
Mul
-> applyOp state' (*)
Div
-> applyOp state' div
Or_
-> applyOp state' max
And_
-> applyOp state' (*)
Inv
-> state' {stack = (a+1)`mod`2:s}
where a:s = stack state
Cmp rel
-> state' {stack = mkInt (evalRel rel b a):s}
where a:b:s = stack state
Jump adr
-> state {pc = contents state adr}
JumpF lab
-> state {pc = if a == 0 then lab
else pc state+1,
stack = s} where a:s = stack state
Read adr
-> if null s then state'
else (updState state' adr $ head s)
{io = tail s} where s = io state
Write
-> if null s then state'
else state' {io = io state++[head s]}
where s = stack state
282
where state' = state {pc = pc state+1}
applyOp :: State -> (Int -> Int -> Int) -> State
applyOp state op = state {stack = op b a:s}
where a:b:s = stack state
execute wird ebenfalls an das neue Zustandsmodell angepasst:
execute :: [StackCom] -> State -> State
execute cs state = if curr >= length cs then state
else execute cs $ executeCom (cs!!curr) state
where curr = pc state
13.2 Grammatik und abstrakte Syntax von JavaLight+
JavaLight+ enthält neben den Sorten von JavaLight die Sorten Formals und Actuals
für Listen formaler bzw. aktueller Parameter von Prozeduren. Auch die Basismengen von
JavaLight werden übernommen. Hinzu kommt eine für formale Parameter. Sie besteht aus
mit zwei Konstruktoren aus dem jeweiligen Parameternamen und einem Typdeskriptor
gebildeten Ausdruck:
283
data TypeDesc = INT | BOOL | UNIT | Fun TypeDesc Int | ForFun TypeDesc
data Formal
= Par String TypeDesc | FunPar String [Formal] TypeDesc
FunPar(x)(t) bezeichnet einen funktionalen formalen Parameter, also eine Prozedurvariable. Sie hat den Typ ForFun(t). t ist hier der Typ der Prozedurergebnisse. Demgegenüber bezeichnet Fun(t,lab) den Typ einer Prozedurkonstanten mit Ergebnistyp t
und Codeadresse lab.
Dementsprechend enthält JavaLight+ auch die Regeln von JavaLight. Hinzu kommen die
folgenden Regeln für Ein/Ausgabebefehle, Deklarationen, Prozeduraufrufe und Parameterlisten. Außerdem sind jetzt auch Boolesche Variablen und Zuweisungen an diese zugelassen.
Command →
Formals
Formals 0
ExpSemi
ExpBrac
ExpComm
Factor
→
→
→
→
→
→
read String; | write ExpSemi | {Commands} |
TypeDesc String Formals {Commands} | TypeDesc String; |
String = ExpSemi | String Formals {Commands} |
String Actuals
() | (Formals 0
Formal ) | Formal , Formals 0
Sum; | Disjunct;
Sum) | Disjunct)
Sum, | Disjunct,
String Actuals
284
Literal →
Actuals →
Actuals0 →
String | String Actuals
() | (Actuals0
ExpBrac | ExpComm Actuals0
In Beispiel 6.2 wurde begründet, warum drei verschiedene Sorten für Ausdrücke (ExpSemi ,
ExpBrac und ExpComm) benötigt werden. Beim Übergang zur abstrakten Syntax können
wir die Unterscheidung zwischen den drei Sorten wieder aufheben.
Erlaubt man nur JavaLight+-Algebren A, die Formals durch Formal ∗ und Actuals durch
(ASum + ADisjunct )∗ interpretieren, dann lautet der Datentyp der JavaLight+-Algebren wie
folgt (siehe Java2.hs):
data JavaLightP commands command exp sum_ sumsect prod prodsect
factor disjunct conjunct literal formals actuals =
JavaLightP {seq_
:: command -> commands -> commands,
embed
:: command -> commands,
block
:: commands -> command,
assign
:: String -> exp -> command,
applyProc :: String -> actuals -> command,
cond
:: disjunct -> command -> command -> command,
cond1,loop :: disjunct -> command -> command,
read_
:: String -> command,
write_
:: exp -> command,
vardecl
:: String -> TypeDesc -> command,
285
fundecl
formals
embedS
sum_
sumsect
nilS
prod
prodsect
nilP
embedI
varInt
applyInt
encloseS
embedD
disjunct
embedC
conjunct
embedL
not_
atom
embedB
varBool
applyBool
encloseD
actuals
::
::
::
::
::
::
::
::
::
::
::
::
::
::
::
::
::
::
::
::
::
::
::
::
::
String -> formals -> TypeDesc -> commands -> command,
[Formal] -> formals,
sum_ -> exp,
prod -> sumsect -> sum_,
String -> prod -> sumsect -> sumsect,
sumsect,
factor -> prodsect -> prod,
String -> factor -> prodsect -> prodsect,
prodsect,
Int -> factor,
String -> factor,
String -> actuals -> factor,
sum_ -> factor,
disjunct -> exp,
conjunct -> disjunct -> disjunct,
conjunct -> disjunct,
literal -> conjunct -> conjunct,
literal -> conjunct,
literal -> literal,
sum_ -> String -> sum_ -> literal,
Bool -> literal,
String -> literal,
String -> actuals -> literal,
disjunct -> literal,
[exp] -> actuals}
286
13.3 JavaLight+-Algebra javaStackP (siehe Java2.hs)
javaStackP hat wie javaStack nur eine Trägermenge (ComStack) für alle Kommandosorten
sowie eine Trägermenge (ExpStack) für alle Ausdruckssorten (außer denen für Sektionen):
type ComStack = Int -> Symtab -> Int -> Int -> ([StackCom],Symtab,Int)
type ExpStack = Int -> Symtab -> Int -> ([StackCom],TypeDesc)
type Symtab = String -> (TypeDesc,Int,Int)
Die Symboltabelle (vom Typ Symtab) ordnet jeder Variablen drei Werte zu: Typ,
Schachtelungstiefe ihrer Deklaration, also die Zahl der die Deklaration umfassenden Blöcke
und Prozedurrümpfe, sowie eine Relativadresse (s.o.).
Transiente Attribute der Kommandosorten sind die Symboltabelle und die nächste freie
Relativadresse (adr; s.u.).
Weitere vererbte Attribute der Kommando- und Ausdruckssorten sind die nächste freie Befehlsnummer (lab; s.u.) und die Schachtelungstiefe (depth; s.u.) des jeweiligen Kommandos
bzw. Ausdrucks.
Weitere abgeleitete Attribute der Ausdruckssorten sind der Zielcode und der Typdeskriptor
des jeweiligen Ausdrucks. Die Symboltabelle ist bei Ausdruckssorten nur vererbt.
287
Listen formaler bzw. aktueller Parameter werden von javaStackP als Elemente der folgenden Trägermengen interpretiert:
type FormsStack = Symtab -> Int -> Int -> ([ComStack],Symtab,Int)
type ActsStack = Int -> Symtab -> Int -> ([StackCom],Int)
Formale Parameter sind Teile von Funktionsdeklarationen. Deshalb haben Listen formaler Parameter Attribute von Kommandosorten: Symboltabelle, Schachtelungstiefe, nächste
freie Relativadresse und sogar zusätzlichen Quellcode (vom Typ [ComStack]), der aus jedem funktionalen Parameter eine lokale Funktionsdeklaration erzeugt (siehe Übersetzung
formaler Parameter).
Aktuelle Parameter sind Ausdrücke und haben deshalb (fast) die gleichen Attribute wie
Ausdruckssorten. Anstelle eines Typdeskriptors wird die Länge der jeweiligen Parameter
berechnet. Den Platz von TypeDesc nimmt daher Int ein.
Aktuelle Parameter können im Gegensatz zu anderen Vorkommen von Ausdrücken Prozedurvariablen sein. Der Einfachheit halber überprüft unser Compiler nicht, ob diese nur an
Parameterpositionen auftreten, so wie er auch Anwendungen arithmetischer Operationen
auf Boolesche Variablen oder Boolescher Operationen auf Variablen vom Typ Z ignoriert.
288
Die Typverträglichkeit von Deklarationen mit den Verwendungen der deklarierten Variablen
könnte aber mit Hilfe einer Variante von javaStackP überprüft werden, deren Trägermengen
um Fehlermeldungen angereichert sind.
Bis auf die Interpretation von Blöcken und die Einbindung der o.g. Attribute gleicht die Interpretation der Programmkonstrukte von JavaLight in javaStackP derjenigen in javaStack .
An die Stelle der Lade- und Speicherbefehle, die javaStack erzeugt und die zur Laufzeit
Daten von einer Variablenbelegung store : String → Z zum Keller bzw. vom Keller nach
store transportieren, treten die oben definierten Lade- und Speicherbefehle, die Daten oder
Kelleradressen zwischen Kellerplätzen hin- und herschieben.
javaStackP :: JavaAlgP ComStack ComStack ExpStack ExpStack ExpStack ExpStack
ExpStack ExpStack ExpStack ExpStack ExpStack FormsStack
ActsStack
javaStackP = JavaLightP {seq_
= seq_,
embed
= id,
block
= block,
assign
= assign,
applyProc
= applyProc,
cond
= cond,
cond1
= \e c -> (((fst .) .) .) . fork e c 0
loop
= loop,
read_
= read_,
write_
= write_,
289
vardecl
fundecl
formals
embedS
sum_
sumsect
nilS
prod
prodsect
nilP
embedI
varInt
applyInt
encloseS
embedD
disjunct
embedC
conjunct
embedL
not_
atom
embedB
varBool
applyBool
encloseD
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
vardecl,
fundecl,
formals,
id,
apply2 "",
\op -> apply2 "" . apply1 op,
_ _ -> ([],INT),
apply2 "",
\op -> apply2 "" . apply1 op,
\_ _ _ -> ([],INT),
\i _ _ _ -> ([Push $ Con i],INT),
var,
applyFun,
id,
id,
apply2 "|",
id,
apply2 "&",
id,
apply1 "!",
\e rel e' -> apply (Cmp rel) (e,e'),
\b _ _ _ -> ([Push $ Con $ mkInt b],BOOL),
var,
applyFun,
id,
290
actuals
= actuals}
where apply1 :: String -> ExpStack -> ExpStack
apply1 op e lab st dep = (fst (e lab st dep)++
[case op of "!" -> Inv; "+" -> Add; "-" -> Sub
"*" -> Mul; _ -> Div],
INT)
seq_ :: ComStack -> ComStack -> ComStack
seq_ c c' lab st dep adr = (code++code',st2,adr2)
where (code,st1,adr1) = c lab st dep adr
(code',st2,adr2) = c' (lab+length code) st1 dep adr1
apply2 :: String -> ExpStack -> ExpStack -> ExpStack
apply2 op e e' lab st dep = (code++code'++cs,td)
where (code,td) = e lab st dep
code' = fst $ e' (lab+length code) st dep
cs = case op of "" -> [];
"|" -> [Or_]
"&" -> [And_]; _ -> [Cmp op]
291
Übersetzung eines Blocks
block :: ComStack -> ComStack
block c lab st dep adr = (code',st,adr)
where bodylab = lab+dep+3; dep' = dep+1
(code,_,local) = c bodylab st dep' dep'
code' = Move TOP STP:pushDisplay BA dep++Move STP BA:
bodylab:
code++replicate (local-dep') Pop++Save BA:
replicate dep' Pop
pushDisplay :: Int -> SymAdr -> [StackCom]
pushDisplay dep reg = foldr push [Push reg] [0..dep-1]
where push i code = Push (Dex reg i):code
javaStackP umschließt im Gegensatz zu javaStack bei der Zusammenfassung einer Kommandofolge cs
zu einem Block den Code von cs mit zusätzlichen Zielcode:
Move TOP STP
Speichern des Stacktops im Register STP
pushDisplay dep BA
Kellern des Displays des umfassenden Blocks und dessen Adresse
Move STP BA
Der Inhalt von STP wird zur neuen Basisadresse
code
Zielcode für die Kommandofolge des Blocks
replicate (local-dep') Pop Entkellern der lokalen Variablen des Blocks
Save BA
Speichern der alten Basisadresse in BA
replicate dep' Pop
Entkellern des Displays
292
Übersetzung einer Zuweisung
assign :: String -> ExpStack -> ComStack
assign x e lab st dep adr = (fst (e lab st dep)++[Save $ Dex ba adrx,Pop],
st,adr)
where (_,declDep,adrx) = st x
ba = baseAdr declDep dep
Zielcodeerläuterung:
Zielcode für den Ausdruck e, der mit einem Befehl zur Kellerung des
aktuellen Wertes von e schließt
Save $ Dex ba adrx Speichern des aktuellen Wertes von e unter der Kelleradresse Dex ba adrx
von x, die sich aus der Basisadresse ba von x und der in der Symboltabelle
gespeicherten Relativadresse adrx von x ergibt
Pop
Entkellern des aktuellen Wertes von e
code
Übersetzung von Konditionalen und Schleifen
cond :: ExpStack -> ComStack -> ComStack -> ComStack
cond e c c' lab st dep adr = (code++Jump (Con $ exit+length code'):code',
st2,adr2)
where ((code,st1,adr1),exit) = fork e c 1 lab st dep adr
(code',st2,adr2) = c' exit st1 dep adr1
293
loop :: ExpStack -> ComStack -> ComStack
loop e c lab st dep adr = (code++[Jump $ Con lab],st',adr')
where (code,st',adr') = fst $ fork e c 1 lab st dep adr
fork :: ExpStack -> ComStack -> Int -> Int -> Symtab -> Int -> Int
-> (([StackCom],Int,Symtab,Int),Int)
fork e c n lab st dep adr = ((code++JumpF exit:code',st',adr'),exit)
where code = fst $ e lab st dep
lab' = lab+length code+1
(code',st',adr') = c lab' st dep adr
exit = lab'+length code'+n
Übersetzung eines Lesebefehls
read_ :: String -> ComStack
read_ x _ st dep adr = ([Read $ Dex ba adrx],st,adr)
where (_,declDep,adrx) = st x
ba = baseAdr declDep dep
Zielcodeerläuterung:
Read $ Dex ba adrx Speichern des ersten Elementes c des Ein/Ausgabestroms io unter der
Adresse Dex ba adrx, die sich aus der Basisadresse ba des
Kellerbereichs, in dem x deklariert wurde, und der in der Symboltabelle
gespeicherten Relativadresse adrx von x ergibt. c wird aus io entfernt.
294
Übersetzung eines Schreibbefehls
write_ :: ExpStack -> ComStack
write_ e lab st dep adr = (fst (e lab st dep)++[Write,Pop],st,adr)
Zielcodeerläuterung:
Zielcode für den Ausdruck e, der mit einem Befehl zur Kellerung des aktuellen Wertes
von e schließt
Write Anhängen des Wertes von e an den Ein/Ausgabestrom io
Pop
Entkellern des aktuellen Wertes von e
code
Übersetzung der Deklaration einer nichtfunktionalen Variable
vardecl :: String -> TypeDesc -> ComStack
vardecl x td _ st dep adr = ([Push $ Con 0],update st x (td,dep,adr),adr+1)
Reservierung eines Kellerplatzes für Werte von x
Außerdem trägt vardecl den zum Typ von x gehörigen Typdeskriptor sowie dep (aktuelle Schachtelungstiefe) und adr (nächste freie Relativadresse) unter x in die Symboltabelle ein.
295
Übersetzung einer Funktions- oder Prozedurdeklaration
fundecl :: String -> FormsStack -> TypeDesc -> ComStack -> ComStack
fundecl f pars td body lab st dep adr = (code',st1,adr+1)
where codelab = lab+2
st1 = update st f (Fun td codelab,dep,adr)
dep' = dep+1
(parcode,st2,_) = pars st1 dep' $ -2
coms = foldl1 seq_ $ parcode++[body]
bodylab = codelab+dep+2
(code,_,local) = coms bodylab st2 dep' dep'
retlab = Dex TOP $ -1
exit = bodylab+length code+local+1
code' = Push (Con 0):Jump (Con exit):
codelab:
Move TOP BA:pushDisplay dep STP++
bodylab:
code++replicate local Pop++[Jump retlab]
exit:
Zielcodeerläuterung:
Push $ Con 0
Jump $ Con exit
Reservierung eines Kellerplatzes für den Wert eines Aufrufs von f
Sprung hinter den Zielcode
Der Code zwischen codelab und exit wird erst bei der Ausführung des Zielcodes eines Aufrufs von f
abgearbeitet (siehe applyFun):
Move TOP BA
Die nächste freie Kellerposition wird zur neuen Basisadresse.
296
Dieser Befehl hat die von fundecl berechnete Nummer codelab
und wird angesprungen, wenn der Befehl Jump codelab
des Zielcodes eines Aufrufs von f ausgeführt wird (siehe applyProc).
pushDisplay dep STP Kellern des Displays des umfassenden Prozedurrumpfs und dessen Adresse
parcode++[body]
erweiterter Prozedurrumpf (siehe formals)
replicate local Pop Entkellern der lokalen Variablen und des Displays des Aufrufs von f
Jump retlab
Sprung zur Rücksprungadresse, die bei der Ausführung des Zielcodes
des Aufruf von f vor dem Sprung zur Adresse des Codes von f gekellert wurde,
so dass sie am Ende von dessen Ausführung wieder oben im Keller steht
Übersetzung formaler Parameter
formals :: [Formal] -> FormsStack
formals pars st dep adr = foldl f ([],st,adr) pars
where f (cs,st,adr) (Par x td) = (cs,update st x (td,dep,adr'),adr') (1)
where adr' = adr-1
f (cs,st,adr) (FunPar x@('@':g) pars td) = (cs++[c],st',adr')
(2)
where adr' = adr-3
st' = update st x (ForFun td,dep,adr')
c = fundecl g (formals pars) td $ assign g $
applyFun x $ actuals $ map act pars
act (Par x _)
= var x
act (FunPar (_:g) _ _) = var g
297
Formale Parameter einer Prozedur g sind Variablen mit negativen Relativadressen, weil ihre aktuellen
Werte beim Aufruf von g direkt vor dem für den Aufruf reservierten Kellerabschnitt abgelegt werden.
Eine Variable vom Typ INT (Fall 1) benötigt einen Kellerplatz für ihre aktuelle Basisadresse, eine
Prozedurvariable (Fall 2) hingegen drei:
• einen für die aktuelle Basisadresse, die hier die Anfangsadresse des dem aktuellen Prozeduraufruf
zugeordneten Kellerabschnitt ist,
• einen für die aktuelle Codeadresse und
• einen für die aktuelle Resultatadresse, das ist die Position des Kellerplatzes mit dem Wert des
aktuellen Prozeduraufrufs.
Der Compiler formal (siehe Java2.hs) erkennt einen formalen Funktions- oder Prozedurparameter
g an dessen Parameterliste pars und speichert diesen unter dem Namen @g. formals erzeugt den
attributierten Code c der Funktionsdeklaration g(pars){g = @g(pars)} und übergibt ihn als Teil von
parcode an die Deklaration des statischen Vorgängers von g (s.u.).
Damit werden g feste Basis-, Code- und Resultatadressen zugeordnet, so dass nicht nur die Aufrufe
von g, sondern auch die Zugriffe auf g als Variable korrekt übersetzt werden können.
298
Übersetzung von Variablenzugriffen
var :: String -> ExpStack
var x _ st dep = case td of Fun _ codelab -> ([Push ba,Push $ Con codelab,
PushA $ Dex ba adr],td)
_
-> ([Push $ Dex ba adr],td)
where (td,declDep,adr) = st x
ba = baseAdr declDep dep
(1)
(2)
Zielcodeerläuterung:
Im Fall (1) ist x ein aktueller Parameter eines Aufrufs e = g(e1 , . . . , en ) einer Prozedur g, d.h. es gibt
1 ≤ i ≤ n mit ei = x, und x ist selbst der Name einer Prozedur! Im Quellprogramm geht e eine
Deklaration von g voran, deren i-ter formaler Parameter eine Prozedurvariable f ist. f wird bei der
Ausführung des Codes für e durch x aktualisiert, indem die drei von (siehe formals) für f reservierten
Kellerplätze mit den o.g. drei Wertkomponenten von x belegt werden (siehe parcode in applyFun).
Beim Aufruf von x werden die drei Komponenten gekellert:
Push ba
Push $ Con codelab
PushA $ Dex ba adr
Kellern der Basisadresse ba von x
Kellern der Codeadresse codelab von x
Kellern der Resultatadresse Dex ba adr von x, die sich aus der
Basisadresse ba von x und der von fundecl bzw. formals in die
Symboltabelle eingetragenen Relativadresse adr für das Resultat von e
ergibt
299
Im Fall (2) genügt ein Befehl:
Push $ Dex ba adr
Kellern des Wertes von x, der unter der Kelleradresse Dex ba adr steht,
die sich aus der Basisadresse ba von x und der in der von vardecl bzw.
formals in die Symboltabelle eingetragenen Relativadresse adr von x ergibt
Übersetzung von Prozeduraufrufen
applyFun :: String -> ActsStack -> ExpStack
applyFun f acts lab st dep =
case td of Fun td codelab -> (code ba (Con codelab) $ Dex ba adr, (1)
td)
ForFun td
-> (code (Dex ba adr) (Dex ba $ adr+1)
(2)
$ Dex (Dex ba $ adr+2) 0,
td)
where (td,declDep,adr) = st f
(parcode,parLg) = acts lab st dep
retlab = lab+length parcode+4
ba = baseAdr declDep dep
code ba codelab result = parcode++Push BA:Move ba STP:
Push (Con retlab):Jump codelab:
retlab:
Pop:Save BA:Pop:replicate parLg Pop++
[Push result]
300
Zielcodeerläuterung:
parcode
Push BA
Move ba STP
Push $ Con retlab
Jump codelab
Zielcode für die Aufrufparameter
Kellern der aktuellen Basisadresse
Speichern der Basisadresse von f im Register STP
Kellern der Rücksprungadresse retlab
Sprung zur Codeadresse codelab von f, die von fundecl
berechnet wurde
An dieser Stelle wird bei der Ausführung des Zielcodes zunächst code(f ) abgearbeitet (siehe fundecl).
Dann geht es weiter mit:
Pop
Save BA
Pop
replicate parLg Pop
Push result
Entkellern der Rücksprungadresse retlab
Speichern der alten Basisadresse in BA
Entkellern der alten Basisadresse
Entkellern der Aufrufparameter
Kellern des Aufrufwertes
301
Im Fall (1) geht dem Aufruf von f im Quellprogramm eine Deklaration von f voran, deren Übersetzung die Symboltabelle um Einträge für f erweitert hat, aus denen applyFun die Adressen bzw.
Befehlsnummern ba, codelab und result berechnet und in obigen Zielcode einsetzt.
Im Fall (2) ist f eine Prozedurvariable, d.h. im Quellprogramm kommt der Aufruf von f im Rumpf
der Deklaration einer Prozedur g vor, die f als formalen Parameter enthält.
Der Zielcode wird erst bei einem Aufruf von g ausgeführt, d.h. nachdem f durch eine Prozedur h
aktualisiert und die von formals für f reservierten Kellerplätze belegt wurden. Der Zielcode des
Aufrufs von f ist also in Wirklichkeit Zielcode für einen Aufruf von h. Dementsprechend sind ba die
Anfangsadresse des dem Aufruf zugeordneten Kellerabschnitts und damit
Dex ba adr, Dex ba $ adr+1 und Dex ba $ adr+2
die Positionen der Kellerplätze mit der Basisadresse, der Codeadresse bzw. der Resultatadresse von h.
Folglich kommt applyFun durch Zugriff auf diese Plätze an die aktuellen Werte von ba, codelab und
result heran und kann sie wie im Fall (1) in den Zielcode des Aufrufs einsetzen.
Damit Push result analog zum Fall (1) nicht die Resultatadresse von h, sondern den dort abgelegten
Wert kellert, muss sie vorher derefenziert werden. Deshalb wird result im Fall (2) auf
Dex (Dex ba $ adr+2) 0
gesetzt.
302
Display d.
statischen
Vorgängers
Basisadr.
d. stat.
Vorgängers
STP
Display d.
statischen
Vorgängers
Display d.
statischen
Vorgängers
BA
Parameter
Parameter
Parameter
Basisadr. d. dyn. Vorgängers
Basisadr. d. dyn. Vorgängers
Basisadr. d. dyn. Vorgängers
Rücksprungadresse
Rücksprungadresse
Rücksprungadresse
TOP
TOP
BA
BA
Display d.
statischen
Vorgängers
Basisadr. d. stat. Vorgängers
lokale Adressen
TOP
Kellerzustand beim Sprung zur Codeadresse
Kellerzustand während der Ausführung
des Funktionsrumpfes
Kellerzustand beim Rücksprung
Der Zielcode der Parameterliste acts setzt sich aus dem Zielcode der einzelnen Ausdrücke von acts
zusammen, wobei acts von hinten nach vorn abgearbeitet wird, weil in dieser Reihenfolge Speicherplatz
für die entsprechenden formalen Parameter reserviert wurde (siehe formals).
303
Den Aufruf einer Prozedur p ohne Rückgabewert (genauer gesagt: mit Rückgabewert vom Typ UNIT)
betten wir in eine Zuweisung an p ein:
applyProc :: String -> ActsStack -> ComStack
applyProc f p = assign p . applyFun p
Übersetzung aktueller Parameter
actuals :: [ExpStack] -> ActsStack
actuals pars lab st dep = foldr f ([],0) pars
where f e (code,parLg) = (code++code',parLg+ case td of Fun _ _ -> 3
_ -> 1)
where (code',td) = e (lab+length code) st dep
In [27], Kapitel 5, wird auch die Übersetzung von Feldern und Records behandelt. Grundlagen der Kompilation funktionaler Sprachen liefert [27], Kapitel 7. Die Übersetzung objektorientierter Sprachen ist Thema von [44], Kapitel5.
304
Beispiel 13.4 (Vier JavaLight+-Programme für die Fakultätsfunktion aus Java2.hs)
Int x; read x; Int fact; fact=1;
while x>1 fact=x*fact; x=x-1; write fact;
Int f(Int x) if x<2 f=1; else f=x*f(x-1);
Int x; read x; write f(x);
f(Int x,Int fact) if x<2 write fact; else f(x-1,fact*x)
Int x; read x; f(x,1)
Int f(Int x,Int g(Int x,Int y)) if x<2 f=1; else f=g(x,f(x-1,g));
Int g(Int x,Int y) g=x*y;
Int x; read x; write f(x,g);
javaToStack "prog" [n] übersetzt jedes der obigen Programme in eine Befehlsfolge vom
Typ [StackCom], legt diese in der Datei javacode ab und transformiert die Eingabeliste
[n] in die Ausgabeliste [n!].
Der Zielcode des vierten Programms lautet wie folgt:
0: Push (Con 0)
1: Jump (Con 68)
305
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
Move
Push
Push
Jump
Move
Push
Push
Push
Push
Push
Move
Push
Jump
Pop
Save
Pop
Pop
Pop
Push
Save
Pop
Pop
Pop
Jump
Push
TOP BA
STP
(Con 0)
(Con 26)
TOP BA
(Dex STP 0)
STP
(Dex BA (-4))
(Dex BA (-3))
BA
(Dex (Dex BA 1) (-6))
(Con 15)
(Dex (Dex BA 1) (-5))
begin f
begin g
y
x
STP
BA
(Dex (Dex (Dex BA 1) (-4)) 0)
(Dex (Dex BA 1) 1)
g=@g(x,y)
(Dex TOP (-1))
(Dex BA (-3))
end g
x
306
27:
28:
29:
30:
31:
32:
33:
34:
35:
36:
37:
38:
39:
40:
41:
42:
43:
44:
45:
46:
47:
48:
49:
50:
51:
Push (Con 2)
Cmp "<"
JumpF 34
Push (Con 1)
Save (Dex (Dex BA 0) 0)
Pop
Jump (Con 65)
Push BA
Push (Con 6)
PushA (Dex BA 1)
Push (Dex BA (-3))
Push (Con 1)
Sub
Push BA
Move (Dex BA 0) STP
Push (Con 44)
Jump (Con 2)
Pop
Save BA
Pop
Pop
Pop
Pop
Pop
Push (Dex (Dex BA 0) 0)
x<2
f=1
g
g
g
x
x-1
jump to f
f(x-1,g)
307
52:
53:
54:
55:
56:
57:
58:
59:
60:
61:
62:
63:
64:
65:
66:
67:
68:
69:
70:
71:
72:
73:
74:
75:
76:
Push
Push
Move
Push
Jump
Pop
Save
Pop
Pop
Pop
Push
Save
Pop
Pop
Pop
Jump
Push
Jump
Move
Push
Push
Push
Mul
Save
Pop
(Dex BA (-3))
BA
BA STP
(Con 57)
(Con 6)
x
jump to g
BA
(Dex BA 1)
(Dex (Dex BA 0) 0)
g(x,f(x-1,g))
f=g(x,f(x-1,g))
(Dex TOP (-1))
(Con 0)
(Con 79)
TOP BA
STP
(Dex BA (-3))
(Dex BA (-4))
end f
(Dex (Dex BA 0) 1)
begin g
x
y
x*y
g=x*y
308
77:
78:
79:
80:
81:
82:
83:
84:
85:
86:
87:
88:
89:
90:
91:
92:
93:
94:
95:
96:
97:
98:
Pop
Jump (Dex TOP (-1))
Push (Con 0)
Read (Dex BA 2)
Push BA
Push (Con 70)
PushA (Dex BA 1)
Push (Dex BA 2)
Push BA
Move BA STP
Push (Con 89)
Jump (Con 2)
Pop
Save BA
Pop
Pop
Pop
Pop
Pop
Push (Dex BA 0)
Write
Pop
end g
read x
g
g
g
x
jump to f
f(x,g)
write f(x,g)
309
14 Mehrpässige Compiler
Sei G = (S, BS, R) eine CFG und A = (A, Op) eine Σ(G)-Algebra. Wir kommen zurück
auf die in Kapitel 12 behandelte Form
Av1 × . . . × Avm → Aa1 × . . . × Aan
der Trägermengen von A und sehen uns das Schema der Definition einer Operation von A
mal etwas genauer an. O.B.d.A. setzen wir für die allgemeine Betrachtung voraus, dass alle
Trägermengen von A miteinander übereinstimmen.
Sei c : s1 × . . . × sk → s ein Konstruktor von Σ(G). Das Schema einer Interpretation
cA : Ak → A von c in A offenbar wie folgt:
cA(f1, . . . , fk )(x1, . . . , xm) = (t1, . . . , tn) where (x11, . . . , x1n) = f1(t11, . . . , t1m)
...
(1)
(xk1, . . . , xkn) = fk (tk1, . . . , tkm)
Hier sind f1, . . . , fk , x1, . . . , xm, xi1, . . . , x1n, . . . , xk1, . . . , xkn paarweise verschiedene Variablen und e1, . . . , en, xi1, . . . , t1m, . . . , tk1, . . . , tkm Σ(G)-Terme.
310
Ist s → w0s1w1 . . . sk wk die Grammatikregel, aus der c hervorgeht, dann wird (1) oft als
Liste von Zuweisungen an die Attribute der Sorten s, s1, . . . , sk geschrieben:
s.a1 := t1 . . . s.an := tn
s1.v1 := t11 . . . s1.vm := t1m
...
sk .v1 := tk1 . . . sk .vm := tkm
cA ist genau dann wohldefiniert, wenn für alle f1, . . . , fk , x1, . . . , xm (1) eine Lösung in A
hat, d.h., wenn es eine Belegung g der Variablen xi1, . . . , x1n, . . . , xk1, . . . , xkn in A gibt
mit
(g(xi1), . . . , g(xin)) = f1(g ∗(ti1), . . . , g ∗(tim))
für alle 1 ≤ i ≤ k. Hinreichend für die Lösbarkeit ist die Zyklenfreiheit der Benutzt-Relation
zwischen den Termen und Variablen von (1). Enthält sie einen Zyklus, dann versucht man,
die parallele Berechnung und Verwendung von Attributwerten in r hintereinander ausgeführte Schritte (“Pässe”) zu zerlegen, so dass in jedem Schritt zwar nur einige Attribute
berechnet bzw. benutzt werden, die Komposition der Schritte jedoch eine äquivalente Definition von cA liefert.
Notwendig wird die Zerlegung der Übersetzung zum Beispiel dann, wenn die Quellsprache
Deklarationen von Variablen verlangt, diese aber im Text des Quellprogramms bereits vor
ihrer Deklaration benutzt werden dürfen.
311
Zerlegt werden müssen die Menge At = {v1, . . . , vm, a1, . . . , an} aller Attribute in r Teilmengen At1, . . . , Atr sowie jedes (funktionale) Argument fi von cA, 1 ≤ i ≤ k, in r
Teilfunktionen fi1, . . . , fir derart, dass die Benutzt-Relation in jedem Pass azyklisch ist.
Um die Benutzt-Relation zu bestimmen, erweitern wir die Indizierung der äußeren Variablen
bzw. Terme von (1) wie folgt:
cA(f1, . . . , fk )(x01, . . . , x0m) = (t(k+1)1, . . . , t(k+1)n)
where (x11, . . . , x1n) = f1(t11, . . . , t1m)
...
(xk1, . . . , xkn) = fk (tk1, . . . , tkm)
(2)
Für jeden Konstruktor c und jedes Attributpaar (at, at0) ist der Abhängigkeitsgraph
depgraph(c)(at, at0) ⊆ {0, . . . , k} × {1, . . . , k + 1}
für c und (at, at0) wie folgt definiert:
312
∃ 1 ≤ i ≤ m, 1 ≤ j ≤ n : at = vi ∧ at0= aj
(at vererbt, at0 abgeleitet)
{(0, k + 1)} falls x0i in t(k+1)j vorkommt,
⇒ depgraph(c)(at, at0) =
∅
sonst,
∃ 1 ≤ i, j ≤ m : at = vi ∧ at0 = vj
(at und at0 vererbt)
⇒ depgraph(c)(at, at0) = {(0, s) | 0 ≤ s ≤ k, x0i kommt in tsj vor},
∃ 1 ≤ i ≤ n, 1 ≤ j ≤ m : at = ai ∧ at0 = vj
(at abgeleitet, at0 vererbt)
⇒ depgraph(c)(at, at0) = {(r, s) | 0 ≤ r, s ≤ k, xri kommt in tsj vor},
∃ 1 ≤ i, j ≤ n : at = ai ∧ at0 = aj
(at und at0 abgeleitet)
⇒ depgraph(c)(at, at0) = {(r, k + 1) | 0 ≤ r ≤ k, xri kommt in t(k+1)j vor}.
(2) hat genau dann eine Lösung in A (die durch Auswertung der Terme von (2) berechnet
werden kann), wenn
• für alle Konstruktoren c von Σ(G), at, at0 ∈ At und (i, j) ∈ depgraph(c)(at, at0) i < j
gilt.
Ist diese Bedingung verletzt, dann wird At so in Teilmengen At1, . . . , Atr zerlegt, dass für
alle at, at0 ∈ At entweder at in einem früheren Pass als at0 berechnet wird oder beide
Attribute in demselben Pass berechnet werden und die Bedingung erfüllen.
313
Für alle 1 ≤ p, q ≤ r, at ∈ Atp und at0 ∈ Atq muss also Folgendes gelten:
p < q ∨ (p = q ∧ ∀ (i, j) ∈ depgraph(c)(at, at0) : i < j).
(3)
Für alle 1 ≤ p ≤ r und 1 ≤ i ≤ k sei
{vip1 , . . . , vipmp } = {v1, . . . , vm} ∩ Atp, {ajp1 , . . . , ajpnp } = {a1, . . . , an} ∩ Atp,
πp : Av1 × . . . × Avm
(x1, . . . , xm)
πp0 : Aa1 × . . . × Aan
(x1, . . . , xn)
→
7→
→
7→
Avip1 × . . . × Avipmp
(xip1 , . . . , xipmp ),
Aajp1 × . . . × Aajpnp
(xjp1 , . . . , xjpnp )
und fip : Avi1 × . . . × Avimp → Aaj1 × . . . × Aajnp die eindeutige Funktion, die das folgende
Diagramm kommutativ macht:
πp
Av1 × . . . × Avm Avip1 × . . . × Avipmp
fi
fip
g
g
πp0
Aa1 × . . . × Aan Aajp1 × . . . × Aajpnp
314
Die zu (2) äquivalente Definition von cA mit r Pässen lautet wie folgt:
cA(f1, . . . , fk )(x01, . . . , x0m) = (t(k+1)1, . . . , t(k+1)n)
where (x1j11 , . . . , x1j1n1 ) = f11(t1i11 , . . . , t1i1m1 )
...
Pass 1
(xkj11 , . . . , xkj1n1 ) = fk1(tki11 , . . . , tki1m1 )
...
...
(x1jr1 , . . . , x1jrnr ) = f1r (t1ir1 , . . . , t1irmr )
...
Pass r
(xkjr1 , . . . , xkjrnr ) = fkr (tkir1 , . . . , tkirmr )
Der LAG-Algorithmus (Left-to-right-Attributed-Grammar) berechnet die kleinste Zerlegung von At, die (3) erfüllt, sofern eine solche existiert.
Sei ats = At und constrs die Menge aller Konstruktoren von Σ(G). Ausgehend von der einelementigen Zerlegung [ats] verändert check partition die letzten beiden Elemente (curr
und next) der jeweils aktuellen Zerlegung, bis entweder next leer und damit eine Zerlegung
gefunden ist, die (3) erfüllt, oder curr leer ist, was bedeutet, dass keine solche Zerlegung
existiert.
315
least_partition ats = reverse . check_partition [] [ats]
check_partition next (curr:partition) =
if changed
then check_partition next' (curr':partition)
else case (next',curr') of
([],_) -> curr':partition
Zerlegung von ats, die (3) erfüllt
(_,[]) -> []
Es gibt keine Zerlegung, die (3) erfüllt.
_ -> check_partition [] (next':curr':partition)
Die aktuelle Zerlegung wird um das Element nextfl erweitert.
where (next',curr',changed) = foldl check_constr (next,curr,False)
constrs
check_constr state c = foldl (check_atpair deps) state
[(at,at') | at <- ats, at' <- ats,
not $ null $ deps (at,at')]
where deps = depgraph c
check_atpair deps state atpair = foldl (check_dep atpair) state $
deps atpair
316
check_dep (at,at') state@(next,curr,changed) (i,j) =
if at' `elem` curr && ((at `elem` curr && i>=j) || at `elem` next)
Die aktuelle Zerlegung next:curr:... verletzt (3).
then (at':next,curr`minus`[at'],True)
atfl wird vom vorletzten Zerlegungselement (curr) zum letzten (next) verschoben.
else state
317
15 Funktoren und Monaden in Haskell
Typen und Typvariablen höherer Ordnung
Während bisher nur Typvariablen erster Ordnung vorkamen, sind Funktoren und Monaden Instanzen der Typklasse Functor bzw. Monad, die eine Typvariable zweiter Ordnung
enthält. Typvariablen erster Ordnung werden durch Typen erster Ordnung instanziiert wie
z.B. Int oder Bool. Typvariablen zweiter Ordnung werden durch Typen zweiter Ordnung
instanziiert wie z.B. durch den folgenden:
data Tree a = F a [Tree a] | V a
Demnach ist ein Typ erster Ordnung eine Menge und ein Typ zweiter Ordnung eine Funktion von einer Menge von Mengen in eine – i.d.R. andere – Menge von Mengen: Tree bildet
jede Menge A auf eine Menge von Bäumen ab, deren Knoteneinträge Elemente von A sind.
Funktoren
Die meisten der in Abschnitt 5.1 definierten Funktoren sind in Haskell stndardmäßig als
Instanzen der Typklasse Functor implementiert:
318
class Functor f where fmap :: (a -> b) -> f a -> f b
newtype Id a = Id {run :: a}
Identitätsfunktor
instance Functor Id where fmap h (Id a) = Id $ h a
instance Functor [ ] where fmap = map
data Maybe a = Just a | Nothing
Listenfunktor
Ausnahmefunktoren
instance Functor Maybe where
fmap f (Just a) = Just $ f a
fmap _ _
= Nothing
data Either e a = Left e | Right a
instance Functor Either e where
fmap f (Right a) = Right $ f a
fmap _ e
= e
319
instance Functor ((->) state) where
fmap f h = f . h
instance Functor ((,) state) where
fmap f (st,a) = (st,f a)
Leserfunktor
Schreiberfunktor
Transitionsfunktoren
newtype Trans state a = T {runT :: state -> (a,state)}
instance Functor (Trans state) where
fmap f (T h) = T $ (\(a,st) -> (f a,st)) . h
newtype TransM state m a = TM {runTM :: state -> m (a,state)}
instance Monad m => Functor (TransM state m) where
s.u.
fmap f (TM h) = TM $ (>>= \(a,st) -> return (f a,st)) . h
Anforderungen an die Instanzen der Typklasse Functor:
Für alle Mengen a, b, c, f : a → b und g : b → c,
fmap id
fmap (f . g)
=
=
id
fmap f . fmap g
320
Monaden
Monad ist eine Unterklasse von Functor:
class Functor m
return ::
(>>=) ::
(>>)
::
fail
::
m >> m' =
=> Monad m where
a -> m a
Einheit η
m a -> (a -> m b) -> m b bind-Operatoren
m a -> m b -> m b
String -> m a
Wert im Fall eines Matchfehlers
m >>= const m'
instance Monad Id where return = Id
Id a >>= f = f a
instance Monad [ ] where return a = [a]
(>>=) = flip concatMap
fail _ = []
instance Monad Maybe where return = Just
Just a >>= f = f a
e >>= _
= e
fail _ = Nothing
Identitätsmonade
Listenmonade
Ausnahmemonaden
321
instance Monad (Either e) where return = Right
Right a >>= f = f a
e >>= _
= e
instance Monad ((->) state) where return = const
Lesermonade
(h >>= f) st = f (h st) st
class Monoid a where
mempty :: a; mappend :: a -> a -> a
Schreibermonade
instance Monoid state => Monad ((,) state) where
return a = (mempty,a)
(st,a) >>= f = (st `mappend` st',b) where (st',b) = f a
instance Monad (Trans state) where
Transitionsmonaden
return a = T $ \st -> (a,st)
T h >>= f = T $ (\(a,st) -> runT (f a) st) . h
instance Monad m => Monad (TransM state m) where
return a = TM $ \st -> return (a,st)
TM h >>= f = TM $ (>>= \(a,st) -> runTM (f a) st) . h
322
Hier komponiert der bind-Operator >>= zwei Zustandstransformationen (h und dann f )
sequentiell. Dabei liefert die von der ersten erzeugte Ausgabe die Eingabe der zweiten.
Trans(state) und TransM(state) werden auch Zustandsmonade bzw. (Zustands-)Monadentransformer genannt. Wir bevorzugen den Begriff Transitionsmonade, weil Zustände auch zu Leser- und Schreibermonaden gehören (s.o.), aber nur Transitionsmonaden
aus Zustandstransformationen bestehen.
Anforderungen an die Instanzen von Monad:
Für alle m ∈ m(a), f : a → m(b) und g : b → m(c),
(m >>= f) >>= g
m >>= return
(>>= f) . return
m >>= f
=
=
=
=
m >>= (>>= g . f)
m
f
fmap f m >>= id
(1)
(2)
(3)
Anforderungen an die Instanzen von Monoid:
(a `mappend` b) `mappend` c
mempty `mappend` a
a `mappend` mempty
=
=
=
a `mappend` (b `mappend` c)
a
a
323
Plusmonaden
MonadPlus ist eine Unterklasse von Monad:
class Monad m => MonadPlus m where
mzero :: m a
mplus :: m a -> m a -> m a
scheiternde Berechnung
parallele Komposition
instance MonadPlus Maybe where mzero = Nothing
Nothing `mplus` m = m
m `mplus` _
= m
instance MonadPlus [ ] where mzero = []; mplus = (++)
instance MonadPlus m => MonadPlus (TransM state m) where
mzero = TM $ const mzero
TM g `mplus` TM h = TM $ liftM2 mplus g h
s.u.
Anforderungen an die Instanzen von MonadPlus:
mzero >>= f
m >>= const mzero
mzero `mplus` m
=
=
=
mzero
mzero
m
(4)
(5)
324
m `mplus` mzero
=
m
Wir werden im folgenden Kapitel anstelle von MonadPlus eine auf die Implementierung von
Compilern zugeschnittene Unterklasse von Monad einführen, die Bedingungen nicht nur an
die Menge state der Zustände (die dort den möglichen Compiler-Eingaben entsprechen),
sondern auch an die eingebettete Monade m stellt.
Monaden-Kombinatoren
sequence :: Monad m => [m a] -> m [a]
sequence (m:ms) = do a <- m; as <- sequence ms; return $ a:as
sequence _
= return []
sequence(ms) führt die Prozeduren der Liste ms hintereinander aus. Wie bei some(m)
und many(m) werden die dabei erzeugten Ausgaben aufgesammelt.
Die do-Notation für monadische Ausdrücke wurde in Kapitel 6 eingeführt.
sequence_ :: Monad m => [m a] -> m ()
sequence_ = foldr (>>) $ return ()
sequence (ms) arbeitet wie sequence(ms), vergisst aber die erzeugten Ausgaben.
325
Die folgenden Funktionen führen die Elemente mit map bzw. zipWith erzeugter Prozedurlisten hintereinander aus:
mapM :: Monad m => (a -> m b) -> [a] -> m [b]
mapM f = sequence . map f
mapM_ :: Monad m => (a -> m b) -> [a] -> m ()
mapM_ f = sequence_ . map f
liftM2 liftet eine zweistellige Funktion f : A → (B → C) zu einer Funktion des Typs
M (A) → (M (B) → M (C)):
liftM2 :: Monad m => (a -> b -> c) -> m a -> m b -> m c
liftM2 f ma mb = do a <- ma; b <- mb; return $ f a b
liftM2 gehört zum ghc-Modul Control.Monad.
326
16 Monadische Compiler
Trans state a
state -> (a,state)
state =
internal system states
total
functions
IO a
TransM state m a
state -> m (a,state)
m = [ ]
m = Maybe
partial
functions
nondeterministic
functions
Monad m
compilers returning
a target object or
an error message
compilers returning
a list of target objects
Compiler input m
input = (Pos,String)
m = Either String
input = String
m = [ ]
TransM input m a
input -> m (a,input)
327
Sei G = (S, BS, R) eine CFG, X =
S
BS und M eine Compilermonade (siehe Kapitel 5).
Ein generischer Compiler
∗
compileG = (compileA
G : X → M (A))A∈AlgΣ(G) ,
genauer gesagt: die von compileG verwendeten Transitionsfunktionen
∗
∗
transA
s : X → M (As × X ),
s ∈ S ∪ BS, A ∈ AlgΣ(G),
ließen sich als Transitionsmonaden vom Typ TransM(String)(M) implementieren.
Oft erfordern die Übersetzung oder auch die Ausgabe differenzierter Fehlermeldungen zusätzliche Informationen über die Eingabe wie z.B. Zeilen- und Spaltenpositionen von Zeichen
des Eingabetextes, um Syntaxfehler den Textstellen, an denen sie auftreten können, zuzuordnen. Den Eingabetyp auf String zu beschränken ist dann nicht mehr adäquat.
Deshalb ersetzen wir String durch die Typvariable input und fassen die erforderlichen,
input bzw. die Compilermonade M betreffenden Grundfunktionen in folgender Unterklasse von Monad zusammen. Die Compiler selbst werden dann als Objekte vom Typ
TransM(input)(M) implementiert.
class Monad m => Compiler input m | input -> m, m -> input where
errmsg :: input -> m a
empty :: input -> Bool
328
ht
plus
:: input -> m (Char, input)
(erstes Eingabezeichen, Resteingabe)
:: m a -> m a -> m a
errmsg behandelt fehlerhafte Eingaben. empty prüft, ob die Eingabe leer ist. Wenn nicht,
dann liefert ht das erste Zeichen der Eingabe und die Resteingabe.
Kommen im Typ einer Funktion einer Typklasse nicht alle Typvariablen der Klasse vor,
dann müssen die fehlenden von den vorkommenden abhängig gemacht werden. So sind z.B.
in der Typklasse Compiler die Abhängigkeiten input → m und m → input der Funktion
empty bzw. plus geschuldet.
Die Menge der im Programm definierten Instanzen einer Typklasse muss deren funktionale
Abhängigkeiten tatsächlich erfüllen. Zwei Instanzen von Compiler mit derselben inputInstanz müssen also auch dieselbe m-Instanz haben.
Zwei Instanzen der Typklasse Compiler
Für mehrere korrekte Ausgaben, aber nur eine mögliche Fehlermeldung:
instance Compiler String [ ] where
errmsg _ = []
empty = null
329
ht (c:str) = [(c,str)]
plus = (++)
Für höchstens eine korrekte Ausgabe, aber mehrere mögliche Fehlermeldungen:
type Pos = (Int,Int)
instance Compiler (Pos,String) (Either String) where
errmsg (pos,_) = Left $ "error at position "++show pos
empty = null . snd
ht ((i,j),c:str) = Right (c,(pos,str)) where
pos = if c == '\n' then (i+1,1)
else (i,j+1)
Left _ `plus` m = m
m `plus` _
= m
Hier sind die Eingaben vom Typ (Pos,String). Am Argument ((i,j),c:str) von ht erkennt
man, dass, bezogen auf die gesamte Eingabe, c in der i-ten Zeile und j-ten Spalte steht.
Folglich ist pos im Wert Right (c,(pos,str)) von ht((i,j),c:str) die Anfangsposition des
Reststrings str.
Scheitert ein Compiler dieses Typs, dann ruft er errmsg mit der Eingabeposition auf, an
der er den Fehler erkannt hat, der ihn scheitern ließ.
330
16.1 Compilerkombinatoren
runC :: Compiler input m => TransM input m a -> input -> m a
runC comp input = do (a,input) <- runTM comp input
if empty input then return a else errmsg input
runC(comp)(input) führt den Compiler comp auf der Eingabe input aus und scheitert,
falls comp eine nichtleere Resteingabe zurücklässt.
cplus :: Compiler input m => TransM input m a -> TransM input m a
-> TransM input m a
TM f `cplus` TM g = TM $ liftM2 plus f g
csum :: Compiler input m => [TransM input m a] -> TransM input m a
csum = foldr1 cplus
some, many :: Compiler input m => TransM input m a
-> TransM input m [a]
some comp = do a <- comp; as <- many comp; return $ a:as
many comp = csum [some comp, return []]
some(comp) und many(comp) wenden den Compiler comp auf die Eingabe an. Akzeptiert
331
comp ein Präfix der Eingabe, dann wird comp auf die Resteingabe angewendet und dieser
Vorgang wiederholt, bis comp scheitert. Die Ausgabe beider Compiler ist die Liste der
Ausgaben der einzelnen Iterationen von comp.
some(comp) scheitert, wenn bereits die erste Iteration von comp scheitert. many(comp)
scheitert in diesem Fall nicht, sondern liefert die leere Liste von Ausgaben.
cguard :: Compiler input m => Bool -> TransM input m ()
cguard b = if b then return () else TM errmsg
Sind (1), (4) und (5) erfüllt, dann gelten die folgenden semantischen Äquivalenzen:
do cguard True; m1; ...; mn ist äquivalent zu
do cguard False; m1; ...; mn ist äquivalent zu
do m1; ...; mn
mzero
Lifting von (:) : a → [a] → [a] auf Compilerebene
append :: Compiler input m => TransM input m a
-> TransM input m [a] -> TransM input m [a]
append = liftM2 (:)
332
16.2 Monadische Scanner
Scanner sind Compiler, die einzelne Symbole erkennen. Der folgende Scanner sat(f ) erwartet, dass das Zeichen am Anfang des Eingabestrings die Bedingung f erfüllt:
sat :: Compiler input m => (Char -> Bool) -> TransM input m Char
sat f = TM $ \input -> do p@(c,_) <- if empty input then errmsg input
else ht input
if f c then return p else errmsg input
Compiler input m => Char -> TransM input m Char
char chr = sat (== chr)
nchar :: Compiler input m => String -> TransM input m Char
nchar chrs = sat (`notElem` chrs)
Darauf aufbauend, erwarten die folgenden Scanner eine Ziffer, einen Buchstaben bzw. einen
Begrenzer am Anfang des Eingabestrings:
digit,letter,delim :: Compiler input m => TransM input m Char
digit = csum $ map char ['0'..'9']
letter = csum $ map char $ ['a'..'z']++['A'..'Z']
delim = csum $ map char " \n\t"
333
Der folgende Scanner string(str) erwartet den String str am Anfang des Eingabestrings:
string :: Compiler input m => String -> TransM input m String
string = mapM char
Die folgenden Scanner erkennen Elemente von Standardtypen und übersetzen sie in entsprechende Haskell-Typen:
bool :: Compiler input m => TransM input m Bool
bool = csum [do string "True"; return True,
do string "False"; return False]
nat,int :: Compiler input m => TransM input m Int
nat = do ds <- some digit; return $ read ds
int = csum [nat, do char '-'; n <- nat; return $ -n]
identifier :: Compiler input m => TransM input m String
identifier = append letter $ many $ nchar "(){}<>=!&|+-*/^.,:; \n\t"
relation :: Compiler input m => TransM input m String
relation = csum $ map string $ words "<= >= < > == !="
334
Die Kommas trennen die Elemente der Argumentliste von csum.
token(comp) erlaubt vor und hinter dem von comp erkannten String Leerzeichen, Zeilenumbrüche oder Tabulatoren:
token :: Compiler a -> Compiler a
token comp = do many delim; a <- comp; many delim; return a
tchar
tstring
tbool
tint
tidentifier
trelation
=
=
=
=
=
=
token
token
token
token
token
token
. char
. string
bool
int
identifier
relation
16.3 Monadische LL-Compiler
Sei G = (S, BS, R) eine nicht-linksrekursive CFG. Wir setzen voraus, dass S eine ausgezeichnete Sorte start enthält, und implementieren die Übersetzungsfunktionen
∗
compileA
G : X → M (A),
A ∈ AlgΣ(G),
und ihre Hilfsfunktionen (siehe Kapitel 6) wie folgt: Sei S = {s1, . . . , sk }.
335
compile_G :: Compiler input m => Alg s1...sk -> input -> m start
compile_G = runC . trans_start
Für alle B ∈ BS ∪ Z sei
trans_B :: Compiler input m => TransM input m B
gegeben.
Seien s ∈ S und r1, . . . , rm die Regeln von R mit linker Seite s.
trans_s :: Compiler input m => TransM input m s
trans_s alg = csum [try_r1,...,try_rm] where
...
try_ri = do a1 <- trans_e1; ...; an <- trans_en
return $ f_ri alg a_i1 ... a_ik
...
wobei ri = (s → e1 . . . en) und {i1, . . . , ik } = {1 ≤ i ≤ n | ei ∈ S ∪ BS}.
Beispiel
Aus der abstrakten Syntax der Grammatik SAB von Beispiel 4.5 ergibt sich die folgende
Haskell-Implementierung der Klasse aller Σ(SAB)-Algebren:
336
data SAB s a b = SAB {f_1 :: b -> s, f_2 :: a -> s, f_3 :: s,
f_4 :: s -> a, f_5 :: a -> a -> a,
f_6 :: s -> b, f_7 :: b -> b -> b,}
Nach obigem Schema liefern die Regeln
r1 = S → aB, r2 = S → bA, r3 = S → ,
r4 = A → aS r5 = A → bAA, r6 = B → bS, r7 = B → aBB
von SAB die folgende Haskell-Version des generischen SAB-Compilers von Beispiel 6.1:
compS :: Compiler input m => SAB s a b -> TransM input m s
compS alg = csum [do char 'a'; c <- compB alg; return $ f_1 alg c,
do char 'b'; c <- compA alg; return $ f_2 alg c,
return $ f_3 alg]
compA :: Compiler input m => SAB s a b -> TransM input m a
compA alg = csum [do char 'a'; c <- compS alg; return $ f_4 alg c,
do char 'b'; c <- compA alg; d <- compA alg; return $ f_5 alg c d]
compB :: Compiler input m => SAB s a b -> TransM input m b
compB alg = csum [do char 'b'; c <- compS alg; return $ f_6 alg c,
do char 'a'; c <- compB alg; d <- compB alg; return $ f_7 alg c d]
337
In Compiler.hs steht dieser Compiler zusammen mit der Zielalgebra SABcount von Beispiel
4.5. Für m = Maybe und alle w ∈ {a, b}∗ und i, j ∈ N gilt
compileSAB (SABcount)(w) = Just(i, j)
genau dann, wenn i und j die Anzahl der Vorkommen von a bzw. b in w ist.
o
16.4 Generischer JavaLight-Compiler (siehe Java.hs)
compJava :: Compiler input m => JavaLight s1 s2 s3 s4 s5 s6 s7 s8 s9 s10
-> TransM input m s1
compJava alg = commands where
commands = do c <- command
csum [do cs <- commands; return $ seq_ alg c cs,
return $ embed alg c]
command = csum [do tstring "if"; e <- disjunctC; c <- command
csum [do tstring "else"; c' <- command
return $ cond alg e c c',
return $ cond1 alg e c],
do tstring "while"; e <- disjunctC; c <- command
return $ loop alg e c,
do tchar '{'; cs <- commandsC; tchar '}'
return $ block alg cs,
do x <- tidentifier; tchar '='; e <- sumC
tchar ';'; return $ assign alg x e]
338
sumC = do e <- prodC; f <- sumsectC; return $ sum_ alg e f
sumsectC = csum [do op <- csum $ map tstring ["+","-"]
e <- prodC; f <- sumsectC
return $ sumsect alg op e f,
return $ nilS alg]
prodC = do e <- factor; f <- prodsectC; return $ prod alg e f
prodsectC = csum [do op <- csum $ map tstring ["*","/"]
e <- factor; f <- prodsectC
return $ prodsect alg op e f,
return $ nilP alg]
factor = csum [do i <- tint; return $ embedI alg i,
do x <- tidentifier; return $ var alg x,
do tchar '('; e <- sumC; tchar ')'
return $ encloseS alg e]
disjunctC = do e <- conjunctC
csum [do tstring "||"; e' <- disjunctC
return $ disjunct alg e e',
return $ embedC alg e]
conjunctC = do e <- literal
csum [do tstring "&&"; e' <- conjunctC
return $ conjunct alg e e',
return $ embedL alg e]
literal = csum [do b <- tbool; return $ embedB alg b,
do tchar '!'; e <- literal; return $ not_ alg e,
do e <- sumC; rel <- trelation; e' <- sumC
339
return $ atom alg e rel e',
do tchar '('; e <- disjunctC; tchar ')'
return $ encloseD alg e]
Der ensprechende Compiler für JavaLight+ (siehe Kapitel 13) steht hier.
16.5 Korrektheit des JavaLight-Compilers
Sei
Store = String → Z,
comS = {Commands, Command },
expS = {Sum, Prod , Factor },
bexpS = {Disjunct, Conjunct, Literal },
sectS = {Sumsect, Prodsect},
Sem = javaState (siehe Abschnitt 11.5),
A = javaStack (siehe Abschnitt 12.5).
Dann gilt für alle Sorten s von JavaLight:


Store (→ Store falls s ∈ comS ,



Store → Z
falls s ∈ expS ,
Sem s =
Store → 2
falls s ∈ bexpS ,



 Store → (Z → Z) falls s ∈ sectS ,
340
As = Z → StackCom ∗.
Sei Mach s = (Z∗ × Store) (→ (Z∗ × Store). Die Funktionen evaluate : A → Mach
und encode : Sem → Mach des Interpreterdiagramms (1) am Anfang von Kapitel 5 sind
im Fall Σ = JavaLight wie folgt definiert: Für alle f ∈ Sem s, g ∈ As, stack ∈ Z∗ und
store ∈ Store,

(stack, g(store))
falls s ∈ comS





und g(store) definiert ist,




(g(store) : stack, store) falls s ∈ expS ∪ bexpS



und g(store) definiert ist,
encodes(g)(stack, store) =
(g(store)(head(stack)) : tail(stack), store)





falls s ∈ sectS , stack 6= 



und g(store) definiert ist,



undefiniert
sonst,
evaluates(f )(stack, store) = hπ1, π2i(execute(f (0))(stack, store, 0))
(siehe Abschnitt 12.5).
S
Sei (S, BS, R) = JavaLight, X = BS und M eine Compilermonade (siehe Kapitel 5).
Da compJava ein generischer Compiler für JavaLight ist, kommutiert (1) am Anfang von
Kapitel 5 genau dann, wenn das folgende Diagramm kommutiert:
341
X∗
compJava(Sem)
g
M (Sem)
compJava(A)
(2)
M (encode)
M (A)
M (evaluate)
g
M (Mach)
(siehe Diagramm (13) in Kapitel 5). (2) beschreibt vor allem folgende Zusammenhänge
zwischen compJava(A) und dem Interpreter evaluate der Zielsprache A:
• Sei com ein JavaLight-Programm einer Sorte von comS . Wird com von compJava(A)
in die Befehlsfolge cs übersetzt und beginnt die Ausführung von cs im Zustand state,
dann endet sie in einem Zustand, dessen Speicherkomponente mit der Speicherkomponente von encode(compJava(Sem)(com))(state) übereinstimmt.
• Sei exp ein JavaLight-Programm einer Sorte von expS ∪ bexpS . Wird exp von
compJava(A) in die Befehlsliste cs übersetzt und beginnt die Ausführung von cs im
Zustand state, dann endet sie in einem Zustand, dessen oberstes Kellerelement mit dem
obersten Kellerelement von encode(compJava(Sem)(exp))(state) übereinstimmt.
342
Wie in Kapitel 5 erwähnt wurde, kommutiert (1) am Anfang von Kapitel 5 und damit auch
das obige Diagramm (2), wenn Mach eine JavaLight-Algebra ist und die mehrsortigen
Funktionen encode und evaluate JavaLight-homomorph sind.
Um das beweisen zu können, fehlt uns neben der Interpretation des Σ(JavaLight)-Konstruktors loop in A (siehe 11.5) auch die aller Konstruktoren von Σ(JavaLight) in Mach. Wir
werden darauf im Abschnitt 19.1 bzw. 19.2 zurückkommen.
Eine Testumgebung für Übersetzungen des JavaLight-Compilers in die in den Kapiteln 11 und 12 definierten JavaLight-Algebren liefert die Funktion javaToAlg(file) von
Java.hs, die ein Quellprogramm vom Typ commands aus der Datei file und in die jeweilige
Zielalgebra überführt.
javaToAlg(file)(5) und javaToAlg(file)(6) starten nach jeder Übersetzung eine Schleife,
die in jeder Iteration eine Variablenbelegung einliest und die sich aus dem jeweiligen Zielcode
entsprechenden Zustandstransformation darauf anwendet.
Eine Testumgebung für Übersetzungen des JavaLight-Compilers in die im Abschnitt
13.3 definierte JavaLight+-Algebra javaStackP liefert die Funktion javaToStack(file) von
Java2.hs, die ein Quellprogramm vom Typ commands aus der Datei file und nach javaStackP überführt.
343
16.6 Generischer XMLstore-Compiler (siehe Compiler.hs)
compXML :: Compiler input m => XMLstore s1 s2 s3 s4 s5 s6 s7 s8 s9
-> TransM input m s1
compXML alg = storeC where
storeC
= do tstring "<store>"
csum [do stck <- stock'; return $ store alg stck,
do ords <- ordersC; stck <- stock'
return $ storeO alg ords stck]
stock'
= do tstring "<stock>"; stck <- stockC
tstring "</stock>"; tstring "</store>"; return stck
ordersC
= do (p,is) <- order
csum [do os <- ordersC
return $ orders alg p is os,
return $ embedO alg p is]
order
= do tstring "<order>"; tstring "<customer>"
p <- personC; tstring "</customer>"
is <- itemsC; tstring "</order>"; return (p,is)
personC
= do tstring "<name>"; name <- text; tstring "</name>"
csum [do ems <- emailsC
return $ personE alg name ems,
return $ person alg name]
emailsC
= csum [do em <- emailC; ems <- emailsC
return $ emails alg em ems,
return $ none alg]
344
emailC
= do tstring "<email>"; em <- text; tstring "</email>"
return $ email alg em
itemsC
= do (id,price) <- item
csum [do is <- itemsC; return $ items alg id price is,
return $ embedI alg id price]
item
= do tstring "<item>"; id <- idC; tstring "<price>"
price <- text; tstring "</price>"
tstring "</item>"; return (id,price)
stockC
= do (id,qty,supps) <- iqs
csum [do is <- stockC
return $ stock alg id qty supps is,
return $ embedS alg id qty supps]
iqs
= do tstring "<item>"; id <- idC; tstring "<quantity>"
qty <- tint; tstring "</quantity>"
supps <- suppliers; tstring "</item>"
return (id,qty,supps)
suppliers = csum [do tstring "<supplier>"; p <- personC
tstring "</supplier>"; return $ supplier alg p,
do stck <- stockC; return $ parts alg stck]
idC
= do tstring "<id>"; t <- text; tstring "</id>"
return $ id_ alg t
text :: Compiler input m => TransM input m String
text = do strs <- some $ token $ some $ nchar "< \n\t"
return $ unwords strs
345
17 Induktion, Coinduktion und rekursive Gleichungen
Induktion
Sei CΣ = (S, I, C) eine konstruktive Signatur. Die zentrale Methode zum Beweis von
Eigenschaften der initialen CΣ-Algebra - unabhängig von deren konkreter Repräsentation
– ist die Induktion. Ihre Korrektheit (soundness) folgt aus Satz 3.2 (3):
Sei A eine initiale CΣ-Algebra.
A ist die einzige CΣ-Unteralgebra von A.
(1)
Sei ϕ eine prädikatenlogische Formel, die eine Eigenschaft der Elemente von A beschreibt,
und B = {a ∈ A | a erfüllt ϕ}. Die Frage, ob ϕ für alle Elemente von A gilt, reduziert sich
wegen (1) auf die Frage, ob B eine Σ-Unteralgebra von A ist, ob also
für alle c : e → s ∈ C cA(Be) ⊆ Bs gilt.
(2)
Induktion heißt demnach: die Gültigkeit von ∀ x : ϕ(x) in TCΣ aus einem Beweis von (2)
schließen.
Induktion erlaubt es u.a., die Korrektheit als Gleichungen formulierter rekursiver Programme zu beweisen, indem man zeigt, dass deren gewünschte Semantik die Gleichungen löst:
346
Rekursive Ψ-Gleichungssysteme
Sei CΣ = (S, BS, BF, C) eine konstruktive, DΣ = (S, BS 0, BF 0, D) eine destruktive
Signatur und Σ = CΣ ∪ DΣ. Dann nennen wir Ψ = (CΣ, DΣ) eine Bisignatur.
Eine Menge
E = {dc(x1, . . . , xnc ) = td,c | c : e1 × · · · × enc → s ∈ C, d : s → e ∈ D}
von Σ-Gleichungen (siehe Kapitel 3) mit folgenden Eigenschaften heißt rekursives ΨGleichungssystem:
• Für alle d ∈ D und c ∈ C, free(td,c) ⊆ {x1, . . . , xnc }.
• C ist die Vereinigung disjunkter Mengen C1 und C2.
• Für alle d ∈ D, c ∈ C1 und Teilterme du von td,c ist u eine Variable und td,c ein Term
ohne Elemente von C2.
• Für alle d ∈ D, c ∈ C2, Teilterme du und Pfade p (der Baumdarstellung) von td,c
besteht u aus Destruktoren und einer Variable und kommt auf p höchstens einmal ein
Element von C2 vor.
347
Induktive Lösungen
Sei Σ0 eine Teilsignatur einer Signatur Σ und A eine Σ-Algebra. Die in A enthaltene Σ0Algebra heißt Σ0-Redukt von A und wird mit A|Σ0 bezeichnet.
Sei E ein rekursives Ψ-Gleichungssystem und A eine CΣ-Algebra.
Eine induktive Lösung von E in A ist eine Σ-Algebra, deren CΣ-Redukt mit A übereinstimmt und die E erfüllt.
Lambeks Lemma Sei s ∈ S.
(1) Sei {c1 : e1 → s, . . . , cn : en → s} = {c : e → s0 ∈ C | s0 = s}. Die Extension
A
[cA
1 , . . . , cn ] ist bijektiv. Es gibt also eine Funktion
dA
s : As → Ae1 + · · · + Aen
A
A
A
A
A
mit [cA
1 , . . . , cn ] ◦ ds = idAs und ds ◦ [c1 , . . . , cn ] = idAe1 +···+Aen .
(2) Sei {d1 : s1 → e1, . . . , d1 : sn → en} = {d : s0 → e ∈ C | s0 = s}. Die Extension
A
hdA
1 , . . . , dn i ist bijektiv. Es gibt also eine Funktion
cA
s : Ae1 × . . . × Aen → As
A
A
A
A
A
mit cA
s ◦ hd1 , . . . , dn i = idAs und hd1 , . . . , dn i ◦ cs = idAe1 ×...×Aen .
o
348
Satz 17.1 Sei C2 leer und A eine initiale CΣ-Algebra. Dann hat E genau eine induktive
Lösung in A.
Beweis. Zunächst wird die Existenz einer induktiven Lösung von E in A durch Induktion
bewiesen. Wir zeigen, dass B mit
Bs = {a ∈ As | für alle d : s → e ∈ D ist dA(a) definiert},
s ∈ S,
eine Unteralgebra von A ist.
Sei also a ∈ As. Wegen Lambeks Lemma (1) gibt es 1 ≤ i ≤ n und b ∈ Aei mit dA
s (a) =
(b, i) und cA
i (b) = a.
Sei d : s → e0 ∈ D und b = (a1, . . . , anci ) ∈ Bei , d.h. für alle 1 ≤ j ≤ nci ist dA(aj ) definiert. Dann ist auch a0 = val[a1/x1, . . . , anci /xnci ]∗(td,ci ) definiert, wobei val eine beliebige
Variablenbelegung ist.
Um dA an der Stelle a durch a0 zu definieren, bleibt zu zeigen, dass die Darstellung von a
als Applikation cA
i (b) eines Konstruktors eindeutig ist.
0
Sei 1 ≤ j ≤ n und b0 ∈ Aej mit a = cA
j (b ). Dann ist
(4)
A A 0
A
A
A
0
0
0
(b, i) = dA
s (a) = ds (cj (b )) = ds ([c1 , . . . , cn ](b , j)) = (idAe1 +···+Aen )(b , j) = (b , j),
also b = b0 und ci = cj .
0
A
Folglich liefert dA(cA
i (b)) = a eine eindeutige Definition von d an der Stelle a. Damit gilt
349
(2) und wir schließen aus (1), dass dA(a) für alle a ∈ As (eindeutig) definiert ist.
Auch die Eindeutigkeit der induktiven Lösung von E in A lässt sich durch Induktion zeigen:
Seien A1, A2 zwei Lösungen von E in A.
Wir zeigen, dass B mit
Bs = {a ∈ As | für alle d : s → e ∈ D, dA1 (a) = dA2 (a)}
eine Unteralgebra von A ist.
Sei c : e → s ∈ C, d : s → e0 ∈ D und a = (a1, . . . , anc ) ∈ Be, d.h. für alle 1 ≤ i ≤ nc ist
dA1 (ai) = dA2 (ai). Daraus folgt für val1 : V → A1 und val2 : V → A2 mit
val1(xi) = val2(xi) = ai:
dA1 (cA(a))
A1 löst E
=
val1∗(td,c) = val2∗(td,c)
A2 löst E
=
dA2 (cA(a)).
Damit gilt (2) und wir schließen aus (1), dass dA1 mit dA2 übereinstimmt.
o
Beispiel 17.2 (CΣ = Nat; siehe 2.9)
Seien
DΣ = ({nat}, ∅, ∅, {plus : nat → natnat, sum : nat → nat}),
350
Ψ = (Nat, DΣ) und x, y Variablen. Dann bilden die Gleichungen
plus(zero)
= λx.x,
plus(succ(x)) = λy.succ(plus(x)(y)),
sum(zero)
= zero,
sum(succ(x)) = plus(sum(x))(succ(x))
ein rekursives Ψ-Gleichungssystem E, das in der initialen Nat-Algebra Ini mit Ini nat = N,
zeroIni = 0 und succIni = λn.n + 1 die induktive Lösung A hat mit plusA = λm.λn.m + n
und sumA = λn.n ∗ (n + 1)/2.
Nach Satz 17.1 ist A die einzige induktive Lösung von E in Ini.
o
Beispiel 17.3 (CΣ = List(X); siehe 2.9)
Seien X eine Menge mit Halbordnung ≤: X × X → 2,
DΣ = ({list}, {X, 2}, {≤: X × X → 2, ∗ : 2 × 2 → 2},
{sorted : list → 2, sorted0 : list → 2X }),
Ψ = (List(X), DΣ) und x, y, s Variablen. Dann bilden die Gleichungen
sorted(nil)
sorted(cons(x, s))
sorted0(nil)
sorted0(cons(x, s))
=
=
=
=
1,
sorted0(s)(x),
λx.1,
λy.(y ≤ x) ∗ sorted0(s)(x)
351
ein rekursives Ψ-Gleichungssystem E, das in der initialen List(X)-Algebra Ini mit Ini list =
X ∗, nilIni = , und consIni = λ(x, w).xw die induktive Lösung A hat mit
sortedA(w) = 1
⇔ w ist bzgl. ≤ sortiert,
sorted0A(w)(x) = 1 ⇔ xw ist bzgl. ≤ sortiert.
o
Nach Satz 17.1 ist A die einzige induktive Lösung von E in Ini.
Beispiel 17.4 (CΣ = Reg(BS); siehe 2.9 und 2.10) Sei X =
S
CS,
DΣ = ({reg}, {2, X}, {max, ∗ : 2 × 2 → 2} ∪ { ∈ C : X → 2 | C ∈ CS},
{δ : reg → reg X , β : reg → 2})
und Ψ = (Reg(BS), DΣ). Dann bildet die Menge BRE der Brzozowski-Gleichungen von
Abschnitt 3.11 ein rekursives Ψ-Gleichungssystem, das in der initialen Reg(BS)-Algebra
TReg(BS) die in Abschnitt 2.7 durch Bro(BS) definierte induktive Lösung A hat.
Nach Satz 17.1 ist A die einzige induktive Lösung von BRE in TReg(BS).
o
Weitere Induktionsverfahren, mit denen Eigenschaften kleinster Relationen bewiesen werden und die daher nicht nur in initialen Modellen anwendbar sind, werden in meinen LVs
über Logisch-Algebraischen Systementwurf behandelt.
352
Coinduktion
Sei DΣ = (S, I, D) eine destruktive Signatur. Die zentrale Methode zum Beweis von
Eigenschaften der finalen DΣ-Algebra - unabhängig von deren konkreter Repräsentation –
ist die Coinduktion. Ihre Korrektheit folgt aus Satz 3.4 (3):
Sei A eine finale DΣ-Algebra.
Die Diagonale von A ist die einzige DΣ-Kongruenz auf A.
(1)
Sei E eine Menge von Σ-Gleichungen und A zu einer Σ-Algebra erweiterbar.
Wegen (1) gilt E in A, wenn
RE = {(g ∗(t), g ∗(u)) ∈ A2 | t = u ∈ E, g : V → A}
in einer DΣ-Kongruenz R enthalten ist (siehe Termäquivalenz und Normalformen).
Coinduktion heißt demnach: die Gültigkeit von E in A aus der Existenz einer DΣKongruenz schließen, die RE enthält.
Beispiel 17.5
Die finale Stream(X)-Algebra A = StreamFun(X ) = (X N, Op) interpretiert die Destruktoren von Stream(X) (siehe 2.10) bzw. die Konstruktoren evens : list → list und
353
zip : list × list → list wie folgt:
Für alle f : N → X und n ∈ N,
headA(f )
tailA(f )
= f (0),
= λn.f
(n + 1),
f (2n)
falls n gerade ist,
evensA(f )(n) =
f (2n + 1) sonst,
zipA(f, g)(n) =
f (n/2)
falls n gerade ist,
g(n/2 + 1) sonst.
Fin erfüllt die Gleichungen
head(evens(s))
tail(evens(s))
head(zip(s, s0))
tail(zip(s, s0))
=
=
=
=
head(s)
evens(tail(tail(s)))
head(s)
zip(s0, tail(s))
(1)
(2)
(3)
(4)
mit s, s0 ∈ Vlist. Die Gültigkeit der Gleichung
evens(zip(s, s0)) = s
(5)
in Fin soll allein unter Verwendung von (1)-(4) gezeigt werden.
354
Gemäß Coinduktion gilt (5) in Fin, falls
RE = {(evensA(zipA(f, g)), f ) | f, g : N → X}
eine Stream(X)-Kongruenz ist, d.h. falls für alle (f, g) ∈ RE Folgendes gilt:
headA(f ) = headA(g) ∧ (tailA(f ), tailA(g)) ∈ R.
(6)
Beweis von (6). Sei (f, g) ∈ RE . Dann gibt es h : N → X mit f = evensA(zipA(g, h)).
Daraus folgt
(1)
(3)
headA(f ) = headA(evensA(zipA(g, h))) = headA(zipA(g, h)) = headA(g),
(2)
tailA(f ) = tailA(evensA(zipA(g, h))) = evensA(tailA(tailA(zipA(g, h))))
(4)
(4)
= evensA(tailA(zipA(h, tail(g)))) = evensA(zipA(tailA(g), tailA(h))).
Daraus folgt (tailA(f ), tailA(g)) = (evensA(zipA(tailA(g), tailA(h))), tailA(g)) ∈ RE . o
Sei A eine Σ-Algebra und R ⊆ A2. Der C-Abschluss RC von R ist die kleinste Äquivalenzrelation auf A, die R enthält und für alle c : e → e0 ∈ C und a, b ∈ Ae folgende
Bedingung erfüllt:
(a, b) ∈ ReC ⇒ (cA(a), cA(b)) ∈ ReC0 .
R ist eine DΣ-Kongruenz modulo C, wenn für alle d : s → e ∈ D und a, b ∈ Ae gilt:
(a, b) ∈ Rs ⇒ (dA(a), dA(b)) ∈ ReC .
355
Ist A final und gibt es ein rekursives Ψ-Gleichungssystem, dann ist der C-Abschluss einer
DΣ-Kongruenz modulo C auf A nach Satz 17.7 (11) eine DΣ-Kongruenz.
Deshalb folgt die Gültigkeit von E in A bereits aus der Existenz einer DΣ-Kongruenz
modulo C, die RE enthält.
356
Coinduktion modulo C heißt demnach: die Gültigkeit von E in A aus der Existenz
einer DΣ-Kongruenz modulo C schließen, die RE enthält.
Beispiel 17.6
S
Sei X = CS und A die (Reg(BS) ∪ Acc(X))-Algebra mit
A|Reg(BS) = Lang(X) und A|Acc(X) = P(X) (siehe 2.7). A erfüllt die Gleichungen
δ(par(t, u))
δ(seq(t, u))
β(par(t, u))
β(seq(t, u))
par(par(t1, u1), par(t2, u2))
par(t, mt)
=
=
=
=
=
=
λx.par(δ(t)(x), δ(u)(x))
λx.par(seq(δ(t)(x), u), ite(β(t), δ(u)(x), mt))
max{β(t), β(u)}
β(t) ∗ β(u)
par(par(t1, t2), par(u1, u2))
t
(1)
(2)
(3)
(4)
(5)
(6)
mit t, u, v ∈ Vreg und x ∈ VX . Die Gültigkeit des Distributivgesetzes
seq(t, par(u, v)) = par(seq(t, u), seq(t, v))
(7)
in A soll allein unter Verwendung von (1)-(6) gezeigt werden.
357
Gemäß Coinduktion modulo C = {par} gilt (7) in A, falls
RE = {(seq A(f, parA(g, h)), parA(seq A(f, g), seq A(f, h))) | f, g, h ∈ Lang(X)}
eine Acc(X)-Kongruenz modulo C ist, d.h. falls für alle (f, g) ∈ RE Folgendes gilt:
β A(f ) = β A(g) ∧ (δ A(f ), δ A(g)) ∈ REC .
(8)
Beweis von (8). Sei (f, g) ∈ RE und x ∈ X. Dann gibt es f 0, g 0, h ∈ A mit
f = seq A(f 0, parA(g 0, h)) und g = parA(seq A(f 0, g 0), seq A(f 0, h))).
Daraus folgt
(4)
β A(f ) = β A(seq A(f 0, parA(g 0, h))) = β A(f 0) ∗ β A(parA(g 0, h))
(3)
= β A(f 0) ∗ max{β A(g 0), β A(h)} = max{β A(f 0) ∗ β A(g 0), β A(f 0) ∗ β A(h))}
(4)
= max{β A(seq A(f 0, g 0)), β A(seq A(f 0, h))}
(3)
= β A(parA(seq A(f 0, g 0), seq A(f 0, h))) = β A(g),
δ A(f )(x) = δ A(seq A(f 0, parA(g 0, h)))
(2)
= parA(seq A(δ A(f 0)(x), parA(g 0, h)), if β A(f 0) = 1 then δ A(parA(g 0, h))(x) else mtA)
(1)
= parA(seq A(δ A(f 0)(x), parA(g 0, h)),
if β A(f 0) = 1 then parA(δ A(g 0)(x) else δ A(h)(x), mtA)),
358

 parA(seq A(δ A(f 0)(x), parA(g 0, h)),
(6)
=
parA(δ A(g 0)(x), δ A(h)(x)))
falls β A(f 0) = 1

seq A(δ A(f 0)(x), parA(g 0, h))
sonst,
δ A(g)(x) = δ A(parA(seq A(f 0, g 0), seq A(f 0, h))))(x)
(1)
= parA(δ A(seq A(f 0, g 0))(x), δ A(seq A(f 0, h))(x))
(2)
= parA(parA(seq A(δ A(f 0)(x), g 0), if β A(f 0) = 1 then δ A(g 0)(x) else mtA),
A
A A 0
A 0
A
A
 par (seq (δ (f )(x), h), if β (f ) = 1 then δ (h)(x) else mt ))

parA(parA(seq A(δ A(f 0)(x), g 0), δ A(g 0)(x)),



parA(seq A(δ A(f 0)(x), h), δ A(h)(x))) falls β A(f 0) = 1
=
parA(parA(seq A(δ A(f 0)(x), g 0), mtA),




parA(seq A(δ A(f 0)(x), h), mtA))
sonst

 parA(parA(seq A(δ A(f 0)(x), g 0), seq A(δ A(f 0)(x), h)),
(5),(6)
=
parA(δ A(g 0)(x), δ A(h)(x)))
falls β A(f 0) = 1

parA(seq A(δ A(f 0)(x), g 0), seq A(δ A(f 0)(x), h))
sonst.
Sei
f1 = seq A(δ A(f 0)(x), parA(g 0, h)),
f2 = parA(seq A(δ A(f 0)(x), g 0), seq A(δ A(f 0)(x), h)),
f3 = parA(δ A(g 0)(x), δ A(h)(x)).
359
Wegen (f1, f2) ∈ RE gilt also
(δ A(f )(x), δ A(g)(x)) = (parA(f1, f3), parA(f2, f3)) ∈ REC
im Fall β A(f 0) = 1 und
(δ A(f )(x), δ A(g)(x)) = (f1, f2) ∈ REC
im Fall β A(f 0) = 0.
o
Weitere Coinduktionsverfahren, mit denen Eigenschaften größter Relationen bewiesen werden und die daher nicht nur in finalen Modellen anwendbar sind, werden in meinen LVs
über Logisch-Algebraischen Systementwurf behandelt.
Coinduktive Lösungen
Sei E ein rekursives Ψ-Gleichungssystem und A eine DΣ-Algebra.
Eine coinduktive Lösung von E in A ist eine Σ-Algebra, deren DΣ-Redukt mit A
übereinstimmt und die E erfüllt.
360
Satz 17.7 Sei A eine finale DΣ-Algebra. Dann hat E genau eine coinduktive Lösung in
A, die unten zur Σ-Algebra TE erweiterte Termalgebra TCΣ erfüllt E und es gilt
unfold TE = fold A.
Beweis. Sei V eine (S∪BS)-sortierte Variablenmenge, die die Trägermengen von A enthält.
Wir erweitern die Menge TCΣ(A) der CΣ-Terme über A wie folgt zur Σ-Algebra TE (A):
Für alle Operationssymbole f : e → e0 von Σ und t ∈ TCΣ(A)e,
f TE (A)(t) = eval(f t),
wobei eval : TΣ(V ) → TCΣ(V ) wie unten definiert ist. Die für eine induktive Definition
erforderliche wohlfundierte Ordnung auf den Argumenttermen von eval lautet wie folgt:
Für alle t, t0 ∈ TΣ(V ),
t t0 ⇔def (depC2 (t), depD (t), size(t)) >lex (depC2 (t0), depD (t0), size(t0)).
>lex ⊆ N3 ×N3 bezeichnet die lexikographische Erweiterung von >⊆ N×N auf Zahlentripel.
Sei G ⊆ F . size(t) bezeichnet die Anzahl der Symbole von t, depG(t) die maximale Anzahl
von G-Symbolen auf einem Pfad von t. .
Die induktive Definition von eval lautet wie folgt:
• Für alle x ∈ V , eval(x) = x.
• Für alle f : X → Y ∈ BF ∪ BF 0 und x ∈ X, eval(f x) = f (x).
361
• Für alle f : s → e ∈ D und a ∈ As, eval(f (a)) = f A(a).
• Für alle x ∈ V und t ∈ TΣ(V ), eval(λx.t) = λx.eval(t).
• Für alle c : e1 × · · · × en → s ∈ C und ti ∈ TΣ(V )ei , 1 ≤ i ≤ n,
eval(c(t1, . . . , tn)) = c(eval(t1), . . . , eval(tn)).
• Für alle t, u ∈ TΣ(V ), eval(t(u)) = eval(t)(eval(u)).
• Für alle t, u, v ∈ TΣ(V ), eval(ite(t, u, v)) = ite(eval(t), eval(u), eval(v)).
• Für alle d : s → e0 ∈ D, c : e → s ∈ C und (t1, . . . , tn) ∈ TΣ(V )e,
eval(dc(t1, . . . , tn)) = u{t1/x1, . . . , tn/xn, eval(u1σ)/z1, . . . , eval(uk σ)/zk },
wobei u ∈ TCΣ(V ), {z1, . . . , zk } = var(u) \ {x1, . . . , xn}, u1, . . . , uk ∈ TΣ(V )
Destruktoren und Variablen bestehen, σ = {t1/x1, . . . , tn/xn} und
(1)
(2)
(3)
aus
td,c = u{u1/z1, . . . , uk /zk }.
• Für alle d : s → e ∈ D, d0 : s0 → s ∈ D und u ∈ TΣ(V )s0 ,
eval(dd0u) = eval(d eval(d0u)).
(4)
(5) Für alle t ∈ TΣ(V ) ist eval(t) definiert und depC2 (eval(t)) ≤ depC2 (t).
Beweis von (5) durch Induktion über t entlang .
Fall (1): Es gibt f : e → e0 ∈ BF ∪ BF 0 ∪ D und a ∈ Ae mit t = f a. Dann ist
eval(t) = f (a) bzw. eval(t) = f A(a) und depC2 (eval(t)) = 0 = depC2 (t).
362
Fall (2): Es gibt c : e → s ∈ C ∪ {λx. | x ∈ V } ∪ { ( ), ite} und (t1, . . . , tn) ∈ TΣ(v)e
mit t = c(t1, . . . , tn). Sei 1 ≤ i ≤ n.
Ist c ∈ C2, dann gilt depC2 (ti) < depC2 (t). Ist c 6∈ C2, dann gilt depG(eval(ti)) ≤ depG(t)
für G ∈ {C2, D}, aber size(ti) < size(t). Demnach gilt t ti in beiden Unterfällen. Also
ist nach Induktionsvoraussetzung eval(ti) definiert und depC2 (eval(ti)) ≤ depC2 (ti).
Daraus folgt, dass auch eval(t) = c(eval(t1), . . . , eval(tn)) definiert ist und
depC2 (eval(t)) = max{depC2 (eval(ti)) | 1 ≤ i ≤ n} ≤ max{depC2 (ti) | 1 ≤ i ≤ n}
= depC2 (t)
im Fall c ∈ C1 bzw.
depC2 (eval(t)) = 1 + max{depC2 (eval(ti)) | 1 ≤ i ≤ n}
≤ 1 + max{depC2 (ti) | 1 ≤ i ≤ n} = depC2 (t)
im Fall c ∈ C2.
Fall (3): Es gibt d : s → e0 ∈ D, c : e → s ∈ C und (t1, . . . , tn) ∈ TΣ(V )e mit
t = dc(t1, . . . , tn). Seien k, u, σ, z1, . . . , zk , u1, . . . , uk wie oben und 1 ≤ i ≤ k. Ist c ∈ C1,
dann ist uiσ ein echter Teilterm von t und damit depG(uiσ) ≤ depG(t) für G ∈ {C2, D},
aber size(uiσ) < size(t). Ist c ∈ C2, dann gilt depC2 (uiσ) < depC2 (t).
Folglich gilt t uiσ in beiden Unterfällen. Also ist nach Induktionsvoraussetzung eval(uiσ)
definiert und depC2 (eval(uiσ)) ≤ depC2 (uiσ).
363
Demnach ist auch
eval(t) = u{t1/x1, . . . , tn/xn, eval(u1σ)/z1, . . . , eval(uk σ)/zk }
definiert und depC2 (eval(t)) ≤ depC2 (t), weil jeder Pfad von u im Fall c ∈ C1 kein C2Symbol und im Fall c ∈ C2 höchstens eins enthält.
Fall (4): Es gibt d : s → e ∈ D, d0 : s0 → s ∈ D und u ∈ TΣ(V )s0 mit t = dd0u.
Dann gilt depC2 (d0u) ≤ depC2 (t), aber depD (d0u) ≤ depD (t), also t d0u. Damit ist
nach Induktionsvoraussetzung eval(d0u) definiert und depC2 (eval(d0u)) ≤ depC2 (d0u), also
auch depC2 (d eval(d0u)) = depC2 (eval(d0u)) ≤ depC2 (t). Wegen eval(d0u) ∈ TCΣ(V ) ist
jedoch depD (d eval(d0u)) < depD (t), so dass nach Induktionsvoraussetzung auch eval(t) =
eval(d eval(d0u)) definiert ist und depC2 (eval(d eval(d0u))) ≤ depC2 (d eval(d0u)). Daraus
folgt schließlich depC2 (eval(t)) ≤ depC2 (d eval(d0u)) ≤ depC2 (t).
o
Wie man ebenfalls durch Induktion über t entlang zeigen kann, kommen alle Variablen von eval(t) in t vor. Daraus folgt f TE (A)(t) = eval(f t) ∈ TCΣ(A) und f TE (A)(u) =
eval(f u) ∈ TCΣ für alle Operationssymbole f : e → e0 von Σ, t ∈ TCΣ(A)e und u ∈ TCΣ,e.
Folglich ist die Einschränkung TE von TE (A) auf die Menge TCΣ der CΣ-Grundterme eine
DΣ-Unteralgebra von TE (A).
364
(6) Für alle c : e → s ∈ C und (t1, . . . , tn) ∈ TCΣ(A)e, cTE (A)(t1, . . . , tn) = c(t1, . . . , tn).
Beweis von (6).
eval(t) = t für alle t ∈ TCΣ(A) erhält man durch Induktion über die Größe von t. Daraus
folgt
c
TE (A)
(t1, . . . , tn)
eval(ti )=ti
=
Def . cTE (A)
=
(2)
eval(c(t1, . . . , tn)) = c(eval(t1), . . . , eval(tn))
c(t1, . . . , tn).
(7) Für alle g : V → TCΣ(A) und Σ-Terme t, die aus Destruktoren und einer Variable x
bestehen, gilt eval(tσ) = g ∗(t), wobei σ = {g(x)/x}.
Beweis von (7) durch Induktion über die Anzahl der Destruktoren von t.
Sei d1, . . . , dn ∈ D und t = d1 . . . dnx. Ist n = 0, dann gilt
eval(tσ) = eval(xσ) = eval(g(x))
Andernfalls ist
g(x)∈TCΣ (A)
=
g(x) = g ∗(x) = g ∗(t).
(4)
eval(tσ) = eval(d1 . . . dnxσ) = eval(d1 eval(d2 . . . dnxσ))
ind. hyp.
=
Def . g ∗
=
T (A)
∗
eval(d1 g (d2 . . . dnx))
g ∗(d1 . . . dnx) = g ∗(t).
Def . d1 E
=
T (A)
d1 E
(g ∗(d2 . . . dnx))
o
365
(8) TE (A) erfüllt E.
Beweis von (8).
Für alle c : s1 × · · · × sn → s ∈ C, d : s → e ∈ D und σ = g : V → TCΣ(A),
∗
g (dc(x1, . . . , xn))
Def . dTE (A)
=
Def . g ∗
=
dTE (A)(cTE (A)(g(x1), . . . , g(xn)))
(6)
eval(dcTE (A)(g(x1), . . . , g(xn))) = eval(dc(g(x1), . . . , g(xn)))
(3)
= u{x1σ/x1, . . . , xnσ/xn, eval(u1σ)/z1, . . . , eval(uk σ)/zk }
(7)
= u{x1σ/x1, . . . , xnσ/xn, g ∗(u1)/z1, . . . , g ∗(uk )/zk }
u∈TCΣ (V )
=
g ∗(td,c).
o
Da TE (A) eine DΣ-Algebra und A die finale DΣ-Algebra ist, gibt es den eindeutigen DΣHomomorphismus unfold TE (A) : TE (A) → A.
(9) Für alle a ∈ A, unfold TE (A)(a) = a.
Beweis von (9).
Für alle d : s → e ∈ D gilt dA(a) = dTE (A)(a). Folglich sind die Inklusion
incA : A → TCΣ(A) und daher auch die Komposition
unfold TE (A) ◦ incA : A → A
DΣ-homomorph. Also stimmt diese wegen der Finalität von A mit der Identität auf A
überein.
o
366
A lässt sich zur CΣ-Algebra erweitern: Für alle c : e → s ∈ C und a ∈ Ae,
cA(a) =def unfold TE (A)(c(a)).
(10)
Für alle c : s1 × · · · × sn → s ∈ C, d : s → e ∈ D und g : V → A,
g ∗(dc(x1, . . . , xn)) = dA(cA(g(x1), . . . , g(xn)))
(10)
= dA(unfold TE (A)(c(g(x1), . . . , g(xn))))
unfold TE (A) DΣ−homomorph
=
unfold TE (A)(dTE (A)(c(g(x1), . . . , g(xn))))
(6)
= unfold TE (A)(dTE (A)(cTE (A)(g(x1), . . . , g(xn)))) = unfold TE (A)(g ∗(dc(x1, . . . , xn)))
(8)
(9)
= unfold TE (A)(g ∗(td,c)) = g ∗(td,c).
Also gibt es eine coinduktive Lösung von E in A.
(11) Die größte DΣ-Kongruenz R ist eine CΣ-Kongruenz.
Beweis von (11). Sei RC der C-Abschluss von R (s.o.). Ist RC eine DΣ-Kongruenz, dann
ist RC in R enthalten, weil R die größte DΣ-Kongruenz ist. Andererseits ist R in RC
enthalten. Also stimmt R mit RC überein, ist also wie RC eine CΣ-Kongruenz. Demnach
bleibt zu zeigen, dass RC eine DΣ-Kongruenz ist.
Sei also d : s → e ∈ D und (t, u) ∈ RsC .
367
Gehört (t, u) zu R, dann gilt das auch für (dTE (A)(t), dTE (A)(u)), weil R eine DΣ-Kongruenz
ist. Wegen R ⊆ RC folgt (dTE (A)(t), dTE (A)(u)) ∈ ReC .
Andernfalls gibt es c : s1 × · · · × sn → s ∈ C und t1, . . . , tn, u1, . . . , un ∈ TCΣ(A) mit
t = c(t1, . . . , tn), u = c(u1, . . . , un) und (ti, ui) ∈ RC für alle 1 ≤ i ≤ n.
Nach Induktionsvoraussetzung gilt (d0TE (A)(ti), d0TE (A)(ui)) ∈ ReC0 für alle 1 ≤ i ≤ n und
d0 : si → e0 ∈ D. Seien g, g 0 Belegungen von V in TCΣ(A) mit g(xi) = ti und g 0(xi) = ui
für alle 1 ≤ i ≤ n.
Wegen
(6)
(8)
dTE (A)(t) = dTE (A)(c(t1, . . . , tn)) = dTE (A)(cTE (A)(t1, . . . , tn)) = g ∗(td,c),
(6)
(8)
dTE (A)(u) = dTE (A)(c(u1, . . . , un) = dTE (A)(cTE (A)(u1, . . . , un)) = g 0∗(td,c)
und weil RC eine CΣ-Kongruenz auf TCΣ(A) ist, folgt (dTE (A)(t), dTE (A)(u)) ∈ ReC aus
(g(xi), g 0(xi)) ∈ RC für alle 1 ≤ i ≤ n. Also ist RC eine DΣ-Kongruenz.
o
(11) liefert folgende CΣ-Algebra B mit den Trägermengen von A:
Für alle c : e → s ∈ C und t ∈ TCΣ(A)e,
cB (unfold TE (A)(t)) =def unfold TE (A)(c(t)).
(12)
368
cB ist wohldefiniert: Sei t, u ∈ TCΣ(A)e mit unfold TE (A)(t) = unfold TE (A)(u). Da A final
ist, stimmt R nach Satz 3.4 (3) mit dem Kern von unfold TE (A) überein.
Also impliziert (11), dass der Kern von unfold TE (A) eine CΣ-Kongruenz auf TCΣ(A) ist.
Daraus folgt
(12)
(6)
cB (unfold TE (A)(t)) = unfold TE (A)(c(t)) = unfold TE (A)(cTE (A)(t))
(6)
(12)
= unfold TE (A)(cTE (A)(u)) = unfold TE (A)(c(u)) = cB (unfold TE (A)(u)).
(13) cB stimmt mit cA überein: Für alle a ∈ A,
(9)
(12)
(10)
cB (a) = cB (unfold TE (A)(a)) = unfold TE (A)(c(a)) = cA(a).
(14) unfold TE (A) ist CΣ-homomorph: Für alle c : e → s ∈ C und t ∈ TCΣ(A)e,
(6)
(12)
(13)
unfold TE (A)(cTE (A)(t)) = unfold TE (A)(c(t)) = cB (unfold TE (A)(t)) = cA(unfold TE (A)(t)).
Sei TE die DΣ-Unteralgebra von TE (A) mit Trägermenge TCΣ.
(15) unfold TE = fold A: Da A eine finale DΣ-Algebra ist, existiert genau ein DΣ-Homomorphismus von TE nach A. Folglich stimmt unfold TE mit der Einschränkung von unfold TE (A)
auf TCΣ überein. Da incTCΣ : TCΣ → TE (A) wegen (6) und unfold TE (A) wegen (14) CΣhomomorph ist, folgt (15) aus der Initialität von TCΣ.
369
unfold TE
TE
id
incTCΣ
g
TCΣ
TE (A)
A
unfold TE (A)
(15)
fold A
id
g
A
Es bleibt zu zeigen, dass je zwei coinduktive Lösungen A1, A2 von E in A miteinander
übereinstimmen.
Sei Q die kleinste S-sortige Relation auf A1 × A2, die die Diagonale von A enthält und für
alle c : e → s ∈ C und a, b ∈ Ae die folgende Implikation erfüllt:
(a, b) ∈ Q ⇒ (cA1 (a), cA2 (b)) ∈ Q.
(16)
(17) Q ist eine DΣ-Kongruenz.
Beweis. Sei d : s → e ∈ D und (a, b) ∈ Qs. Gehört (a, b) zu ∆2A, dann gilt a = b, also
dA(a) = dA(b). Daraus folgt (dA(a), dA(b)) ∈ Qe, weil Q die Diagonale von A enthält.
370
Andernfalls gibt es c : s1 × · · · × sn → s ∈ C und a1, . . . , an, b1, . . . , bn ∈ A mit
a = cA1 (a1, . . . , an), b = cA2 (b1, . . . , bn) und (ai, bi) ∈ Q für alle 1 ≤ i ≤ n. Nach Induktionsvoraussetzung gilt (d0A(ai), d0A(bi)) ∈ Qe0 für alle 1 ≤ i ≤ n und
d0 : si → e0 ∈ D. Seien g, g 0 : V → A Belegungen mit g(xi) = ai und g 0(xi) = bi für alle
1 ≤ i ≤ n. Wegen
dA(a) = dA(cA1 (a1, . . . , an)) = g ∗(td,c),
dA(b) = dA(cA2 (b1, . . . , bn) = g 0∗(td,c)
und weil Q ein CΣ-Kongruenz ist, folgt (dA(a), dA(b)) ∈ Qe aus
(g(xi), g 0(xi)) ∈ Q für alle 1 ≤ i ≤ n.
o
Wegen der Finalität von A ist nach Satz 3.4 (3) die Diagonale von A die einzige DΣKongruenz auf A. Also impliziert (17), dass Q mit ∆2A übereinstimmt. Sei c : e → s ∈ C
und a ∈ Ae. Aus (a, a) ∈ Q und (16) folgt (cA1 (a), cA2 (a)) ∈ Qe, also cA1 (a) = cA2 (a)
wegen Q = ∆2A. Demnach gilt A1 = A2.
o
Satz 17.8 Sei C2 leer, A die laut Satz 17.7 eindeutige coinduktive Lösung von E in der
finalen DΣ-Algebra und B eine induktive Lösung von E in der initialen CΣ-Algebra TCΣ.
Dann ist fold A = unfold B .
0
Beweis. Nach Satz 17.7 kann TCΣ zu einer DΣ-algebra B 0, die E und fold A = unfold B
erfüllt, erweitert werden. Aus Satz 17.1 folgt B = B 0, also fold A = unfold B .
o
371
Beispiel 17.9 Sei
CΣ = ({list}, ∅, ∅, {evens, odds, exchange, exchange0 : list → list}),
Ψ = (CΣ, Stream(X)) und s eine Variable. Dann bilden die Gleichungen
head(evens(s))
head(odds(s))
head(exchange(s))
head(exchange0(s))
=
=
=
=
head(s),
head(tail(s)),
head(tail(s)),
head(s),
tail(evens(s))
tail(odds(s))
tail(exchange(s))
tail(exchange(s))
=
=
=
=
evens(tail(tail(s))),
odds(tail(tail(s))),
exchange0(s),
exchange(tail(tail(s)))
ein rekursives Ψ-Gleichungssystem E, das nach Satz 17.7 genau eine coinduktive Lösung
in der finalen Stream(X)-Algebra X N hat. evens(s) und odds(s) listen die Elemente von
s auf, die dort an geraden bzw. ungeraden Positionen stehen. exchange(s) vertauscht die
Elemente an geraden mit denen an ungeraden Positionen.
Satz 17.1 ist auf E nicht anwendbar, da hier die Menge C2 der Konstruktoren, in deren
Gleichungen auf der rechten Seite Destruktoren geschachtelt auftreten dürfen, nicht leer ist.
o
Beispiel 17.10
Sei Ψ = (Reg(BS), DΣ) die Bisignatur von Beispiel 17.4, Σ = Reg(BS) ∪ DΣ und A die
Σ-Algebra mit A|Reg(BS) = Lang(X) und A|Acc(X) = P(X).
372
A erfüllt das rekursive Ψ-Gleichungssystem BRE von Abschnitt 3.11 und ist daher nach
Satz 17.7 die einzige coinduktive Lösung von BRE in P(X).
∗
Da χ : P(X ∗) → 2X (siehe 2.7) Σ-homomorph ist, wird BRE auch von der Bildalgebra
χ(A) erfüllt. Demnach ist χ(A) nach Satz 17.7 die einzige coinduktive Lösung von BRE in
χ(P(X)) = Beh(X, 2).
Wegen TBRE = Bro(BS) folgen die Gleichungen
fold Lang(X) = unfold Bro(BS) : Bro(BS) → P(X),
fold regB = unfoldB Bro(BS) : Bro(BS) → Beh(X, 2)
aus Satz 17.8 und damit die Acc(X)-Homomorphie beider Faltungen.
Sei t ∈ TReg(BS) und v = fold Regword (BS)(t). Im Haskell-Modul Compiler.hs entspricht der
Erkenner fold regB (t) bzw. unfoldB Bro(BS)(t) dem Aufruf regToAlg "" v n mit n = 1
bzw. n = 3. Er startet eine Schleife, die nach der Eingabe von Wörtern w fragt, auf die der
Erkenner angewendet werden soll, um zu prüfen, ob w zur Sprache von t gehört oder nicht.
o
373
Entscheidende Argumente im Beweis von Satz 17.7 entstammen dem Beweis von [37], Thm.
3.1, und [38], Thm. A.1, wo rekursive Stream(X)-Gleichungen für Stromkonstruktoren
untersucht werden (siehe auch [7], Anhänge A.5 and A.6).
Zur Zeit werden Satz 17.7 ähnliche Theoreme entwickelt und auf weitere destruktive Signaturen – neben Stream(X) und Acc(X) – angewendet, z.B. auf unendliche Binärbäume
([39], Thm. 2), nichtdeterministische Systeme [1], Mealy-Automaten [6], nebenläufige Prozesse [10, 35, 11] und formale Potenzreihen ([37], Kapitel 9).
Hierbei wird oft von der traditionellen Darstellung rekursiver Gleichungssysteme als strukturell-operationelle Semantikregeln (SOS) ausgegangen, wobei “strukturell” und “operationell” für die jeweiligen Konstruktoren bzw. Destruktoren steht. Z.B. lauten die Gleichungen
von BRE als SOS-Regeln wie folgt:
δ
δ
eps −→ λx.mt
δ
mt −→ λx.mt
δ
B −→ λx.ite(x ∈ B, eps, mt)
δ
δ
t −→ t0, u −→ u0
δ
par(t, u) −→ λx.par(t0(x), u0(x))
δ
t −→ t0, u −→ u0
δ
seq(t, u) −→ λx.par(seq(t0(x), u),
ite(β(t), u0(x), mt))
374
δ
t −→ t0
δ
iter(t) −→ λx.seq(t0(x), iter(t))
β
eps −→ 1
β
β
t −→ m, u −→ n
β
par(t, u) −→ max{m, n}
β
β
mt −→ 0
B −→ 0
β
β
t −→ m, u −→ n
β
seq(t, u) −→ m ∗ n
β
iter(t) −→ 0
Im Gegensatz zur operationellen Semantik besteht eine denotationelle Semantik nicht aus
Regeln, sondern aus der Interpretation von Konstruktoren und Destruktoren in einer Algebra.
In der Kategorientheorie werden rekursive Gleichungssysteme und die Beziehungen zwischen
ihren Komponenten mit Hilfe von distributive laws und Bialgebren verallgemeinert
(siehe z.B. [40, 13, 8, 11]).
375
17.11 Cotermbasierte Erkenner regulärer Sprachen
Unter Verwendung der Haskell-Darstellung von Cotermen als Elemente des rekursiven Datentyps StateC(x)(y) (siehe Beispiel 10.4) machen wir DTAcc(X) zur Reg(BS)-Algebra
regC und zwar so, dass regC mit ξ(χ(Lang(X)) übereinstimmt:
Für alle B ∈ BS, f, g : X → DTAcc(X), b, c ∈ 2 und t ∈ DTAcc(X),
epsregC = StateC(λx.mtregC )(1),
mtregC = StateC(λx.mtregC )(0),
regC
B
= StateC(λx.if (x ∈ B) = 1 then epsregC else mtregC )(0),
parregC (StateC(f )(b), StateC(g)(c)) = StateC(λx.parregC (f (x), g(x)))(max{b, c}),
seq regC (StateC(f )(1), StateC(g)(c)) = StateC(λx.parregC (seq regC (f (x), StateC(g)(c)),
g(x)))(c),
seq regC (StateC(f )(0), t) = StateC(λx.seq regC (f (x), t))(0),
iterregC (StateC(f )(b)) = StateC(λx.seq regC (f (x), iterregC (StateC(f )(b))))(1).
Sei Ψ = (Reg(BS), DΣ) die Bisignatur von Beispiel 17.4, Σ = Reg(BS) ∪ DΣ und A die
Σ-Algebra mit A|Reg(BS) = Lang(X) und A|Acc(X) = DTAcc(X).
Da das rekursive Ψ-Gleichungssystem BRE (siehe 3.11) von A erfüllt wird und χ : P(X ∗) →
∗
∗
2X (siehe 2.7) und die Bijektion ξ : 2X → DTAcc(X) von Beispiel 2.20 Σ-homomorph sind,
wird das rekursive Ψ-Gleichungssystem BRE von Abschnitt 3.11 BRE auch von der Bildalgebra ξ(χ(A)) erfüllt.
376
Demnach ist ξ(χ(A)) nach Satz 17.7 die einzige coinduktive Lösung von BRE in
ξ(χ(P(X))) = ξ(Beh(X, 2)) = DTAcc(X).
Wegen TBRE = Bro(BS) folgt außerdem
fold regC = unfoldC Bro(BS) : Bro(BS) → DTAcc(X)
– analog zu Beispiel 17.10 – aus Satz 17.8 und damit die Acc(X)-Homomorphie dieser
Faltung.
Aus der Eindeutigkeit von unfoldC Bro(BS), der Acc(X)-Homomorphie von χ und ξ (s.o.)
und (1) in Abschnitt 2.11 erhalten wir
unfoldC Bro(BS) = ξ ◦ χ ◦ unfold Bro(BS) = ξ ◦ χ ◦ fold Lang(X).
(3)
Sei t ∈ TReg(BS). Wegen (3) realisiert der initiale Automat (Bro(BS), t) den Acc(X)Coterm ξ(χ(fold Lang(X)(t))).
Sei t ∈ TReg(BS) und v = fold Regword (BS)(t). Im Haskell-Modul Compiler.hs entspricht der
Erkenner unfoldB DTAcc(X) (fold regC (t)) dem Aufruf regToAlg "" v 2. Dieser startet eine
Schleife, die nach der Eingabe von Wörtern w fragt, auf die der Erkenner angewendet werden
soll, um zu prüfen, ob w zur Sprache von t gehört oder nicht.
o
377
18 Iterative Gleichungen
Sei Σ = (S, I, F ) eine konstruktive Signatur und I, V endliche S-sortige Variablenmengen.
Eine S-sortige Funktion
E : V → TΣ(V )
heißt iteratives Σ-Gleichungssystem, falls das Bild von E keine Variablen enthält.
Demnach sind iterative Gleichungssysteme spezielle Substitutionen (siehe Abschnitt 3.6).
Sei A eine Σ-Algebra und AV die Menge der S-sortigen Funktionen von V nach A.
g : V → A löst E in A, wenn für alle x ∈ V , g ∗(E(x)) = g(x) gilt.
Lemma 18.1
Sei E 0 eine Menge von Σ-Gleichungen, A ∈ AlgΣ,E 0 und reduce : TΣ(V ) → TΣ(V ) eine
Funktion, die jeden Σ-Term auf einen E 0-äquivalenten Term abbildet (siehe Kapitel 3). Für
alle Lösungen g : V → A von E in A und n ∈ N gilt
g ∗ = g ∗ ◦ (reduce ◦ E ∗)n.
Beweis durch Induktion über n.
g ∗ = g ∗ ◦ idTΣ(V ) = g ∗ ◦ (E ∗)0.
378
Da ≡E 0 die kleinste Σ-Kongruenz auf TΣ(V ) ist, die Inst(E 0) enthält, ist ≡E 0 im Kern von
g ∗ enthalten. Daraus folgt nach Voraussetzung
g ∗ ◦ reduce = g ∗,
also
g∗
ind. hyp.
=
g ∗ ◦ (reduce ◦ E ∗)n
3.7(1)
g löst E in A
=
(1)
(g ∗ ◦ E)∗ ◦ (reduce ◦ E ∗)n
(1)
= g ∗ ◦ E ∗ ◦ (reduce ◦ E ∗)n = g ∗ ◦ reduce ◦ E ∗ ◦ (reduce ◦ E ∗)n
= g ∗ ◦ (reduce ◦ E ∗)n+1.
o
18.2 Das iterative Gleichungssystem einer CFG
S
Sei G = (S, BS, R) eine CFG und X = BS.
Im Folgenden erlauben wir den Konstruktoren par and seq von Reg(BS) auch mehr als
zwei Argumente und schreiben daher
• par(t1, . . . , tn) anstelle von par(t1, par(t2, . . . , par(tn−1, tn) . . . )) und
• seq(t1, . . . , tn) anstelle von seq(t1, seq(t2, . . . , seq(tn−1, tn) . . . )).
par(t) und seq(t) stehen für t.
379
G induziert ein iteratives Reg(BS)-Gleichungssystem:
EG : S → TReg(BS)(S)
s 7→ par(w1, . . . , wk ),
wobei {w1, . . . , wk } = {w ∈ (S ∪ CS)∗ | s → w ∈ R}
und für alle n > 1, e1, . . . , en ∈ S ∪ CS and s ∈ S,
e1 . . . en = seq(e1, . . . , en),
s = s.
EG heißt Gleichungssystem von G.
Satz 18.3 (Fixpunktsatz für CFGs)
(i)
Die Funktion solG : S → Lang(X) mit solG(s) = L(G)s für alle s ∈ S löst EG in
Lang(X).
Sei g : S → P(X ∗) eine Lösung von EG in Lang(X).
(ii)
Die S-sortige Menge Sol mit Sol s = g(s) für alle s ∈ S ist Trägermenge einer
Σ(G)-Unteralgebra von Word (G).
(iii)
Für alle s ∈ S, solG(s) ⊆ g(s), m.a.W.: die Sprache von G ist die kleinste Lösung
von EG in Lang(X).
380
Beweis.
Sei g : V → Lang(X), s ∈ S, {r1, . . . , rk } die Menge aller Regeln von G mit linker Seite
s. Sei 1 ≤ i ≤ k,
ri = (s → wi) und dom(fri ) = ei1 × . . . × eini .
Dann gibt es s1, . . . , sn ∈ S ∪ CS mit e1 . . . en = wi und
g ∗(wi) = g ∗(e1 . . . en) = g ∗(seq(e1, . . . , en)) = seq Lang(X)(g ∗(e1), . . . , g ∗(en))
Word (G)
= g ∗(e1) · · · · · g ∗(en) = fri
(g ∗(ei1) · · · · · g ∗(eini )).
(1)
Beweis von (i).
(G)
solG(s) = L(G)s = fold Word
(TΣ(G),s)
s
S
(G)
= ki=1{fold Word
(fri (t)) | t ∈ TΣ(G),ei1×...×ein }
s
i
Sk
Word (G)
Word (G)
(fold ei1×...×ein (t)) | t ∈ TΣ(G),ei1×...×ein }
= i=1{fri
i
i
Sk
Sk
Word (G)
Word (G)
Word (G)
= i=1 fri
(fold ei1×...×ein (TΣ(G),ei1×...×ein )) = i=1 fri
(L(G)ei1×...×eini )
i
i
S
Word (G)
= ki=1 fri
(L(G)ei1 · · · · · L(G)eini )
381
Sk
Word (G)
∗
∗
(ei1) · · · · · solG
(solG
(eini ))
i=1 fri
(1) Sk
∗
∗
∗
(w1), . . . , solG
(wk ))
= i=1 solG
(wi) = parLang(X)(solG
=
∗
∗
= solG
(par(w1, . . . , wk )) = solG
(EG(s)).
∗
Also ist solG = solG
◦ EG und damit eine Lösung von EG in Lang(X).
Beweis von (ii). Zu zeigen: Für alle 1 ≤ i ≤ k,
(G)
(Sol ei1 · · · · · Sol eini ) ⊆ Sol s.
frWord
i
(2)
Nach Definition von Sol gilt Sol eij = g ∗(eij ) für alle 1 ≤ j ≤ ni.
Beweis von (2).
Word (G)
Word (G)
(g ∗(ei1) · · · · · g ∗(eini ))
(Sol ei1 · · · · · Sol eini ) = fri
S
(1) ∗
= g (wi) ⊆ ki=1 g ∗(wi) = parLang(X)(g ∗(w1), . . . , g ∗(wk )) = g ∗(par(w1, . . . , wk ))
fri
Def . EG
=
g ∗(EG(s)) = g(s) = Sol s.
Beweis von (iii). Nach Satz 3.2 (3) ist L(G) = fold Word (G)(TΣ(G)) die kleinste Σ(G)Unteralgebra von Word (G). Wegen (ii) gilt demnach L(G)s ⊆ Sol s für alle s ∈ S, also
solG(s) = L(G)s ⊆ Sol s = g(s).
o
382
18.4 Beispiele
1. Sei X = {a, b} und G = ({A, B}, ∅, X, {A → BA, A → a, B → b}). EG ist wie folgt
definiert:
EG(A) = par(seq(A, B), a),
EG(B) = b.
Die einzige Lösung g von EG in Lang(X) lautet:
g(A) = {b}∗ · {a},
g(B) = {b}.
2. Sei G = SAB (siehe Beispiel 4.5). EG ist wie folgt definiert:
EG(S) = par(seq(a, B), seq(b, A), eps)
EG(A) = par(seq(a, S), seq(b, A, A)),
EG(B) = par(seq(b, S), seq(a, B, B)).
Die einzige Lösung g von EG in Lang(X) lautet:
g(S) = {w ∈ {a, b}∗ | #a(w) = #b(w)},
g(A) = {w ∈ {a, b}∗ | #a(w) = #b(w) + 1},
g(B) = {w ∈ {a, b}∗ | #a(w) = #b(w) − 1}.
383
Andererseits ist g mit g(S) = g(A) = g(B) = {a, b}+ keine Lösung von EG in Lang(X),
weil a einerseits zu g(S) gehört, aber nicht zu
g ∗(EG(S)) = g ∗(par(seq(a, B), seq(b, A))) = ({a} · g(B)) ∪ ({b} · g(A)).
Beide Grammatiken sind nicht linksrekursiv. Deshalb haben ihre Gleichungssysteme nach
Satz 18.8 (s.u.) jeweils genau eine Lösung in Lang(X).
o
18.5 Von Erkennern regulärer Sprachen zu Erkennern kontextfreier Sprachen
Sei Σ = (S, I, F ) eine konstruktive Signatur und V eine S-sortige Menge.
ΣV =def (S, BS ∪ {Vs | s ∈ S}, BF, F ∪ {ins : Vs → s | s ∈ S}).
Lemma 18.6
σV : V → TΣV bezeichnet die Substitution mit σV (x) = insx für alle x ∈ Vs und s ∈ S.
Für alle ΣV -Algebren A gilt
(inA)∗ = fold A ◦ σV∗ : TΣ(V ) → A,
wobei inA = (inA
s : Vs → As )s∈S .
384
Beweis. Wir zeigen zunächst
fold A ◦ σV = inA.
(3)
Beweis von (3). Sei s ∈ S und x ∈ Xs. Da fold A : TΣV → A mit ins verträglich ist, gilt
fold A(σV (x)) = fold A(insx) = inA
s (x).
Aus (3) folgt (inA)∗ = (fold A ◦ σV )∗
Satz 3.7(1)
=
fold A ◦ σV∗ .
o
Sei G = (S, BS, R) eine nicht-linksrekursive CFG und reduce die in Beispiel 3.10 beschriebene Reduktionsfunktion für reguläre Ausdrücke.
Dann gibt es für alle s ∈ S ks, ns > 0, Bs,1, . . . , Bs,ns ∈ BS und Reg(BS)-Terme
ts,1, . . . , ts,ns über S mit
(reduce ◦ EG∗ )ks (s) = par(seq(Bs,1, ts,1), . . . , seq(Bs,ns , ts,ns ))
(4)
(reduce ◦ EG∗ )ks (s) = par(seq(Bs,1, ts,1), . . . , seq(Bs,ns , ts,ns ), eps).
(5)
oder
Seps bezeichne die Menge aller Sorten von S, die (5) erfüllen.
Sei Reg(BS)0 die Erweiterung von Reg(BS) um die Menge S der Sorten von G als weitere
Basismenge und den Konstruktor in =def inreg : S → reg als weiteres Operationssymbol.
385
Sei DΣ wie in Beispiel 17.4 definiert, ΨS = (Reg(BS)0, DΣ) und Σ = Reg(BS)0 ∪ DΣ.
Mit den Notationen von (4) und (5) erhalten wir das folgende rekursive ΨS -Gleichungssystem:
rec(EG) = {δ(in(s)) = λx.σS∗ (par(ite(x ∈ Bs,1, ts,1, mt), . . . ,
ite(x ∈ Bs,ns , ts,ns , mt))) | s ∈ S} ∪
{β(in(s)) = 1 | s ∈ Seps} ∪
{β(in(s)) = 0 | s ∈ S \ Seps}.
Satz 18.7
S
Sei X = BS, g : S → Lang(X) eine Lösung von EG in Lang(X) und Ag die Σ-Algebra
A
mit Ag |Reg(BS) = Lang(X), Ag |Acc(X) = P(X) und ins g = gs für alle s ∈ S.
Ag erfüllt das rekursive ΨS -Gleichungssystem rec(EG).
Beweis.
Zu zeigen ist, dass für alle t = t0 ∈ rec(EG) und h : V → Ag h∗(t) = h∗(t0) gilt.
Sei h : V → Ag . Für alle s ∈ S,
h∗(in(s)) = inAg (s) = g(s) = g ∗(s)
Lemma 18.1
=
g ∗((reduce ◦ EG∗ )ks (s))
(6)
Lemma 18.6 liefert
g ∗ = (inAg )∗ = fold Ag ◦ σS∗ : TReg(BS)(S) → A.
(7)
386
Gehört s nicht zu Seps, dann gilt
g ∗((reduce ◦ EG∗ )ks (s)) = g ∗(par(seq(Bs,1, ts,1), . . . , seq(Bs,ns , ts,ns )))
Ag
Ag
= parAg (seq Ag (Bs,1 , g ∗(ts,1)), . . . , seq Ag (Bs,ns , g ∗(ts,ns )))
S s
= ni=1
(Bs,i · g ∗(ts,i)),
(8)
also
(6)
h∗(δ(in(s))) = δ Ag (h∗(in(s))) = δ Ag (g ∗((reduce ◦ EG∗ )ks (s)))
S
(8) A Sns
= δ g ( i=1(Bs,i · g ∗(ts,i))) = λx.δ Ag ( ni=1(Bs,i · g ∗(ts,i)))(x)
S
Def . δ Ag
= λx.{w ∈ X ∗ | xw ∈ ni=1(Bs,i · g ∗(ts,i))}
= λx.{w ∈ X ∗ | x ∈ Bs,i, w ∈ g ∗(ts,i), 1 ≤ i ≤ n}
S s
= λx. ni=1
{w ∈ X ∗ | x ∈ Bs,i, w ∈ g ∗(ts,i)}
S ns
= λx. i=1{w ∈ X ∗ | if x ∈ Bs,i then w ∈ g ∗(ts,i) else w ∈ ∅}
S s
= λx. ni=1
(if x ∈ Bs,i then g ∗(ts,i) else ∅)
S s
= λx. ni=1
(if x ∈ Bs,i then g ∗(ts,i) else mtA)
S s
= λx. ni=1
(if x ∈ Bs,i then g ∗(ts,i) else g ∗(mt))
S s ∗
= λx. ni=1
g (ite(x ∈ Bs,i, ts,i, mt))
= λx.g ∗(par(ite(x ∈ Bs,1, ts,1, mt), . . . , ite(x ∈ Bs,ns , ts,ns , mt)))
= g ∗(λx.par(ite(x ∈ Bs,1, ts,1, mt), . . . , ite(x ∈ Bs,ns , ts,ns , mt)))
387
(7)
= fold Ag (σS∗ (λx.par(ite(x ∈ Bs,1, ts,1, mt), . . . , ite(x ∈ Bs,ns , ts,ns , mt))))
= h∗(σS∗ (λx.par(ite(x ∈ Bs,1, ts,1, mt), . . . , ite(x ∈ Bs,ns , ts,ns , mt))))
und
(6)
h∗(β(in(s))) = β Ag (h∗(in(s))) = β Ag (g ∗((reduce ◦ EG∗ )ks (s)))
(8) A Sns
Def . β Ag
= β g ( i=1(Bs,i · g ∗(ts,i))) = 0 = h∗(0).
Gehört s zu Seps, dann gilt
g ∗((reduce ◦ EG∗ )ks (s)) = g ∗(par(seq(Bs,1, ts,1), . . . , seq(Bs,ns , ts,ns ), eps))
Ag
Ag
= parAg (seq Ag (Bs,1 , g ∗(ts,1)), . . . , seq Ag (Bs,ns , g ∗(ts,ns ), epsAg ))
S s
= ni=1
(Bs,i · g ∗(ts,i)) ∪ {},
(9)
also
(6)
h∗(δ(in(s))) = δ Ag (h∗(in(s))) = δ Ag (g ∗((reduce ◦ EG∗ )ks (s)))
S s
(9) A Sns
= δ g ( i=1(Bs,i · g ∗(ts,i)) ∪ {}) = λx.δ Ag ( ni=1
(Bs,i · g ∗(ts,i)) ∪ {})(x)
S s
Def . δ Ag
= λx.{w ∈ X ∗ | xw ∈ ni=1
(Bs,i · g ∗(ts,i)) ∪ {}}
S s
= λx.{w ∈ X ∗ | xw ∈ ni=1
(Bs,i · g ∗(ts,i))}
wie oben
=
h∗(σS∗ (λx.par(ite(x ∈ Bs,1, ts,1, mt), . . . , ite(x ∈ Bs,ns , ts,ns , mt))))
388
und
(6)
h∗(β(in(s))) = β Ag A(h∗(in(s))) = β Ag (g ∗((reduce ◦ EG∗ )ks (s)))
(9) A Sns
Def . β Ag
= β g ( i=1(Bs,i · g ∗(ts,i)) ∪ {}) = 1 = h∗(1).
o
Satz 18.8 AsolG ist die einzige coinduktive Lösung von E = BRE ∪ rec(EG) in P(X).
Proof. Nach Satz 18.3 (i) ist solG eine Lösung von EG in Lang(X). Also wird rec(EG)
nach Satz 18.7 von AsolG erfüllt. Nach Beispiel 17.10 wird auch BRE von AsolG erfüllt.
Folglich ist AsolG nach Satz 17.7 die einzige Lösung von E in P(X).
o
∗
∗
Da χ : P(X ∗) → 2X (siehe 2.7) und die Bijektion ξ : 2X → DTAcc(X) von Beispiel 2.20
Σ-homomorph sind, wird E auch von den Bildalgebren χ(AsolG ) und ξ(χ(AsolG )) erfüllt.
Folglich ist χ(AsolG ) bzw. ξ(χ(AsolG )) nach Satz 17.7 die einzige coinduktive Lösung von E
in χ(P(X)) = Beh(X, 2) bzw. ξ(χ(P(X))) = ξ(Beh(X, 2)) = DTAcc(X).
Satz 18.9 solG ist die einzige Lösung von EG in Lang(X).
Beweis. Sei sol eine weitere Lösung von EG in Lang(X). Dann sind nach Satz 18.7 sind
AsolG und Asol coinduktive Lösungen von E in P(X). Nach Satz 18.8 stimmen sie miteinander überein. Also sind auch solG und sol gleich.
o
389
rec(EG) legt folgende Erweiterung des Brzozowski-Automaten Bro(BS) zur Reg(BS)0Algebra Bro(BS)0 nahe:
Für alle s ∈ S,
0
δ Bro(BS) (in(s)) = λx.σS∗ (par(ite(x ∈ Bs,1, ts,1, mt), . . . , ite(x ∈ Bs,ns , ts,ns , mt))),
0
β Bro(BS) (in(s)) = if s ∈ Seps then 1 else 0.
Sei Lang(X)0 = AsolG |Reg(BS)0 , regB 0 = χ(AsolG )|Reg(BS)0 , regC 0 = ξ(χ(AsolG ))|Reg(BS)0 und
Σ = Reg(BS)0 ∪ DΣ.
Bro(BS)0 stimmt mit der zur Σ-Algebra erweiterten Termalgebra TReg(BS)0 überein (siehe
Satz 17.7). Demnach gelten die Gleichungen
0
0
fold Lang(X) = unfold Bro(BS) : Bro(BS)0 → P(X),
0
0
fold regB = unfoldB Bro(BS) : Bro(BS)0 → Beh(X, 2),
0
0
fold regC = unfoldC Bro(BS) : Bro(BS)0 → DTAcc(X),
(10)
(11)
(12)
was die Acc(X)-Homomorphie der drei Faltungen impliziert.
Bro(BS)0, regB 0 und regC 0 sind in Compiler.hs durch accT rules, regB rules bzw.
regC rules implementiert. Hierbei ist rules eine Liste der Regeln von G. Alle drei
Algebren-Implementierungen verwenden die Funktion eqs rules, die σS∗ ◦ EG berechnet.
390
0
0
Sei s ∈ S. Die obigen Definitionen von δ Bro(BS) (in(s)) bzw. β Bro(BS) (in(s)) sind in der
Implementierung von accT rules von Bro(BS)0 durch die Gleichungen
0
0
δ Bro(BS) (in(s)) = δ Bro(BS) (σS∗ (EG(s))),
β
Bro(BS)0
(in(s)) = β
Bro(BS)0
(13)
(σS∗ (EG(s)))
realisiert. Wegen der Nicht-Linksrekursivität von G garantiert Haskells lazy evaluation,
0
0
dass für jeden Reg(BS)0-Grundterm t die Reduktionen von δ Bro(BS) (t) und β Bro(BS) (t)
mit Hilfe von BRE und (13) terminieren und die gewünschten Ergebnisse liefern.
Wie lässt sich die zugrundeliegende Eindeutigkeit der Lösung von (13) in den “Variablen”
0
0
δ Bro(BS) ◦ in und β Bro(BS) ◦ in nachweisen?
0
0
Aus (10), der Eindeutigkeit von unfoldB Bro(BS) und unfoldC Bro(BS) und der Acc(X)Homomorphie von χ und ξ folgt
0
0
0
unfoldB Bro(BS) = χ ◦ unfold Bro(BS) = χ ◦ fold Lang(X) ,
also auch
0
0
0
unfoldC Bro(BS) = ξ ◦ χ ◦ unfold Bro(BS) = ξ ◦ χ ◦ fold Lang(X) .
391
Also erkennt für alle Reg(BS)0-Grundterme t der initiale Automat (Bro(BS)0, t) die Spra0
che fold Lang(X) (t) von t (im Sinne von Abschnitt 2.11) und realisiert den Acc(X)-Coterm
0
ξ(χ(fold Lang(X) (t))) (im Sinne von Kapitel 20). Für Reg(BS)-Grundterme t haben wir das
bereits in Beispiel 17.10 bzw. Abschnitt 17.11 gezeigt.
Zusätzlich erhalten wir für alle s ∈ S:
0
(10)
0
0
unfold Bro(BS) (in(s)) = fold Lang(X) (in(s)) = inLang(X) (s) = solG(s) = L(G)s.
(14)
Also erkennt der initiale Automat (Bro(BS)0, in(s)) die Sprache von G, realisiert also deren
charakteristische Funktion:
0
(14)
0
unfoldB Bro(BS) (in(s)) = χ(unfold Bro(BS) (in(s))) = χ(L(G)s),
(15)
und den entsprechenden Acc(X)-Coterm:
0
(14)
0
unfoldC Bro(BS) (in(s)) = ξ(χ(unfold Bro(BS) (in(s)))) = ξ(χ(L(G)s)).
(16)
Daraus folgt
0
0
(11)
0
(15)
0
0
(12)
0
(16)
inregB (s) = fold regB (in(s)) = unfoldB Bro(BS) (in(s)) = χ(L(G)s),
inregC (s) = fold regC (in(s)) = unfoldC Bro(BS) (in(s)) = ξ(χ(L(G)s)).
(17)
(18)
392
0
0
Die durch (17) und (18) gegebenen Definitionen von inregB bzw. inregC sind in den Implementierungen regB rules von regB 0 und regC rules von regC 0 durch die Gleichungen
0
0
0
0
inregB = fold regB ◦ σS∗ ◦ EG,
inregC = fold regC ◦ σS∗ ◦ EG
(19)
realisiert.
Wieder garantiert Haskells lazy evaluation wegen der Nicht-Linksrekursivität von G, dass
0
0
für jeden Reg(BS)0-Grundterm t die Reduktionen von fold regB (t) und fold regC (t) mit
0
0
Hilfe der induktiven Definition von fold regB bzw. fold regC und (19) terminieren und die
gewünschten Ergebnisse liefern.
Wie lässt sich die zugrundeliegende Eindeutigkeit der Lösung von (19) in den “Variablen”
0
0
inregB und inregC nachweisen?
Sei t ∈ TReg(BS)0 und v = fold Regword (BS)(t). Im Haskell-Modul Compiler.hs entspricht der
0
0
0
Erkenner fold regB (t), unfoldB DTAcc(X) (fold regC (t)) bzw. unfoldB Bro(BS) (t) dem Aufruf
regToAlg file v n
mit n = 1, n = 2 bzw. n = 3, wobei die Datei file die Regeln von G enthält. Der Aufruf
startet eine Schleife, die nach der Eingabe von Wörtern w fragt, auf die er angewendet
werden soll, um zu prüfen, ob w zu L(G)t gehört oder nicht.
393
19 Interpretation in stetigen Algebren
Eine Menge A ist halbgeordnet, wenn es eine Halbordnung, also eine reflexive, transitive und antisymmetrische binäre Relation auf A gibt. Eine Teilmenge {ai | i ∈ N} von
A heißt ω-Kette, wenn für alle i ∈ N ai ≤ ai+1 gilt.
Eine halbgeordnete Menge A heißt ω-CPO (ω-complete partially ordered set), wenn A
ein bzgl. ≤ kleinstes Element ⊥ und Suprema ti∈Nai aller ω-Ketten {ai | i ∈ N} von A
enthält.
Für jede Menge A liefert die flache Erweiterung, A⊥ =def A + {⊥}, einen ω-CPO: Die
zugrundeliegende Halbordnung ≤ ist
{(a, b) ∈ A2 | a = ⊥ ∨ a = b}.
Der ω-CPO der partiellen Funktionen von A nach B
Für alle f, g : A (→ B,
f ≤ g ⇔def
def (f ) ⊆ def (g) ∧ ∀ a ∈ def (f ) : f (a) = g(a).
Die nirgends definierte Funktion Ω : A (→ B mit def (Ω) =def ∅ ist das kleinste
Element von A (→ B bzgl. ≤.
394
Jede ω-Kette f0 ≤ f1 ≤ f2 ≤ . . . von A (→ B hat das folgende Supremum: Für alle
a ∈ A,
(
fi(a)
falls ∃ i ∈ N : a ∈ def (fi),
(ti∈Nfi)(a) =def
undefiniert sonst.
Ein Produkt A1 ×· · ·×An von n ω-CPOs wie auch die Menge AB aller Funktionen von einer
Menge B in einen ω-CPO A bilden selbst ω-CPOs: Die Halbordnungen auf A1, . . . , An bzw.
A werden – wie im Abschnitt Kongruenzen und Quotienten beschrieben – zur Halbordnung
auf A1 × · · · × An bzw. AB geliftet.
Das kleinste Element von A1 × · · · × An ist das n-Tupel der kleinsten Elemente von
A1, . . . , An. λx.⊥ ist das kleinste Element von AB , wobei ⊥ das kleinste Element von
B ist.
Suprema sind komponenten- bzw. argumentweise definiert: Für alle ω-Ketten
a0 = (a0,1, . . . , a0,n) ≤ a1 = (a1,1, . . . , a1,n) ≤ a2 ≤ . . . von A1 × · · · × An,
ti∈N ai =def (ti∈Nai,0, . . . , ti∈Nai,n).
(2)
Für alle ω-Ketten f0 ≤ f1 ≤ f2 ≤ . . . von B A und a ∈ A,
(ti∈Nfi)(a) =def ti∈Nfi(a).
(3)
395
Seien A, B halbgeordnete Mengen. Eine Funktion f : A → B ist monoton, wenn für alle
a, b ∈ A gilt:
a ≤ b ⇒ f (a) ≤ f (b).
f ist strikt, wenn f das kleinste Element ⊥A von A auf das kleinste Element ⊥B von B
abbildet. Ist A ein Produkt, dann bildet eine strikte Funktion f nicht nur ⊥A, sondern jedes
Tupel von A, das ein kleinstes Element enthält, auf ⊥B ab.
Seien A, B ω-CPOs. f ist ω-stetig (ω-continuous), wenn f (ti∈Nai) = ti∈Nf (ai) für alle
ω-Ketten {ai | i ∈ N} von A gilt.
ω-stetige Funktionen sind monoton.
Sei A →c B die Menge aller ω-stetigen Funktionen von A nach B. A →c B ist ein ω-CPO,
weil Ω und die Suprema ω-Ketten ω-stetiger Funktionen (siehe (2)) selbst ω-stetig sind.
Sei Σ = (S, I, F ) eine konstruktive Signatur und A eine Σ-Algebra.
A ist monoton, wenn es für alle s ∈ S eine Halbordnung ≤s,A ⊆ A2s und ein bzgl. ≤s,A
kleinstes Element ⊥s,A gibt und für alle f ∈ F f A monoton ist.
A ist ω-stetig, wenn A monoton ist, für alle s ∈ S As ein ω-CPO ist und für alle f ∈ F
f A ω-stetig ist.
396
Fixpunktsatz von Kleene
Sei A ein ω-CPO und f : A → A ω-stetig. Da f monoton ist, erhalten wir die ω-Kette
⊥ ≤ f (⊥) ≤ f 2(⊥) ≤ . . . .
Deren Supremum
lfp(f ) =def tn∈N f n(⊥)
ist der (bzgl. ≤) kleinste Fixpunkt von f (siehe [32]).
o
Anwendung auf iterative Gleichungssysteme
Sei E : V → TΣ(V ) ein iteratives Σ-Gleichungssystem (siehe Kapitel 18) und A eine ωstetige Σ-Algebra. Dann können die Halbordnungen, kleinsten Elemente und Suprema von
A, wie in Kapitel 2 beschrieben, nach AV geliftet werden, d.h. AV ist ein ω-CPO.
Die Funktion EA : AV → AV mit
EA(g) = g ∗ ◦ E
für alle g ∈ AV nennen wir Schrittfunktion von E. Offenbar wird E von g genau dann
in A gelöst, wenn g ein Fixpunkt von EA ist.
397
Nach [5], Proposition 4.13, ist EA ω-stetig. Also folgt aus dem Fixpunktsatz von Kleene,
dass
(1)
lfp(EA) = tn∈N EAn (λx.⊥A)
der kleinste Fixpunkt von EA in A ist.
Für alle strikten und ω-stetigen Σ-Homomorphismen h : A → B gilt:
h ◦ lfp(EA) = lfp(EB (h)).
(2)
Beweis. Für alle g ∈ AV ,
h ◦ EA(g)
Def . EA
=
3.7(1)
h ◦ g ∗ ◦ E = (h ◦ g)∗ ◦ E = EB (h ◦ g).
(3)
Wir nehmen an, dass für alle n ∈ N Folgendes gilt:
h ◦ EAn (λx.⊥A) = EB (h)n(λx.⊥B ).
(4)
Aus (4) folgt (2):
h ◦ lfp(EA) = h ◦ tn∈NEAn (λx.⊥A)
h ω−stetig
=
tn∈N(h ◦ EAn (λx.⊥A))
(4)
= tn∈NEB (h)n(λx.⊥B ) = lfp(EB (h)).
398
Zu zeigen bleibt (4). Sei n > 0.
h ◦ EA0 (λx.⊥A) = h ◦ λx.⊥A
h strikt
=
λx.⊥B = EB (h)0(λx.⊥B ),
(3)
h ◦ EAn (λx.⊥A) = h ◦ EA(EAn−1(λx.⊥A)) = EB (h)(h ◦ EAn−1(λx.⊥A))
ind. hyp.
=
EB (h)(EB (h)n−1(λx.⊥B )) = EB (h)n(λx.⊥B ).
o
Iterative Gleichungssysteme dienen u.a. der Spezifikation partieller Funktionen. Sind z.B.
zwei partielle Funktionen f, g gegeben, dann ist die Gleichung h = g ◦ h ◦ f zunächst nur
eine Bedingung an eine dritte Funktion h. Die aus der Gleichung gebildete Schrittfunktion
λh.(g ◦ h ◦ f ) : (A (→ B) → (A (→ B)
ist ω-stetig und hat deshalb nach dem Fixpunktsatz von Kleene einen kleinsten Fixpunkt,
der als denotationelle Semantik von h bezeichnet wird.
19.1 Schleifensemantik
Sei Σ die abstrakte Syntax von JavaLight (siehe Beispiel 4.6) zusammen mit zwei Konstanten e : 1 → Disjunct und c : 1 → Command . Sei Sem die Σ(JavaLight)-Algebra
javaState (siehe 16.5) zusammen mit Interpretationen f ∈ javaState Disjunct von e und
g ∈ javaState Command von c.
399
Sem ist ω-stetig.
Sei V = {x : Command }, E das iterative Σ-Gleichungssystem
E : V → TΣ(V )
x 7→ cond(e, block(seq(c, x)), id)
und ESem die Schrittfunktion von E (s.o.). Der kleinste Fixpunkt von ESem liefert uns die
Interpretation von loop(e, c) in Sem:
loopSem (f, g) =def lfp(ESem )(x) : Store (→ Store.
Die Interpretation der anderen Operationen von JavaLight in Sem ergibt sich direkt aus
ihrer Haskell-Implementierung in Abschnitt 11.5. Die dortige Haskell-Funktion
loop :: St Bool -> St Store -> St Store
loop f g = cond f (loop f g . g) id
implementiert loopSem . Für alle st ∈ Store terminiert loop(f )(g)(st) genau dann, wenn
loopSem (f, g)(st) definiert ist.
o
400
19.2 Semantik der Assemblersprache StackCom ∗
Auch jede Befehlsfolge cs ∈ StackCom ∗ lässt sich als iteratives Gleichungssystem eqs(cs)
darstellen. Im Gegensatz zum vorigen Abschnitt liegt hier wie in Abschnitt 18.2 der einfache
Fall I = ∅ vor.
Die konstruktive Signatur Σ(StackCom), aus deren Operationssymbolen die Terme von
eqs(cs) gebildet sind, lautet wie folgt:
Σ(StackCom) = ( {com}, {Z, String}, F ),
F
= { push : Z → com,
load, save, cmp : String → com,
pop, add, sub, mul, div, or, and, inv, nix : 1 → com,
cons, fork : com × com → com }.
push, load, save, cmp, pop, add, sub, mul, div, or, and, inv entsprechen den Konstruktoren von StackCom. nix, cons, fork ersetzen die Sprungbefehle von StackCom (s.u.).
Sei Eqs die Menge der iterativen Σ(Stackcom)-Gleichungssysteme mit Variablen aus N.
Die folgendermaßen definierte Funktion eqs : StackCom ∗ → Eqs übersetzt Assemblerprogramme in solche Gleichungssysteme:
401
Sei cs ∈ StackCom ∗ und V (cs) die Menge der Nummern der Sprungziele von cs, also
V (cs) = {0} ∪ {n < |cs| | Jump(n) ∈ cs ∨ JumpF (n) ∈ cs}.
eqs(cs) : V (cs) → TΣ(StackCom)(V (cs))
n 7→ mkTerm(drop(n)(cs))
mkTerm : StackCom ∗ → TΣ(StackCom)(V (cs))
7→ nix


n






 nix
c : cs0 7→
fork (n, mkTerm(cs0))




fork (nix, mkTerm(cs0))



 cons(c, mkTerm(cs0))
falls c = Jump(n) ∧ n < |cs|
falls c = Jump(n) ∧ n ≥ |cs|
falls c = JumpF (n) ∧ n < |cs|
falls c = JumpF (n) ∧ n ≥ |cs|
sonst
402
Beispiel Das Assemblerprogramm von Abschnitt 12.5 zur Berechnung der Fakultätsfunktion lautet als iteratives Σ(Stackcom)-Gleichungssystem wie folgt:
E : {0, 3} → TΣ(StackCom)({0, 3})
0 7→ cons(push(1), cons(save(fact), cons(pop, 3)))
3 7→ cons(load(x), cons(push(1), cons(cmp(>), fork (nix,
cons(load(fact), cons(load(x), cons(mul, cons(save(fact),
cons(pop, cons(load(x), cons(push(1), cons(sub, cons(save(x),
cons(pop, 3))))))))))))))
In Abschnitt 16.5 haben wir informell gezeigt, dass compJava(javaStack ) ein Compiler
für JavaLight bzgl. javaState ist, was laut Kapitel 5 impliziert, dass folgendes Diagramm
kommutiert:
TΣ(JavaLight)
fold javaState
g
javaState
fold javaStack
javaStack
(1)
encode
evaluate
g
Mach = State (→ State
403
Hier sind
• javaStack die in Abschnitt 12.5 definierte, zur JavaLight-Algebra erweiterte Assemblersprache StackCom ∗,
• javaState das in Abschnitt 11.5 und 19.1 definierte Zustandsmodell von JavaLight,
• Mach der ω-CPO der partiellen Funktionen auf der Zustandsmenge
State = Z∗ × ZString ,
deren Paare aus jeweils einem Kellerinhalt und einer Speicherbelegung bestehen (siehe
12.5),
• encode und evaluate wie in Abschnitt 16.5 definiert.
Mach : State (→ State wird zunächst zu einer ω-stetigen Σ(StackCom)-Algebra erweitert:
Für alle f, g : State (→ State, ⊗ ∈ {add, sub, mul, div, or, and},
(stack, store) ∈ State, a ∈ Z und x ∈ String,
pushMach (i)(stack, store) = (a : stack, store),
loadMach (x)(stack, store) = (store(x) : stack, store),
saveMach (x)(stack, store) = (stack, update(store)(x)(a)),
404
cmpMach (x)(stack, store) =


(1 : s, store) falls ∃ a, b, s : stack = a : b : s





∧ evalRel(x)(a)(b),


(0 : s, store) falls ∃ a, b, s : stack = a : b : s




∧ ¬evalRel(x)(a)(b),



 undefiniert sonst,
(
(s, store) falls ∃ a, s : stack = a : s,
popMach (stack, store) =
undefiniert sonst,
(
(op(⊗)(b, a) : s, store) falls ∃ a, b, s : stack = a : b : s,
⊗Mach (stack, store) =
undefiniert
sonst,
(
((a + 1) mod 2 : stack, store) falls ∃ a, s : stack = a : s,
inv Mach (stack, store) =
undefiniert
sonst,
nixMach (stack, store) = (stack, store),
consMach (f, g) = g ◦ f,
fork Mach (f, g)(stack, store) =



 f (s, store) falls ∃ s : stack = 0 : s,
g(s, store) falls ∃ a, s : stack = a : s ∧ a 6= 0,


 undefiniert sonst,
405
wobei op(add) = (+), op(sub) = (−), op(mul) = op(and) = (∗), op(div) = (/) und
op(or) = sign ◦ (+).
Sei cs ∈ StackCom ∗. Nach dem Fixpunktsatz von Kleene hat das iterative Σ(StackCom)Gleichungssystem eqs(cs) (s.o.) eine kleinste Lösung in Mach. Sie liefert uns die Funktion
execute von Abschnitt 12.5: Für alle cs ∈ StackCom ∗ und n ∈ V (cs),
execute(cs) = lfp(eqs(cs)Mach ) : V (cs) → Mach.
(4)
Tatsächlich entspricht execute0 der Haskell-Funktion execute in Abschnitt 12.5.
Im Folgenden wird Mach so zu einer JavaLight-Algebra erweitert, dass execute und encode
JavaLight-homomorph werden und aus der Initialität von TΣ(JavaLight) die Kommutativität
von (1) folgt (siehe Kapitel 5).
Für alle Sorten s von JavaLight, Mach s = State (→ State.
Für alle op ∈ {seq, sum, prod, disjunct, conjunct} und f, g : State (→ State,
opMach (f, g) = g ◦ f.
Für alle op ∈ {embed, block, encloseS, embedC, embedL, encloseD},
opMach = idState(→State .
nilS Mach = nilP Mach = idState .
406
Für alle x ∈ String und f : State (→ State,
assignMach (x, f ) = popMach ◦ saveMach (x) ◦ f.
Für alle f, g, h : State (→ State, condMach (f, g, h) = fork Mach (h, g) ◦ f .
Für alle f, g : State (→ State, cond1Mach (f, g) = fork Mach (idMach , g) ◦ f .
Für alle f, g : State (→ State, loopMach (f, g) = lfp(EMach )(x), wobei
E : {x} → TΣ(StackCom)({x, f, g})
x 7→ cons(f, fork (nix, cons(g, x)))
Für alle f, g : State (→ State,
sumsectMach (+, f, g) = g ◦ addMach ◦ f,
sumsectMach (−, f, g) = g ◦ subMach ◦ f,
prodsectMach (∗, f, g) = g ◦ mulMach ◦ f,
prodsectMach (/, f, g) = g ◦ div Mach ◦ f.
embedI Mach = pushMach .
varMach = loadMach .
Für alle f : State (→ State, notMach (f ) = inv Mach ◦ f .
407
Für alle x ∈ String und f, g : State (→ State,
atomMach (f, x, g) = cmpMach (x) ◦ g ◦ f.
Für alle b ∈ 2, embedB Mach (b) = pushMach (b).
Erweiterte Σ-Bäume
Eine monotone bzw. ω-stetige Σ-Algebra T ist initial in der Klasse PAlg Σ bzw. CAlg Σ
aller monotonen bzw. ω-stetigen Σ-Algebren, wenn es zu jeder monotonen bzw. ω-stetigen
Σ-Algebra A genau einen strikten (!) und monotonen bzw. ω-stetigen Σ-Homomorphismus
A
fold A
fin : T → A bzw. fold ω : T → A gibt.
Alle initialen monotonen bzw. ω-stetigen Σ-Algebren sind durch strikte und monotone bzw.
ω-stetige Σ-Isomorphismen miteinander verbunden.
Sei Σ⊥ = (S, BS, BF, F ∪ {⊥s : 1 → s | s ∈ S}) und ≤ die kleinste reflexive, transitive
und Σ-kongruente S-sortige Relation auf CTΣ⊥ derart, dass für alle s ∈ S and t ∈ CTΣ⊥,s,
⊥s ≤ t, wobei ⊥s hier den Baum bezeichnet, der aus einem mit ⊥s markierten Blatt besteht. Er bildet offenbar das bzgl. ≤ kleinste Element.
F TΣ⊥ ist eine monotone und CTΣ⊥ eine ω-stetige Σ-Algebra (siehe [32]).
o
408
Satz 19.3 F TΣ⊥ ist initial in PAlg Σ. CTΣ⊥ ist initial in CAlg Σ.
Beweisskizze. Sei A eine monotone und B eine ω-stetige Σ-Algebra. Zwei S-sortige FunkB
tionen fold A
fin : F TΣ⊥ → A und fold ω : F TΣ⊥ → B erhält man wie folgt:
Für alle s ∈ S und t ∈ F TΣ⊥,s und u ∈ CTΣ⊥,s,
(
⊥s,A
falls t = ⊥s,
fold A
(t)
=
def
fin
A
f A(fold A
fin (λw.t(iw)), . . . , fold fin (λw.t(iw))) sonst,
B
fold B
ω (u) =def tn∈N fold fin (u|n ),
wobei für alle n ∈ N und w ∈ N∗>0,
(
u|n(w) =def
u(w)
falls |w| ≤ n,
undefiniert sonst.
B
Zum Nachweis der geforderten Eigenschaften von fold A
fin und fold ω , siehe [5, 32].
o
Die Faltung fold A
ω (u) eines Σ-Baums u in A ist also das Supremum von Faltungen der
endlichen Präfixe u|0, u|1, u|2, . . . von u.
409
Satz 19.4 Jedes iterative Σ-Gleichungssystem E : V → TΣ(V ) hat genau eine Lösung in
CTΣ.
Beweis. Sei g : V → CTΣ⊥ eine Lösung von E in CTΣ⊥ . Da lfp(ECTΣ ) die kleinste
⊥
Lösung von E in CTΣ⊥ ist, gilt:
lfp(ECTΣ ) ≤ g.
(5)
⊥
Durch Induktion über n lässt sich zeigen, dass für alle x ∈ V und n ∈ N gilt:
n+1
def (g(x)) ∩ Nn ⊆ def (ECT
(λx.⊥)(x))
Σ
(6)
⊥
(siehe [25], Satz 17).
Aus (6) folgt für alle x ∈ V :
def (g(x)) ⊆ def
n
(tn∈NECT
Σ
⊥
(λx.⊥)(x))
Def . lfp(ECT )
Σ
=
⊥
def (lfp(ECTΣ )(x)),
also g(x) = lfp(ECTΣ )(x) ∈ CTΣ wegen (5) und nach Definition von ≤.
⊥
⊥
o
Ein Σ-Baum heißt rational, wenn er nur endlich viele Unterbäume hat. Jeder Pfad eines
rationalen Baums ist endlich oder periodisch. Rationale Bäume können als endliche Graphen dargestellt werden, deren Zyklen aus den Pfad-Perioden entstehen. Solche Graphen
entsprechen iterativen Gleichungssystemen (siehe Kapitel 18). Ein Σ-Baum ist also genau
dann rational, wenn er zum Bild der Lösung eines iterativen Σ-Gleichungssystems in CTΣ⊥
oder CTΣ gehört.
410
Beispiel loop
Sei Σ = Σ(JavaLight). Die eindeutige Lösung sol : {x} → CTΣ({b, c}) des iterativen
Σ-Gleichungssystems
E : {x} → TΣ({x, b, c})
von Abschnitt 19.1 hat folgende Graphdarstellung: Für alle w ∈ N∗,


cond





b



 block
sol(x)(w) =

id





seq




c
falls w ∈ (212)∗
falls w ∈ (212)∗1
falls w ∈ (212)∗2
falls w ∈ (212)∗3
falls w ∈ (212)∗21
falls w ∈ (212)∗211
411
Beispiel StackCom
Sei Σ = Σ(StackCom) und cs das Assemblerprogramm von Beispiel 12.6. Die eindeutige
Lösung sol : {0, 3} → CTΣ des iterativen Σ-Gleichungssystems
eqs(cs) : {0, 3} → TΣ({0, 3})
von Abschnitt 19.2 hat folgende Graphdarstellung:
412
sol(3) =
sol(0) =
413
Offenbar repräsentiert jeder Pfad des Σ-Baums sol(0) eine – möglicherweise unendliche –
Kommandofolge, die einer Ausführungssequenz von cs entspricht.
o
Beispiel
Sei Σ = Σ(StackCom). Die eindeutige Lösung sol : {x} → CTΣ({b, c}) des iterativen
Σ-Gleichungssystems
E : {x} → TΣ({x, b, c}),
dessen kleinste Lösung in Mach den Konstruktor loop von Σ(StackCom) in Mach interpretiert (siehe 19.2), hat folgende Graphdarstellung:
sol(x) =
414
Sei cs ∈ StackCom ∗. Da fold Mach
strikt, ω-stetig und Σ-homomorph ist, folgt
ω
execute0(cs) = fold Mach
◦ lfp(eqs(cs)CTΣ ) : V (cs) → Mach
ω
(7)
aus (2) und (4). Die Ausführung von cs im Zustand state ∈ State liefert also dasselbe –
möglicherweise undefinierte – Ergebnis wie die Faltung des Σ-Baums lfp(eqs(cs)CTΣ )(state)
in Mach.
o
Cosignaturen und ihre Algebren
Die eindeutige Lösung eines iterativen Σ-Gleichungssystems E in CTΣ lässt sich nicht nur als
kleinsten Fixpunkt von ECTΣ darstellen, sondern auch als Entfaltung einer Coalgebra. Dies
folgt aus der Tatsache, dass CTΣ eine finale coΣ-Algebra ist, wobei coΣ die folgendermaßen
aus (der konstruktiven Signatur) Σ = (S, I, F ) gebildete destruktive Signatur ist:
`
coΣ = (S, I, {ds : s → f :e→s∈F e | s ∈ S}).
`
Hier bezeichnet f :e→s∈F e das Coprodukt der Domains e aller Konstrukturen von F
mit Range s. Das bedeutet, dass ds in einer coΣ-Algebra A als Funktion von As in die
disjunkte Vereinigung
]
Ae = {(a, f ) | a ∈ Ae, f : e → s ∈ F }
f :e→s∈F
interpretiert wird
415
Z.B. lautet die Interpretation von ds in CTΣ wie folgt: Für alle s ∈ S und t ∈ CTΣ,s mit
n-stelligem Operationssymbol t(),
Σ (t) =
dCT
def ((λw.t(1w), . . . , λw.t(nw)), t()).
s
CTΣ ist also nicht nur eine Σ-Algebra, sondern auch eine coΣ-Algebra. Mehr noch:
Satz 19.5 CTΣ ist eine finale coΣ-Algebra.
Beweis. Sei A eine coΣ-Algebra. Die S-sortige Funktion unfold A : A → CTΣ ist wie folgt
definiert:
Sei s ∈ S, a ∈ As, i > 0, w ∈ N∗ und dA
s (a) = ((a1 , . . . , an ), f ).
unfold A(a)() = f,
(
unfold A(ai)(w) falls 1 ≤ i ≤ n,
A
unfold (a)(iw) =
undefiniert
sonst.
Der Nachweis der geforderten Eigenschaften von unfold A wird in [32] geführt.
o
416
Sei E : V → TΣ(V ) ein iteratives Σ-Gleichungssystem. E macht TΣ(V ) zur coΣ-Algebra
T E : Für alle s ∈ S, f : e → s ∈ F , t ∈ TΣ(V )e und x ∈ Vs,
TsE =def TΣ(V )s,
E
dTs (f t) =def (t, f ),
E
E
dTs (x) =def dTs (E(x)).
incV
(8) V → T
T
E unfold
→
E
CTΣ löst E in CTΣ (siehe [32]).
(9) g : V → CTΣ löst E genau dann in CTΣ, wenn g ∗ : T E → CTΣ coΣ-homomorph ist
(siehe [32]).
417
Satz 19.6 (coalgebraische Version von Satz 19.4)
Jedes iterative Σ-Gleichungssystem E : V → TΣ(V ) hat genau eine Lösung in CTΣ.
Beweis. Wegen (8) hat E eine Lösung in CTΣ. Angenommen, g, h : V → CTΣ lösen E
in CTΣ. Dann sind g ∗, h∗ : T E → CTΣ wegen (9) coΣ-homomorph. Da CTΣ eine finale
coΣ-Algebra ist, gilt g ∗ = h∗, also g = g ∗ ◦ incV = h∗ ◦ incV = h.
o
Sei cs ∈ StackCom ∗. Aus (7), (8) und Satz 19.4 (oder 19.6) folgt, dass execute0 die Entfaltung von V (cs) in CTΣ mit der Faltung der entstandenen Σ-Terme in Mach verknüpft:
V (cs)
incV
execute0
fold Mach
ω
=
g
TE
unfold
Mach
f
TE
CTΣ
418
Literatur
[1] M. Bonsangue, J. Rutten, A. Silva, An Algebra for Kripke Polynomial Coalgebras,
Proc. 24th LICS (2009) 49-58
[2] J.A. Brzozowski, Derivatives of regular expressions, Journal ACM 11 (1964) 481–494
[3] H. Comon et al., Tree Automata: Techniques and Applications, link, Inria 2008
[4] J. Gibbons, R. Hinze, Just do It: Simple Monadic Equational Reasoning, Proc.
16th ICFP (2011) 2-14 Jeremy Gibbons and Ralf Hinze
[5] J.A. Goguen, J.W. Thatcher, E.G. Wagner, J.B. Wright, Initial Algebra Semantics
and Continuous Algebras, Journal ACM 24 (1977) 68-95
[6] H.H. Hansen, J. Rutten, Symbolic Synthesis of Mealy Machines from Arithmetic
Bitstream Functions, CWI Report SEN-1006 (2010)
[7] R. Hinze, Functional Pearl: Streams and Unique Fixed Points, Proc. 13th ICFP
(2008) 189-200
[8] R. Hinze, D.W.H. James, Proving the Unique-Fixed Point Principle Correct, Proc.
16th ICFP (2011) 359-371
419
[9] J.E. Hopcroft, R. Motwani, J.D. Ullman, Introduction to Automata Theory, Languages, and Computation, Addison Wesley 2001; deutsch: Einführung in die Automatentheorie, Formale Sprachen und Komplexitätstheorie, Pearson Studium 2002
[10] G. Hutton, Fold and unfold for program semantics, Proc. 3rd ICFP (1998) 280-288
[11] B. Jacobs, Introduction to Coalgebra, draft, Radboud University Nijmegen 2012
[12] B. Jacobs, A Bialgebraic Review of Deterministic Automata, Regular Expressions
and Languages, in: K. Futatsugi et al. (eds.), Goguen Festschrift, Springer LNCS 4060
(2006) 375–404
[13] B. Klin, Bialgebras for structural operational semantics: An introduction, Theoretical Computer Science 412 (2011) 5043-5069
[14] D. Knuth, Semantics of Context-Free Languages, Mathematical Systems Theory 2
(1968) 127-145; Corrections: Math. Systems Theory 5 (1971) 95-96
[15] D. Kozen, Realization of Coinductive Types, Proc. Math. Foundations of Prog. Lang.
Semantics 27, Carnegie Mellon University, Pittsburgh 2011
[16] C. Kupke, J. Rutten, On the final coalgebra of automatic sequences, in: Logic and
Program Semantics, Springer LNCS 7230 (2012) 149-164
[17] A. Kurz, Specifying coalgebras with modal logic, Theoretical Computer Science 260
(2001) 119–138
420
[18] P.J. Landin, The Mechanical Evaluation of Expressions, Computer Journal 6 (1964)
308–320
[19] W. Martens, F. Neven, Th. Schwentick, Simple off the shelf abstractions for XML
Schema, SIGMOD Record 36,3 (2007) 15-22
[20] K. Meinke, J.V. Tucker, Universal Algebra, in: Handbook of Logic in Computer
Science 1 (1992) 189 - 368
[21] E. Moggi, Notions of Computation and Monads, Information and Computation 93
(1991) 55-92
[22] F.L. Morris, Advice on Structuring Compilers and Proving Them Correct, Proc.
ACM POPL (1973) 144-152 link
[23] T. Mossakowski, L. Schröder, S. Goncharov, A Generic Complete Dynamic Logic
for Reasoning About Purity and Effects, Proc. FASE 2008, Springer LNCS 4961
(2008) 199-214
[24] R.N. Moll, M.A. Arbib, A.J. Kfoury, Introduction to Formal Language Theory,
Springer (1988)
[25] P. Padawitz, Church-Rosser-Eigenschaften von Graphgrammatiken und Anwendungen auf die Semantik von LISP, Diplomarbeit, TU Berlin 1978
[26] P. Padawitz, Formale Methoden des Systementwurfs, TU Dortmund 2015
421
[27] P. Padawitz, Übersetzerbau, TU Dortmund 2015
[28] P. Padawitz, Modellieren und Implementieren in Haskell, TU Dortmund 2015
[29] P. Padawitz, Logik für Informatiker, TU Dortmund 2015
[30] P. Padawitz, Algebraic Model Checking, in: F. Drewes, A. Habel, B. Hoffmann, D.
Plump, eds., Manipulation of Graphs, Algebras and Pictures, Electronic Communications of the EASST Vol. 26 (2010)
[31] P. Padawitz, From Modal Logic to (Co)Algebraic Reasoning, TU Dortmund 2013
[32] P. Padawitz, Fixpoints, Categories, and (Co)Algebraic Modeling, TU Dortmund
2016
[33] H. Reichel, An Algebraic Approach to Regular Sets, in: K. Futatsugi et al., Goguen
Festschrift, Springer LNCS 4060 (2006) 449-458
[34] W.C. Rounds, Mappings and Grammars on Trees, Mathematical Systems Theory 4
(1970) 256-287
[35] J. Rutten, Processes as terms: non-wellfounded models for bisimulation, Math.
Struct. in Comp. Science 15 (1992) 257-275
[36] J. Rutten, Automata and coinduction (an exercise in coalgebra), Proc. CONCUR
’98, Springer LNCS 1466 (1998) 194–218
422
[37] J. Rutten, Behavioural differential equations: a coinductive calculus of streams,
automata, and power series, Theoretical Computer Science 308 (2003) 1-53
[38] J. Rutten, A coinductive calculus of streams, Math. Struct. in Comp. Science 15
(2005) 93-147
[39] A. Silva, J. Rutten, A coinductive calculus of binary trees, Information and Computation 208 (2010) 578–593
[40] D. Turi, G. Plotkin, Towards a Mathematical Operational Semantics, Proc. LICS
1997, IEEE Computer Society Press (1997) 280–291
[41] J.W. Thatcher, E.G. Wagner, J.B. Wright, More on Advice on Structuring Compilers and Proving Them Correct, Theoretical Computer Science 15 (1981) 223-249
[42] J.W. Thatcher, J.B. Wright, Generalized Finite Automata Theory with an Application to a Decision Problem of Second-Order Logic, Theory of Computing Systems
2 (1968) 57-81
[43] Ph. Wadler, Monads for Functional Programming, Proc. Advanced Functional Programming, Springer LNCS 925 (1995) 24-52
[44] R. Wilhelm, H. Seidl, Übersetzerbau - Virtuelle Maschinen, Springer 2007
423
Index
C-Abschluss, 355
E-Normalform, 100
E-Reduktionsrelation, 100
E-Äquivalenz, 97
DAut(X, Y ), 34
Σ(JavaLight), 121
Σ(XMLstore), 124
Σ-Algebra, 40
Σ-Gleichung, 89
Σ-Homomorphismus, 41
Σ-Isomorphismus, 41
Σ-Kongruenz, 87
Σ(G), 117
λ-Abstraktion, 206
λ-Applikation, 206
ω-CPO, 394
ω-stetig, 396
ω-stetige Funktion, 396
hai, 77
→c, 396
T (S, I), 29
state(ϕ), 183
(++), 216
abgeleitetes Attribut, 256
Abl(G), 129
Ableitungsbaum, 129
Ableitungsrelation, 114
absolute Adresse, 277
abstrakte Syntax, 117
Aktion eines Monoids, 70
all, 224
any, 224
Applikationsoperator, 210
Attribut, 230
Ausnahmefunktor, 141
Basisadresse, 277
424
Baumautomat, 80
Baumsprache, 80
Baumverhalten, 80
Befehlszähler, 276
Bild, 19
Bildalgebra, 41
bind-Operator, 145
bottom-up-Compiler, 177
Brzozowski-Automat, 60
Brzozowski-Gleichungen, 102
Calculus of Communicating Systems, 33
CCS(Act), 33
CFG, 109
charakteristische Funktion, 19
Coalgebra, 40
Coextension, 65
Coinduktion, 353, 357
Coinduktionsprinzip, 89
Compiler, 8
Compilermonade, 150
const, 213
Copotenzfunktor, 142
Coprodukt, 25, 415
Coterm, 47
curry, 214
denotationelle Semantik, 375, 399
destruktive Signatur, 30
Destruktor, 30, 230
deterministischer Baum, 18
Diagonale, 19
Diagonalfunktor, 140
Display, 277
Domain, 30
Domain-Tuplung, 25
drop, 217
eindeutig, 128
elem, 224
erkannte Baumsprache, 80
Erkennung einer Sprache, 71
Erreichbarkeitsfunktion, 61
425
execute, 272, 283
executeCom, 270
Extension, 62
Funktionsapplikation, 206
Funktionseinschränkung, 19
Funktionsprodukt, 24
Funktionssumme, 27
Funktionsupdate, 19
Funktor, 139
Färbung, 65
fail, 321
field label, 230
generischer Compiler, 155
filter, 224
Gleichungssystem einer CFG, 380
finale Algebra, 66
goto-Tabelle, 183
Fixpunktsatz für CFGs, 380
Fixpunktsatz für nicht-linksrekursive CFGs, Graph, 19
Grundcoterm, 48
389
Grundinstanz, 95
Fixpunktsatz von Kleene, 397
Grundterm, 46
flache Erweiterung von A, 394
flip, 214
halbgeordnete Menge, 394
fold, 63
Halbordnung, 394
fold2, 221
head, 216
foldl, 220
id, 213
foldr, 223
Identität, 14
Functor, 319
Identitätsfunktor, 140
Funktion höherer Ordnung, 208
Individuenvariable, 205, 212
426
Induktion, 346
Induktionsprinzip, 85
init, 217
initiale Algebra, 63, 408
initialer Automat, 71
Injektion, 25
Inklusion, 14
Instanz, 206
Instanz eines Terms, 95
Instanz eines Typs, 212
Instanzen von E, 97
Interpreter, 8
Invariante, 83
isomorph, 20, 41
iteratives Gleichungssystem, 378
JavaLight, 110
JavaLightP, 285
javaStackP, 289
Kategorie, 138
kausale Stromfunktion, 37
Kern, 19, 88
Kompositionsoperator, 210
Kongruenz modulo C, 355
Konkatenation, 17
konstanter Funktor, 140
konstruktive Signatur, 30
Konstruktor, 30
kontextfreie Grammatik, 109
Lösung eines iterativen Gleichungssystems, 378
LAG-Algorithmus, 315
Lambeks Lemma, 348
last, 217
leere Wort, 16
Leserfunktor, 142
lines, 219
linksrekursive Grammatik, 114
Liste, 16
Listenfunktor, 141
Listenkomprehension, 225
LL-Compiler, 335
427
lookup, 229
LR(k)-Grammatik, 178
LR-Automat für G, 183
map, 218
mapM, 326
Matching, 206
Mengenfunktor, 141
Methode, 230
Monad, 321
Monade, 143
MonadPlus, 324
Monoid, 43
monomorph, 212
monotone Algebra, 396
monotone Funktion, 396
mplus, 324
mzero, 324
natürliche Abbildung, 88
notElem, 224
Operation, 40
Operationen, 30
Parser, 8, 138
Paull-Unger-Verfahren, 91
Plusmonade, 147
polymorph, 212
Potenzfunktor, 142
präfixabgeschlossen, 18
Produkt, 21
Produktextension, 21
Produktfunktor, 140
Produkttyp, 29
Projektion, 21
Quotientenalgebra, 87
Range, 30
Range-Tuplung, 21
rational, 410
rationaler Coterm, 52
rationaler Term, 50
Realisierung eines Coterms, 68
Rechtsreduktion, 177
428
Redukt, 348
Reduktionsfunktion, 100
Reg(BS), 33
Regel, 109
reguläre Sprache, 64
regulärer Ausdruck, 33
rekursives Ψ-Gleichungssystem, 347
Relativadresse, 277
Resultatadresse, 298
return, 321
SAB, 119
Scanner, 7
Schreiberfunktor, 142
Sektion, 209
sequence, 325
Signatur, 30
Sprache über A, 16
Sprache einer CFG, 128
Sprache eines regulären Ausdrucks, 64
StackCom, 270
State, 270
statischer Vorgänger, 277
strikt, 396
strukturell-operationelle Semantik, 374
Substitution, 94
Summe, 25
Summenextension, 25
Summentyp, 29
symbolische Adressen, 278
Symboltabelle, 287
syntaktische Kongruenz, 93
syntaktisches Monoid, 93
Syntaxbaum, 118, 128
tail, 216
take, 217
Term, 45
Termfaltung, 63
Terminal, 109
top-down-Parser, 157
Trägermenge, 40
429
transientes Attribut, 256
Transitionsfunktor, 142
Transitionsmonoid, 92
Typ, 38
typ, 29
Typdeskriptor, 283
Typklassen, 235
Typkonstruktor, 205
Typvariable, 205
typverträglich, 38, 39
uncurry, 214
unfold, 66
unit-Typ, 205
universelle Eigenschaft, 21
unlines, 219
Unteralgebra, 83
unwords, 219
update, 213
Urbild, 19
vererbtes Attribut, 256
Verhaltensfunktion, 58
Verhaltenskongruenz, 89
Wildcard, 212
wohlfundierter Baum, 18
Word(G), 127
words, 219
Wort, 16
Wortalgebra, 127
zip, 218
zipWith, 218
Zustandsentfaltung, 66
Zustandsmonade, 323
Variablenbelegung, 62
430