Übersetzerbau

Werbung
Übersetzerbau
fldit-www.cs.tu-dortmund.de/∼peter/CbauFolien.pdf
Webseite: http://ls1-www.cs.uni-dortmund.de/∼padawitz/ueb.html
Peter Padawitz, TU Dortmund
1. Februar 2010
1
` Inhalt
` Compiler et al.
` Signaturen und Terme
` Algebren
` Initiale Algebren
` Compiler-Korrektheit
` Reguläre Ausdrücke und die Algebra der Sprachen
` Kontextfreie Grammatiken
` Term-, Wort- und Ableitungsalgebra
` Korrektheit monadischer Parser
` Das Gleichungssystem einer CFG
` Coalgebren = Algebren einer Destruktorsignatur
` Erkennende Automaten als Coalgebren
` Parser für reguläre Ausdrücke
` Vom LL-Parser zum LL-Compiler
2
` LR-Parser und -Compiler
` Haskell: Funktionen
` Haskell: Listen
` Haskell: Datentypen
` Typklassen
` Rund um Expr
` Datentypen mit Attributen
` Ein Haskell-Datentyp für Algebren
` Attributierte Grammatiken
` Haskell: Monaden
` Transitionsmonaden
` Monadische String-Parser
` Monadische Compiler
` Mehrpässige Compiler
` Korrektheit des JavaLight-Compilers*
3
` Deklarationen und ihre Adressierung*
` Literatur über algebraische Methoden im Compilerbau
` Index (In der Regel sind nur die Seiten angegeben, auf denen die jeweiligen Begriffe
oder Abkürzungen eingeführt werden.)
Die gesternten Abschnitte werden in dieser LV nur gestreift!
Interne Links zu den jeweiligen Definitionen sind an ihrer hellblauen Färbung erkennbar. Namen von Haskell-Modulen wie Java.hs sind mit den jeweiligen Programmdateien
verknüpft.
4
` Compiler et al.
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, die der Scanner nicht nur erkennt, sondern auch einem Basistyp (Int,
Float, Bool, Identifier, etc.) zuordnet. In der Grammatik kommen diese Symbole nicht
vor, jedoch ihre jeweiligen Typen.
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.
5
Ein Compiler (Programm zur semantischen Analyse und Codeerzeugung) übersetzt
ein Programm einer Quellsprache in ein semantisch äquivalentes Programm einer Zielsprache.
Ein Interpreter (Programm zur Auswertung oder Ausführung von Ausdrücken bzw.
Prozeduren)
• wertet einen Ausdruck in Abhängigkeit von einer Belegung seiner Variablen aus,
• führt eine Prozedur in Abhängigkeit von einer Belegung ihrer Parameter aus.
Im Grunde sind auch Scanner und Parser Compiler, denn auch sie tun nichts anderes
als Objekte von einem Format in ein anderes zu übersetzen. Und auch ein
Interpreter ist ein Compiler: Sein jeweiliges Zielobjekt ist die Funktion, die später
auf Eingabedaten angewendet wird. Es liegt deshalb nahe, alle o.g. Programme nach den
gleichen 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 (siehe GTI)
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.
6
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.
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 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 Symbol- und 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.
7
Die beiden folgenden Graphiken skizzieren die Struktur des algebraischen Ansatzes und
seine Auswirkung auf die Generizität der Programme. Auf die Details wird in den darauffolgenden Kapiteln eingegangen.
Bool
Auswertungsfunktionen
Σ-Formeln
Verifizierer
Parser
Algebren
Quellsprache
L(G)
Unparser
konkrete Syntax
CFG
G
abstrakte Syntax
Signatur
Σ(G)
Syntaxbäume
Termalgebra
TΣ(G)
Ableitungsbäume
Abl(G)
Transformer
Compiler/
Interpreter
Normalformen
optimierte Programme
Unteralgebra von TΣ(G)
Zielsprache
Z
Von konkreter über abstrakte Syntax zu Algebren
8
Menge_1
Compiler_1
kontextfreie
Sprache
Parser
Termalgebra
Compiler_i
Menge_i
Compiler_n
Menge_n
Algebra_1
kontextfreie
Sprache
Parser
Termalgebra
Compiler
Algebra_i
Algebra_n
Algebra_1
kontextfreie
Sprache
parsierender Compiler
Algebra_i
Algebra_n
Der Weg zum Compiler als interpretierendem Parser
9
` Signaturen und Terme
Um Modelle schrittweise aufzubauen, wird die Menge der Signaturen induktiv definiert:
• (∅, ∅, ∅, ∅) ist eine Signatur,
• Σ = (S, F, BΣ, BA) ist eine Signatur, falls BΣ = (BS, BF, BΣ0, BA0) eine Signatur
(die Basissignatur von Σ), BA eine BΣ-Algebra (die Basisalgebra von Σ), S eine
endliche Menge von Sorten mit BS ⊆ S und F eine endliche Menge typisierter
Funktionssymbole f :: w → s ist, wobei s ∈ S, w ∈ S ∗ und BF ⊆ F gilt.
Für alle f :: w → s ∈ F ist dom(f ) =def w der Domain und ran(f ) =def s der Range
von f .
f heißt Konstante, wenn der Domain von f das leere Wort ist.
f ∈ F \ BF heißt Konstruktor, wenn der Range von f zu S \ BS gehört.
f ∈ F \ BF heißt Destruktor, wenn der Domain von f bis auf genau eine Sorte aus
Basisorten besteht.
Σ heißt Konstruktorsignatur bzw. Destruktorsignatur, falls alle Funktionssymbole
von F \ BF Konstruktoren bzw. Destruktoren sind.
10
Die Basissignatur einer Konstruktor- bzw. Destruktorsignatur ist beliebig, im Rahmen ihres schrittweisen Entwurfs aber oft vom jeweils entgegengesetztem Typ, also eine Destruktor- bzw. Konstruktorsignatur.
In der LV Logik für Informatiker wurde nur der einsortige Fall behandelt. Im Rahmen
der dort behandelten Signaturen lässt sich Mehrsortigkeit mit Hilfe einstelliger Prädikate
simulieren: Mehrere Sorten werden durch mehrere Prädikate ersetzt, die jeweils die Zugehörigkeit zu der durch die Sorte bezeichneten Menge ausdrücken.
Sei S eine Menge. Eine S-sortierte Menge ist eine Mengenfamilie A = {As | s ∈ S}.
Man sagt “Mengenfamilie” und nicht, was die Schreibweise nahelegt, “Menge von Mengen”,
um Antinomien (logische Widersprüche) wie die Menge aller Mengen zu vermeiden.
Sortierung ist hier nur im Sinne einer Indizierung der Elemente von A gemeint, ohne
dass damit eine bestimmte partielle oder totale Ordnung der Elemente von A. Falls die
Mengen As paarweise disjunkt sind, wird A auch als Bezeichnung für die Vereinigung
aller As verwendet.
Sei A eine S-sortierte Menge und s1, . . . , sn ∈ S. Dann bezeichnet As1...sn das kartesische
Produkt As1 × . . . × Asn .
11
Sei X eine S-sortierte Menge logischer Variablen.
Die S-sortierte Menge TΣ(X) der Σ-Terme über X ist induktiv definiert:
• Für alle s ∈ S und x ∈ Xs ∪ BAs ist x ∈ TΣ(X)s.
• Für alle f :: w → s ∈ F und t ∈ TΣ(X)w ist f t ∈ TΣ(X)s.
TΣ =def TΣ(∅) ist die S-sortierte Menge der Σ-Grundterme.
12
` Algebren
Sei Σ = (S, F, BΣ, BA) eine Signatur.
Eine Σ-Algebra (A, OP ) besteht aus einer S-sortierten Menge A und einer Menge OP
von - auch Operationen genannten - Konstanten und Funktionen, den Interpretationen von F , mit folgenden Eigenschaften:
• Für alle f :: → s ∈ F gibt es genau einen Wert f A ∈ As.
• Für alle w ∈ S + und f :: w → s ∈ F gibt es genau eine Funktion f A : Aw → As ∈
OP .
• Das BΣ-Redukt von A (s.u.) stimmt mit BA überein.
BΣ wird also in allen Σ-Algebren gleich interpretiert.
Sei s ∈ S. Die Menge As heißt Trägermenge von s (carrier set; in der LV Logik für
Informatiker Grundmenge genannt).
Wie die hier verwendeten Schreibweisen zeigen, wird (A, OP ), falls sich aus dem jeweiligen
Kontext keine Konflikte ergeben, oft mit A gleichgesetzt!
13
In der klassischen Theorie formaler Sprachen kommen Algebren im Rahmen von Baumautomaten vor (siehe z.B. die grundlegenden Arbeiten [8, 12]). Neben einer Σ-Algebra
A enthält ein Σ-Baumautomat Aut zu jeder Sorte s von Σ eine ausgezeichnete Teilmenge
Bs von As, die dann auch als Menge der (s-)Endzustände von Aut bezeichnet wird. Eine
Menge T von Σ-Termen der Sorte s heißt Σ-erkennbar (recognizable), wenn evalA(T )
mit der Menge der s-Endzustände eines Σ-Baumautomaten übereinstimmt.
Sei Σ0 = (S 0, F 0, BΣ, BA) eine weitere Signatur mit S 0 ⊆ S und F 0 ⊆ F und A eine
Σ-Algebra.
Das Σ0-Redukt von A, geschrieben: A|Σ0 , ist die Σ0-Algebra, die BΣ genauso interpretiert
wie A:
• Für alle s ∈ S 0, A|Σ0,s = As.
• Für alle f :: w → s ∈ F , f A|Σ0 = f A.
Die Trägermengen eines Redukts von A sind also keine echten Teilmengen der entsprechenden Trägermengen von A.
14
Seien A und B S-sortierte Mengen. Eine S-sortierte Funktion h : A → B ist eine
Familie von Funktionen:
h : A → B = {hs : As → Bs | s ∈ S}.
Wieder wird die Sortierung auf Produkte fortgesetzt: Sei w = s1 . . . sn ∈ S +. Dann
bezeichnet hw die Funktion von Aw nach Bw , die jedes Tupel (a1, . . . , an) ∈ Aw auf das
Tupel (hs1 (a1), . . . , hsn (an)) ∈ Bw abbildet.
Sind A und B Σ-Algebren, dann ist h ein Σ-Homomorphismus, falls
• h mit den Interpretationen der Funktionssymbole von Σ in A bzw. B verträglich ist,
d.h. für alle f :: w → s ∈ F gilt:
hs ◦ f A = f B ◦ hw ,
• für alle s ∈ BS und a ∈ As ist hs(a) = a.
Die Übereinstimmung von hs : As → Bs für Basissorten s mit der entsprechenden
Identitätsfunktion kann gefordert werden, weil A und B Σ-Algebren sind und daher
As = BAs = Bs gilt!
A und B Σ-isomorph, falls es einen bijektiven Σ-Homomorphismus von A nach B gibt.
15
` Initiale Algebren
Sei Σ = (S, F, BΣ, BA) eine Konstruktorsignatur mit Basissignatur BΣ. Die S-sortierte
Menge TΣ der Σ-Grundterme lässt sich zu einer Σ-Algebra, der Σ-Termalgebra, erweitern:
• Für alle f :: w → s ∈ F \ BF und t ∈ TΣ,w , f TΣ (t) =def f t.
• TΣ|BΣ =def BA.
Ohne die Einschränkung auf Konstruktoren könnte es Σ-Terme einer Basissorte geben,
die nicht zu BA gehören, womit A = TΣ die Bedingung A|BΣ = BA an eine Σ-Algebra
A verletzen würde.
Allerdings ist jede Signatur mit leerer Basissignatur eine Konstruktorsignatur.
TΣ ist eine initiale Σ-Algebra, d.h. zu jeder Σ-Algebra A gibt es einen eindeutigen ΣHomomorphismus
evalA : TΣ → A.
Alle initialen Σ-Algebren sind zueinander Σ-isomorph. Man spricht deshalb von der initialen Algebra.
16
evalA ist induktiv über TΣ definiert und beschreibt die Auswertung von Σ-Termen
in A (in der LV Logik für Informatiker wurde [ t|]A für evalA(t) geschrieben):
• Für alle s ∈ BS und a ∈ As ist evalsA(a) = a.
• Für alle f :: w → s ∈ F und t ∈ TΣ,w ist evalsA(f t) = f A(evalwA(t)).
Enthält ein Term t Variablen (aus der Menge X), dann müssen diese vor der Auswertung
von t in A mit Elementen von A belegt werden. evalA hat dann den Typ
(X → A) → (TΣ(X) → A)
und folgende Definition: Sei β : X → A eine S-sortierte Funktion, genannt Variablenbelegung (valuation).
• Für alle s ∈ S und x ∈ Xs ist evalsA(β)(x) = β(x).
• Für alle s ∈ BS und a ∈ As ist evalsA(β)(a) = a.
• Für alle f :: w → s ∈ F und t ∈ TΣ(X)w ist evalsA(β)(f t) = f A(evalwA(β)(t)).
17
Nur im Falle einer Konstruktorsignatur Σ ist TΣ eine Σ-Algebra und daher evalA ein
Σ-Homomorphismus.
Eine totale Funktion liefert die obige Definition von evalA aber auch, wenn man sie auf
Terme anderer Signaturen anwendet.
Mit diesen Begriffen können wir nun in einer Algebra rechnen, z.B. Gleichungen lösen:
Sei n > 0. Ein Σ-Gleichungssystem ist eine Konjunktion
GLS
= (t1 = u1 ∧ . . . ∧ tn = un)
mit ti, ui ∈ TΣ(X)si für alle 1 ≤ i ≤ n und s1, . . . , sn ∈ S. Da GLS eine Konjunktion
ist, nennt man ti = ui, 1 ≤ i ≤ n, einen Faktor von GLS .
Sei A eine Algebra. Eine Variablenbelegung β : X → A heißt Lösung von GLS in A,
wenn für alle 1 ≤ i ≤ n
evalsAi (β)(ti) = evalsAi (β)(ui)
gilt.
18
` Compiler-Korrektheit
Mit TΣ als Quellsprache und einer Formulierung der Zielsprache als Σ-Algebra (Ziel) ist
ein Compiler nichts anderes als eine Implementierung der Funktion evalZiel , die Syntaxbäume in Ziel auswertet.
Ist die Semantik der Quellsprache ebenfalls als Σ-Algebra (Sem) gegeben, dann benötigen
wir zum Nachweis der Korrektheit des Compilers ein - unabhängig von Σ definiertes Modell M ach der Zielsprache, meist in Form einer abstrakten Maschine, einen Interpreter
execute, der, intuitiv gesprochen, Zielprogramme in M ach ausführt, sowie eine Funktion
encode, die Sem auf M ach abbildet und die gewünschte Arbeitsweise des Compilers auf
semantischer Ebene reflektiert.
Der Compiler ist korrekt, wenn das folgende Funktionsdiagramm kommutiert:
TΣ
evalSem
evalZiel
-
=
execute
?
?
Sem
Ziel
-
encode
19
M ach
Die Initialität von TΣ erlaubt es uns, den Korrektheitsbeweis auf folgende Schritte zu
reduzieren:
• M ach wird - wie Sem und Ziel - ebenfalls als Σ-Algebra formuliert. Das ist in
der Regel einfach, weil dazu nur die Schemata, nach denen die Definition von Ziel
Zielsprachenoperationen zu Σ-Operationen zusammensetzt, mittels execute auf M ach
homomorph übertragen werden müssen, d.h. für alle Funktionssymbole f :: w → s
muss gelten:
executes ◦ f Ziel = f M ach ◦ executew .
• encode wird ebenfalls als Σ-Homomorphismus formuliert.
Am Ende sind alle vier Abbildungen in obigem Diagramm und damit auch die beiden
Kompositionen execute ◦ evalZiel und encode ◦ evalM ach Σ-Homomorphismen. Da TΣ die
initiale Σ-Algebra ist, sind beide Kompositionen gleich. Also kommutiert das Diagramm.
20
` Reguläre Ausdrücke und die Algebra der Sprachen
Reguläre Ausdrücke sind Terme der folgenden Konstruktorsignatur:
Reg = ( S, F, BΣ, BA ) =
( {reg, word, symbol},
{∅, :: → reg,
_ :: symbol → reg,
_ | _ :: reg reg → reg,
_ · _ :: reg reg → reg,
plus, star :: reg → reg,
[] :: → word,
_ : _ :: symbol word → word},
({symbol}, ∅, ∅, ∅),
({Z}, ∅) )
(sorts)
(regular constants)
(regular operators)
(word constructors)
(base signature)
(base algebra)
Die Reg-Grundterme der Sorte reg sind offenbar genau die regulären Ausdrücke über Z.
Sie bilden daher die Reg-Termalgebra TReg . Was die Klammerung von Teilausdrücken
angeht, so hat wie üblich ein Produktoperator wie · eine höhere Priorität als ein Summenoperator wie |.
21
Um nicht bei jeder einzelnen Algebra unnötig viel interpretieren zu müssen, ist das Funktionssymbol star hier als Abkürzung für den Funktionsterm plus(_)| :: reg → reg
gemeint. Man nennt solche Funktionsterme auch abgeleitete Funktionssymbole. Sie
werden in jeder Σ-Algebra durch die Komposition der Interpretationen ihrer Komponenten interpretiert.
Die übliche Interpretation eines regulären Ausdrucks R als Wortmenge L(R) ⊆ Z ∗ erhält
man durch Auswertung von R in der Wortmengenalgebra Lang. Die Signatur Reg
wird dort folgendermaßen interpretiert:
Langreg
= ℘(Z ∗)
Langword = Z ∗
Langsymbol = Z
Sei a ∈ Z und L, L0 ∈ ℘(Z ∗).
∅Lang
Lang
_Lang (a)
L|Lang L0
L ·Lang L0
plusLang (L)
[]Lang
a :Lang w
=
=
=
=
=
=
=
=
∅
{}
{a}
L ∪ L0
{vw | v ∈ L, w ∈ L0}
{w1 . . . wn | n > 0, ∀ 1 ≤ i ≤ n : wi ∈ L}
[]
aw
22
Lang
(R).
Es gilt also L(R) = evalreg
Auch Bool ist eine Reg-Algebra:
Boolreg
= {True, False}
Boolword = Z ∗
Boolsymbol = Z
Sei a ∈ Z und b, b0 ∈ Bool.
∅Bool
Bool
_Bool (a)
b|Bool b0
b ·Bool b0
plusBool (b)
[]Bool
a :Bool w
=
=
=
=
=
=
=
=
False
True
False
b ∨ b0
b ∧ b0
b
[]
aw
Wie man leicht durch Induktion über den Aufbau von R nachrechnet, beantwortet
Bool
evalreg
(R) die Frage, ob in L(R) enthalten ist oder nicht.
23
` Kontextfreie Grammatiken
ordnen einer Signatur Σ eine konkrete Syntax und damit eine vom Compiler verstehbare Quellsprache zu, so dass er diese in eine - als Σ-Algebra formulierte - Zielsprache
übersetzen kann.
Eine kontextfreie Grammatik (CFG) G = (N, Z, P, BΣ, BA) besteht aus
• einer endlichen Menge N von Nichtterminalen,
• einer Menge Z von Terminalen oder Zeichen,
• einer endlichen Menge P von Regeln oder Produktionen der Form A → w mit
A ∈ N \ BS und w ∈ (N ∪ Z \ BA)∗,
• einer Signatur BΣ mit Sortenmenge BS ⊆ N und einer BΣ-Algebra BA, deren
Trägermengen zu Z gehören. Die Menge der Zeichen, die zu keiner Trägermenge von
BA gehören, wird als endlich vorausgesetzt.
Häufig werden n Regeln A → w1, . . . , A → wn mit Hilfe des regulären Summenoperators | zur Regel A → w1| . . . |wn zusammengefasst. Dies ist aber nur als abkürzende
Schreibweise zu verstehen und bedeutet nicht etwa die Einführung eines neuen Regeltyps!
24
Meistens genügt es, bei der Definition einer CFG nur deren Regeln und Basissorten anzugeben. Automatisch bilden dann die Symbole, die auf der linken Seite einer Regel
vorkommen, die Menge N \ BS. Die Menge der Symbole auf den rechten Seiten, die
nicht zu N gehören, bilden – zusammen mit den in P nicht auftretenden Elementen der
Basisalgebra (!) – die Menge der Terminale. Die Elemente der Basisalgebra muss erst ein
Parser von G kennen, weil sie nur im zu analysierenden Wort vorkommen können.
Beispiel FPF (functional programming fragment [5])
pdigit
digit
digitList
const
constList
fun
→
→
→
→
→
→
funList
→
1 | 2 | ... | 9
0 | pdigit
digit digitList | pdigit digitList | <> | < constList >
const, constList | const
id | head | tail | num | + | = | ≡ const |
fun ◦ fun | [funList] |
if fun then fun else fun | αfun | /fun
fun, funList | fun
25
Beispiel JavaLight
Commands →
Command →
IntE
SumSect
P roduct
P rodSect
F actor
BoolE
Conjunct
Literal
→
→
→
→
→
→
→
→
Command Commands | Command
; | String = IntE; | if BoolE {Commands} |
if BoolE {Commands} else {Commands} |
while BoolE {Commands}
P roduct SumSect
+ P roduct SumSect | − P roduct SumSect | F actor P rodSect
∗ F actor P rodSect | / F actor P rodSect | Int | String | (IntE)
Conjunct || BoolE | Conjunct
Literal && Conjunct | Literal
Bool | IntE IntE | !Literal | (BoolE)
für alle ∈ {<, >, <=, >=, ==, ! =}
Die Basissorten sind hier offenbar Int, Bool und String. Die zugehörigen Trägermengen
der Basisalgebra BA sind Z, {True, False} bzw. die Menge aller Zeichenfolgen außer den
auf der rechten Seite einer Regel auftretenden Terminalen (if, else, while, etc.).
Ein Element der von JavaLight erzeugten Sprache:
fact = 1; while x > 0 {fact = fact*x; x = x-1;}
26
Abstrakte Syntax einer CFG
Sei G = (N, Z, P, BΣ, BA) eine CFG. Die Konstruktorsignatur
Σ(G) = (N, F, BΣ, BA)
mit
F = {fp : A1 . . . An → A |
p = (A → w0A1w1 . . . Anwn) ∈ P,
}
w0, . . . , wn ∈ Z ∗, A1, . . . , An ∈ N }
heißt abstrakte Syntax von G.
Regeln mit n Nichtterminalen auf der rechten Seite werden also zu n-stelligen Funktionssymbolen, Regeln mit terminaler rechter Seite zu Konstanten. Erlaubt man Mixfixnotation und bezieht man den Typ in die Namen der Funktionssymbole bzw. Konstanten ein,
dann ist
w0_w1 . . . _wn :: A1 . . . An → A
eine alternative - eindeutige - Notation für das Funktionssymbol fA,w0A1w1...Anwn , das der
Regel A → w0A1w1 . . . Anwn mit w0, . . . , wn ∈ Z ∗ und A1, . . . , An ∈ N entspricht.
Σ(G)-Terme werden auch Syntaxbäume von G genannt. Sie dürfen nicht mit Ableitungsbäumen verwechselt werden: Die Knoten eines Syntaxbaums sind mit Funktionssymbolen markiert, die Knoten eines Ableitungsbaums mit Terminalen oder Nichtterminalen
(siehe Term-, Wort- und Ableitungsalgebra).
27
Beispiel SAB
Die Grammatik SAB =
Regeln
p1
p4
p6
(N, Z, P, ∅, ∅) besteht aus N = {S, A, B}, Z = {a, b} und den
= S → aB, p2 = S → bA, p3 = S → ,
= A → aS, p5 = A → bAA,
= B → bS, p7 = B → aBB
(vgl. GTI-Folie 9/20). Demnach besteht die abstrakte Syntax von SAB aus den Sorten
S, A, B und den Funktionssymbolen
fp1 :: B → S, fp2 :: A → S, fp3 :: → S,
fp4 :: S → A, fp5 :: AA → A,
fp6 :: S → B, fp7 :: BB → B
Ein Syntaxbaum von SAB:
28
Beispiel Σ(FPF)
N = { pdigit, digit, digits, const, consts, fun, funs }
F = { 1, 2 . . . , 9
:: → pdigit,
0
:: → digit,
pos
:: pdigit → digit,
[]
:: → digits,
_:_
:: digit digits → digits,
int
:: pdigit digits → const,
[]
:: → const,
list
:: const consts → const,
[_]
:: const → consts,
_:_
:: const consts → consts,
id, head, tail, num, add, eq :: → fun,
cfun
:: const → fun,
comp
:: fun fun → fun,
prod
:: fun funs → fun,
[_]
:: fun → funs,
_:_
:: fun funs → funs,
cond
:: fun fun fun → fun,
map, f old
:: fun → fun }
29
Beispiel Σ(JavaLight)
N = { Commands, Command, IntE, SumSect, P roduct, P rodSect, F actor,
BoolE, Conjunct, Literal } ∪ BS = {Int, Bool, String}
F = { seq
:: Command Commands → Commands,
embedC
:: Command → Commands,
skip
:: → Command,
assign
:: String IntE → Command,
cond
:: BoolE Commands Commands → Command,
cond1, loop
:: BoolE Commands → Command,
sum
:: P roduct SumSect → IntE,
addSect, subSect :: P roduct SumSect → SumSect,
emptyS
:: → SumSect,
prod
:: F actor P rodSect → P roduct,
mulSect, divSect :: F actor P rodSect → P rodSect,
emptyP
:: → P rodSect,
embedI
:: Int → F actor,
var
:: String → F actor,
encloseI
:: IntE → F actor,
or
:: Conjunct BoolE → BoolE,
embedA
:: Conjunct → BoolE,
and
:: Literal Conjunct → Conjunct,
embedL
:: Literal → Conjunct,
embedB
:: Bool → Literal,
rel()
:: IntE IntE → Literal für alle ∈ {<, >, <=, >=, ==, ! =},
not
:: Literal → Literal,
encloseB
:: BoolE → Literal }
30
Syntaxbaum des JavaLight-Programms
fact = 1; while x > 0 {fact = fact*x; x = x-1;}
31
` Term-, Wort- und Ableitungsalgebra
Sei G = (N, Z, P, BΣ, BA) eine CFG. Da Σ(G) eindeutig aus G konstruiert ist, nennen
wir eine Σ(G)-Algebra und einen Σ(G)-Homomorphismus schlicht G-Algebra bzw. GHomomorphismus.
Neben der G-Termalgebra bilden auch
• die Menge der Z ∗ der terminalen Wörter und
• die Menge der Ableitungsbäume von G
jeweils eine G-Algebra.
Word (G), die Wortalgebra von G
Sei A ∈ N .
Word (G)A =def
BAA falls A ∈ BS
Z ∗ sonst
Die Trägermengen von Word (G) hängen offenbar nicht von den Regeln von G ab. Diese
gehen jedoch in die Interpretation der Funktionssymbole ein:
Sei A → w0A1w1 . . . Anwn ∈ P mit w0, . . . , wn ∈ Z ∗ und A1, . . . , An ∈ N .
32
Sei v1, . . . , vn ∈ Z ∗.
Word (G)
fA,w0A1w1...Anwn : Word (G)A1 × . . . × Word (G)An → Word (G)A
(v1, . . . , vn) 7→ w0v1w1 . . . vnwn
t ∈ TΣ(G) ist ein Syntaxbaum für w ∈ Z ∗, falls evalWord (G)(t) = w gilt. G ist eindeutig,
wenn evalWord (G) injektiv ist.
Die Sprache L(G) von G ist das Bild von TΣ(G) unter evalWord (G).
L(G) ist selbst eine G-Algebra und zwar die kleinste G-Unteralgebra von Word (G).
Für eine beliebige Signatur Σ ist eine Σ-Unteralgebra B einer Σ-Algebra A bzgl. ihrer
Trägermengen eine Teilmenge von A mit der Eigenschaft, dass für alle Konstanten c :: s
von Σ, cB = cA und für alle Funktionssymbole f :: w → s und a ∈ Bw f B (a) ∈ Bs gilt.
Die Bildmenge evalA(TΣ) bildet stets die kleinste Σ-Unteralgebra von A.
Für CFGs ohne Basissignatur entspricht diese Definition der Sprache einer CFG der in der
Theorie formaler Sprachen verwendeten, die auf der aus GTI bekannten, aus G gebildeten
Wortersetzungsrelation →G basiert: Für alle A ∈ N ,
∗
L(G)A = {w ∈ Z ∗ | A →G w}.
33
Beispiel SAB
Alle drei Trägermengen der Wortalgebra alg = Word (SAB) sind durch {a, b}∗ gegeben.
Ihre Operationen sind wie folgt definiert: Sei v, w ∈ {a, b}∗.
alg
(w)
fS,aB
alg
= fA,aS
(w) = aw
alg
fS,bA
(w)
alg
= fB,bS
(w) = bw
fSalg
= alg
fA,bAA
(v, w) = bvw
alg
fB,aBB
(v, w) = avw
Für alle x ∈ Z und w ∈ Z ∗ bezeichne w#x die Anzahl der Vorkommen von x in w. o
34
` Korrektheit monadischer Parser
Die Wortalgebra erlaubt es uns, den Begriff der Compiler-Korrektheit nicht erst auf der
Zwischensprache TΣ(G) der Syntaxbäume, sondern auf der Quellsprache Z ∗, die auch
syntaktisch inkorrekte Eingaben umfasst, m.a.W. auf der Wortalgebra aufzusetzen.
Wir schalten dazu eine Funktion
parseG : Z ∗ → M (TΣ(G))
vor den Compiler evalZiel der Zwischensprache TΣ(G). M gehört zu einer (Plus-)Monade
(M, return, bind, mzero, mplus).
Hierbei ist M eine Funktion, die, intuitiv gesprochen, jede Menge auf einen bestimmten
Typ von Berechnungen ausgewählter Elemente dieser Menge abbildet. Die jeweilige Instanz von M legt fest, wie die Berechnungen ablaufen und mit welchen Ergebnissen sie
enden. Typische Instanzen von M werden im Abschnitt Haskell: Monaden behandelt.
Demnach beschreibt das Bild M (TΣ(G)) von parseG Berechnungen von Syntaxbäumen.
35
Die anderen Komponenten einer Monade (M, return, bind, mzero, mplus) sind für alle
Mengen A und B die Einbettung returnA : A → M (A), die Extension
_∗ : (A → M (B)) → (M (A) → M (B)),
die parallele Komposition mplusA : M (A) × M (A) → M (A) und die stets scheiternde
Berechnung mzero : M (A) derart, dass mplusA assoziativ ist, mzeroA das links- und
rechtsneutrale Element bzgl. mplusA ist und die folgenden Gleichungen gelten: Für alle
m ∈ M (A), f : A → M (B) und g : B → M (C),
return∗A
= idM (A),
f ∗ ◦ returnA
= f,
f ∗(mzeroA)
= mzeroB ,
(λx.mzeroB )∗(m) = mzeroB ,
g∗ ◦ f ∗
= (g ∗ ◦ f )∗.
Ausdrücke der Form λx.e heißen λ-Abstraktionen und bezeichnen Funktionen: λx.e
bezeichnet die durch die Gleichung f (x) = e definierte Funktion f . Ihre Anwendung auf
einen Wert a des Typs der Variablen x entspricht der λ-Applikation (λx.e)(a). Sie ist
semantisch äquivalent zum Wert des Ausdrucks e, in dem alle Vorkommen von x durch
a ersetzt wurden. In Haskell wird λ als backslash dargestellt (siehe Haskell: Funktionen).
x kann auch ein Tupel mehrerer Variablen sein.
36
Die Haskell-Implementierung von Monaden (siehe Haskell: Monaden) verwendet anstelle
der Extension die sequentielle Komposition
_ >>= _ : (M (A) × (A → M (B)) → M (B).
Für alle m ∈ M (A) und f : A → M (B),
m >>= f =def f ∗(m).
Weitere häufig benutzte abgeleitete Funktionen sind die Wächter
guard : (A → Bool) → (M (A) → M (A)).
Für alle m ∈ M (A) und f : A → Bool,
guardA(f ) =def (λx.if f (x) then return(x) else mzero)∗.
37
Die Funktion parseG kann also unabhängig von der konkreten Instanz von M definiert
werden. Sie ist ein korrekter Parser für L(G), wenn die Teildiagramme (1) und (2) des
folgenden Diagramms kommutieren:
∗
Z \L(G)
λw.mzero
-
(returnZiel ◦ evalZiel )∗-
M (TΣ(G))
M (Ziel)
-
6
6
inc
(1)
rse G
(2)
returnTΣ(G)
pa
evalZiel
?
Z
∗
eval
Word (G)
returnZiel
(3)
TΣ(G)
-
evalSem
(4)
Ziel
execute
?
?
Sem
-
encode
(1)
∀ w ∈ Z ∗ \L(G) : parseG(w) = mzero
(2)
parseG ◦ evalWord (G) = return
(3)
... folgt aus obiger Gleichung f ∗ ◦ returnA = f
(4)
... siehe Compiler-Korrektheit
38
M ach
Alle Funktionen im Korrektheitsdiagramm sind N -sortiert, d.h. dass u.a. der Parser
parseG als auch der Compiler evalZiel aus mehreren Funktionen bestehen – für jedes
Nichtterminal eine –, die sich entsprechend den Regeln von G gegenseitig aufrufen.
Außerdem lässt das Diagramm vermuten, dass die beiden Schritte: rekursive Konstruktion
von Syntaxbäumen durch parseG und deren nachfolgende rekursive Übersetzung/Auswertung durch evalZiel , zu einem zusammengezogen werden können, indem man die Komposition beider Funktionen als eine einzige rekursive Funktion
compileZiel
: Z ∗ → M (Ziel)
G
definiert, so dass nicht erst Syntaxbäume, sondern gleich deren Werte in Ziel konstruiert
werden.
Der Compiler compileZiel
G ist korrekt, wenn parseG ein korrekter Parser für L(G) ist (s.o.)
und (4) kommutiert.
39
Da das Rekursionsschema der Definition von evalZiel nicht von der konkreten Zielalgebra abhängt, kann compileZiel
ähnlich wie evalZiel generisch für alle G-Algebren Ziel
G
definiert werden.
wegen evalTΣ(G) = id - was aus der Initialität von
Für Ziel = TΣ(G) stimmt compileZiel
G
TΣ(G) folgt! - und obiger Gleichung return∗ = id mit parseG überein:
T
compileGΣ(G) = (return ◦ evalTΣ(G) )∗ ◦ parseG = return∗ ◦ parseG = parseG.
Demnach ist parseG genau dann ein korrekter Parser für L(G), wenn compileZiel
ein
G
korrekter Compiler ist.
40
Abl(G), die Ableitungsalgebra von G
Sei A ∈ N .
(
B ∈ Abl(G)A ⇐⇒def
B ist ein Baum mit Knoteneinträgen aus N \ BS ∪ Z
und Wurzeleintrag A
Sei A → w0A1w1 . . . Anwn ∈ P mit w0, . . . , wn ∈ Z ∗ und A1, . . . , An ∈ N .
Für alle 1 ≤ i ≤ n sei Bi ∈ Abl(G)Ai .
Abl(G)
fA,w0A1w1...Anwn : Abl(G)A1 × . . . × Abl(G)An → Abl(G)A
A
w0
B1
Bn
w1
B1
wn
Bn
Die Blätter eines Ableitungsbaums tragen Terminalzeichen (oder ), die inneren Knoten
Nichtterminale. Es handelt sich dabei jeweils um den Wertebereich der Funktion, die an
entsprechender Stelle im Syntaxbaum steht.
41
Ableitungsbaum des JavaLight-Programms
fact = 1; while x > 0 {fact = fact*x; x = x-1;}
42
` Das Gleichungssystem einer CFG
Sei M = {R1, . . . , Rn} eine Menge regulärer Ausdrücke.
P
P
Wir schreiben R∈M R für R1| . . . |Rn und ∅ für
∅.
Sei G = (N, Z, P, BΣ, BA) eine CFG, BS die Menge der Basissorten (Sorten von BΣ)
und {A1, . . . , An} = N \ BS. Das Reg-Gleichungssystem
X
X
GLS (G) = (A1 =
w ∧ · · · ∧ An =
w)
A1 →w∈P
An →w∈P
heißt Gleichungssystem von G. In GLS (G) werden alle Nichtterminale von G als
Variablen aufgefasst!
Satz Die Sprache von G ist die kleinste Lösung β : N → Lang von GLS (G) mit
β(A) = BAA für alle A ∈ BS.
Beweis:
Die Existenz der kleinsten Lösung von GLS (G) in Lang folgt aus dem Fixpunktsatz
von Knaster und Tarski. Seine einzige Bedingung an das zu lösende Gleichungssystem ist die Monotonie der darin auftretenden Operatoren. Die aber gilt aber für
alle Operationen von Reg. Für den Komplementoperator compl :: reg → reg mit
complLang (L) =def Z ∗ \ L gilt sie z.B. nicht!
43
Zusätzlich zur Existenz liefert der Fixpunktsatz von Kleene eine iterative Konstruktion der kleinsten Lösung. Allerdings verlangt dieser die - im Vergleich zur Monotonie stärkere Stetigkeit der im jeweiligen Gleichungssystem auftretenden Operationen, die bei
GLS (G) aber auch gegeben ist.
Beginnend mit dem |N |-Tupel leerer Sprachen vergrößert jeder Iterationsschritt der Lösungskonstruktion die Komponenten eines Sprachtupels (L1, . . . , Ln): Für alle 1 ≤ i ≤
n besteht Li nach dem k-ten Iterationsschritt aus allen Wörtern, deren Urbild unter
evalWord (G) ein Syntaxbaum ist, dessen Höhe k nicht übersteigt.
Beide Fixpunktsätze werden nicht nur auf GLS(G) angewendet. Sie dienen ganz allgemein der Lösung regulärer Gleichungssysteme, die sich dadurch auszeichnen, dass
eine Seite jeder Gleichung eine Variable ist. Jeder knotenmarkierte Graph lässt sich als
reguläres Gleichungssystem darstellen. Im Übersetzerbau werden z.B. häufig imperative
Zielprogramme in Datenflussgraphen umgesetzt und diese durch Gleichungen repräsentiert. Deren Lösungen in geeigneten Wertebereichen liefern wichtige semantische Informationen über die Programme, die wiederum die Grundlage für Optimierungen bilden
(siehe z.B. Kapitel 9 meines Übersetzerbau-Skripts).
44
Nach Definition der Lösung eines Reg-Gleichungssystems ist eine Variablenbelegung β :
N → Lang genau dann eine Lösung von GLS (G), wenn für alle Faktoren A = R von
GLS (G)
β(A) = evalLang (β)(R)
gilt. Wir zeigen zunächst, dass β mit
(
L(G)A falls A ∈ N \ BS,
β(A) =
BAA sonst
eine Lösung von GLS (G) ist. Sei also A = R ein Faktor von GLS (G). Daraus folgt
β(A) = L(G)A = evalWord (G)(TΣ(G),A) =
Word (G)
{fA,A1...An (v1, . . . , vn) | A → w0A1w1 . . . Anwn ∈ P, vi ∈ L(G)Ai , wj ∈ Z ∗} =
{w0v1w1 . . . vnwn | A → w0A1w1 . . . Anwn ∈ P, vi ∈ L(G)Ai , wj ∈ Z ∗} =
{w0v1w1 . . . vnwn | A → w0A1w1 . . . Anwn ∈ P, vi ∈ β(Ai), wj ∈ Z ∗},
(1)
also auch
P
S
evalLang (β)(R) = evalLang (β)( A→w∈P w) = A→w∈P evalLang (β)(w) =
S
Lang
(β)(A1)w1 . . . evalLang (β)(An)wn =
A→w0 A1 w1 ...An wn ∈P, wj ∈Z ∗ w0 eval
S
A→w0 A1 w1 ...An wn ∈P, wj ∈Z ∗ w0 β(A1 )w1 . . . β(An )wn .
(2)
Da (1) und (2) dieselben Mengen darstellen, ist β tatsächlich eine Lösung von GLS (G).
45
β ist die kleinste Lösung von GLS (G) in Lang, weil L(G) das Bild von TΣ(G) unter dem GHomomorphismus evalWord (G) ist und damit die kleinste G-Unteralgebra von Word (G).
Demnach bleibt zu zeigen, dass sich jede Lösung β : N → Lang von GLS (G) als GUnteralgebra β von Word (G) darstellen lässt. Wir definieren β wie folgt:
• Für alle A ∈ N , β A = β(A).
• Für jede Regel A → w0A1w1 . . . Anwn ∈ P (wi ∈ Z ∗, Ai ∈ N )
Word (G)
β
und (v1, . . . , vn) ∈ β(A1) × . . . × β(An), fA,A
(v
,
.
.
.
,
v
)
=
f
(v1, . . . , vn).
1
n
...A
A,A
n
1
1 ...An
Wegen
Word (G)
fA,A1...An (v1, . . . , vn) = w0v1w1 . . . vnwn ∈ w0β(A1)w1 . . . β(An)wn
= w0evalLang (β)(A1)w1 . . . evalLang (β)(An)wn = evalLang (β)(w0A1w1 . . . Anwn)
U
P
⊆ A→w∈P evalLang (β)(w) = evalLang (β)( A→w∈P w) = β(A)
o
ist β eine G-Unteralgebra von Word (G).
46
Beispiel SAB Für alle w ∈ Z ∗ und x ∈ Z bezeichne w#x die Anzahl der Vorkommen
von x in w. Man sieht fast auf Anhieb, dass
GLS (SAB) = (S = aB | bA | ∧
A = aS | bAA
∧
B = bS | aBB)
von β : N → ℘(Z ∗) mit
β(S) = {w ∈ Z ∗ | w#a = w#b},
β(A) = {w ∈ Z ∗ | w#a = w#b + 1},
β(B) = {w ∈ Z ∗ | w#a = w#b − 1}
o
gelöst wird.
47
Eine CFG G = (N, Z, P, BΣ, BA) heißt bewacht, wenn sich N ∪ Z \ BA so ordnen lässt,
dass die Elemente von BS ∪ Z \ BA minimal sind und für alle A → ϕ ∈ P , ϕ = gilt
oder ϕ mit einem Zeichen x < A beginnt.
Beispiele
Die CFG G1 = ({A, B}, {a, b}, {A → BA, A → a, B → b}, ∅, ∅) ist bewacht. GLS (G1)
hat genau eine Lösung in ℘({a, b}∗), nämlich die Sprache des regulären Ausdrucks b∗a.
Demgegenüber ist die CFG G2, die sich von G1 nur darin unterscheidet, dass die Regel
B → b durch B → ersetzt ist, nicht bewacht und jede Funktion β : {A, B} → ℘({a, b}∗)
mit a ∈ β(A) und β(B) = {} löst GLS (G2).
JavaLight ist bewacht: Wähle < als transitiven Abschluss folgender Relation:
{(x, A) | x ∈ BS ∪ Z \ BA, A ∈ N \ BS} ∪
{(F actor, P roduct), (P roduct, IntE), (IntE, Literal), (Literal, Conjunct), (Conjunct, BoolE),
(Command, Commands)}
Bewachtheit motiviert die Form der Regeln von JavaLight, insbesondere derjenigen für arithmetische und
Boolesche Operationen. Die Regeln reflektieren die üblichen Operatorprioritäten sowie die Linksassoziativität arithmetischer und die Rechtassoziativität Boolescher Verknüpfungen.
Ist G bewacht, dann ist G nicht linksrekursiv, was garantiert, dass LL-Parser für
G terminieren (s.u.). Außerdem impliziert Bewachtheit die eindeutige Lösbarkeit von
GLS (G):
48
Fixpunktsatz für CFGs Ist G bewacht, dann hat GLS (G) genau eine Lösung in
Lang mit β(A) = BAA für alle A ∈ BS.
Beweis:
Die Existenz einer Lösung von GLS (G) folgt aus dem Fixpunktsatz von Knaster und
Tarski.
Wir erweitern Belegungen β : N → ℘(Z ∗) zu Funktionen von (N ∪ Z)∗ nach ℘(Z ∗) durch
β(x) = x für alle x ∈ Z und β(x1 . . . xn) = β(x1) · · · · · β(xn) für alle x1, . . . , xn ∈ N ∪ Z.
Seien β, γ : N → ℘(Z ∗) zwei Lösungen von GLS (G) in Lang mit β(A) = BAA für alle
A ∈ BS. Wir zeigen
∀ w ∈ Z ∗ ∀ A ∈ N \ BS : w ∈ β(A) ⇔ w ∈ γ(A)
und damit β = γ durch Induktion über (length(w), A).
Sei ∈ β(A). Da G bewacht ist und β GLS (G) löst, enthält P die Regel A → . Da
auch γ eine Lösung von GLS (G) ist, folgt ∈ γ(A).
Sei x ∈ Z, w ∈ Z ∗ und xw ∈ β(A). Da β GLS (G) löst, gibt es A → ϕ ∈ P mit
xw ∈ β(ϕ). Da G bewacht ist, gibt es y ∈ N ∪ Z \ BA und ψ ∈ (N ∪ Z)∗ mit yψ = ϕ
und y < A. Daraus folgt xw ∈ β(yψ).
49
Fall 1: y ∈ BS. Dann ist xw ∈ β(yψ) = β(y) · β(ψ) ∈ BAA · β(ψ), also x ∈ BAA
und w ∈ β(ψ). Nach Induktionsvoraussetzung folgt w ∈ γ(ψ), also xw ∈ x · γ(ψ) =
γ(y) · γ(ψ) = γ(ϕ).
Fall 2: y ∈ Z. Dann ist xw ∈ β(yψ) = β(y) · β(ψ) ∈ y · β(ψ), also x = y und w ∈ β(ψ).
Nach Induktionsvoraussetzung folgt w ∈ γ(ψ), also xw ∈ x · γ(ψ) = γ(y) · γ(ψ) = γ(ϕ).
Fall 3: y ∈ N \ BS. Wegen xw ∈ β(yψ) = β(y) · β(ψ) gibt es w0, w00 ∈ Z ∗ mit w0w00 = w,
xw0 ∈ β(y) und w00 ∈ β(ψ). Nach Induktionsvoraussetzung folgt xw0 ∈ γ(y) und w00 ∈
γ(ψ), also xw = xw0w00 ∈ γ(y) · γ(ψ) = γ(ϕ).
o
50
Beispiel SAB Da SAB bewacht ist, stimmt die oben angebene Lösung β von GLS (SAB)
mit der von SAB erzeugten Sprache überein.
Ein zu kurzer Blick auf GLS (SAB) könnte auch die Belegung β mit β(S) = Z ∗ und
β(A) = β(B) = Z + als Lösung erscheinen lassen. Das stimmt jedoch nicht. Z.B. gehört
{a} zu β(S), aber weder zu evalLang (β)(aB) noch zu evalLang (β)(bA) noch stimmt {a}
mit Lang = {} überein, d.h. β erfüllt die erste Gleichung von GLS (SAB) nicht!
o
Man braucht also - im Gegensatz zu entsprechenden, in GTI vorgeführten Beweisen keine Induktion, weder über Wortlängen noch über Ableitungslängen, um zu zeigen, dass
eine bewachte CFG eine bestimmte Sprache erzeugt. Das liegt daran, dass wir den Fixpunktsatz für CFGs verwenden konnten, der selbst durch Induktion über Wortlängen
bewiesen wurde.
Im nächsten Abschnitt wird Lang coalgebraisch charakterisiert und damit als alternatives Beweisverfahren für Sprachäquivalenzen die Coinduktion zur Verfügung gestellt.
Der Hauptzweck der coalgebraischen Charakterisierung besteht allerdings in der daraus
herleitbaren Konstruktion von Parsern für reguläre Ausdrücke bzw. bewachte CFGs.
51
` Coalgebren = Algebren einer Destruktorsignatur
Eine Destruktorsignatur Σ = (S, F, BΣ, BA) (siehe Signaturen und Terme) spezifiert
eine Klasse von (nicht notwendig endlichen) Automaten: Die Nicht-Basissorten einer Destruktorsignatur nennen wir deshalb Zustandssorten. Entsprechend stehen die Funktionssymbole f :: w → s von F \ BF für Zustandsübergangsfunktionen (falls s ∈ S \ BS)
bzw. Ausgabefunktionen (falls s ∈ BS).
Folglich eignen sich Destruktorsignaturen auch zur objektorientierten Spezifikation: s ∈
S \ BS bezeichnet eine Klasse, f :: sw → s0 ∈ F mit Basissorte bzw. Zustandssorte s0
ein Attribut bzw. eine Methode der Klasse s. Σ kann auch ein ganzes Klassendiagramm
beschreiben - mit Benutzt-Beziehungen, aber ohne Vererbungsrelationen. Unterklassen
ließen sich durch Signaturerweiterungen beschreiben.
52
Sei s eine Zustandssorte und x eine feste Variable der Sorte s. Ein Σ-Term
[
TΣ,s0 ({x})
t∈
s0 ∈BS
heißt s-Beobachter (observer). Sei A eine Σ-Algebra. Belegt man x mit einem Element
a ∈ As und wertet t unter dieser Belegung in A aus, dann beschreibt das Ergebnis der
Auswertung einen Teil des Verhaltens (behavior) von a. Das gesamte Verhalten von a ist
durch das – i.d.R. unendliche – Tupel der Werte aller s-Beobachter gegeben.
Die Menge der s-Beobachter bezeichnen wir mit Obss. Obs bildet demnach eine (S \BS)sortierte Menge.
Die Typbedingung an die Funktionssymbole einer Destruktorsignatur impliziert, dass
jeder s-Beobachter die Form
fn(. . . (f1(x, a11, . . . , a1k1 ) . . . ), an1, . . . , ankn )
hat, wobei a11, . . . , a1k1 , . . . , an1, . . . , ankn Elemente von Trägermengen der Basisalgebra
sind.
Die Menge aller Tupel ausgewerteter Beobachter ist selbst eine Σ-Algebra, die
Σ-Verhaltensalgebra genannt und mit BehΣ bezeichnet wird.
53
BehΣ ist eine finale Σ-Algebra, d.h. zu jeder Σ-Algebra A gibt es einen eindeutigen
Σ-Homomorphismus
unfold A : A → BehΣ.
Im Vergleich mit evalA : TΣ → A verläuft unfold A : A → BehΣ in umgekehrter Richtung. Deshalb nennt man Finalität die zur Initialität duale Eigenschaft. Die Bezeichnung Coalgebren für Algebren einer Destruktorsignatur rührt ebenso von dieser Dualität
her wie der Name “unfold” für einen Homomorphismus in eine finale Coalgebra: Während
die Auswertung eines Terms mit evalA diesen zu einem Wert in A faltet, so entfaltet die
Funktion unfold A ein Element von A, indem sie die Ergebnisse der o.g. Beobachterauswertungen auflistet.
Auch die unterschiedlichen Aufgaben der Funktionen einer Coalgebra einerseits und einer
Algebra einer Konstruktorsignatur andererseits machen beide dual zueienander: letztere
benutzt ihre Funktionen zur Synthese, erstere zur Analyse (der Elemente) ihrer Trägermengen.
Wie die initialen Algebren einer Konstruktorsignatur, so sind auch die finalen Coalgebren einer Destruktorsignatur zueinander isomorph. Man spricht deshalb von der finalen
Coalgebra.
54
Definition von BehΣ
• Für alle s ∈ S \ BS,
Y
BehΣ,s =def
BAsort(t),
t∈Obss
0
• für alle s ∈ S \ BS, w ∈ BS ∗, f :: sw → s ∈ F ,
(at(x))t∈Obss ∈ BehΣ,s und (b1, . . . , bn) ∈ BAw ,
(
af (x,b1,...,bn)
falls s ∈ BS,
f BehΣ ((at)t∈Obss , b1, . . . , bn) =def
(at(f (x,b1,...,bn)))t∈Obss0 sonst.
• BehΣ|BΣ =def BA.
Sei A eine Σ-Algebra und b : X → A eine Variablenbelegung.
Funktionen in ein Produkt wie BehΣ,s definiert man oft durch Angabe der einzelnen
Komponenten des Bildtupels: Für alle s ∈ S \ BS, x ∈ Xs und t ∈ Obss,
unfold A(b(x))t =def evalA(b)(t).
unfold A bildet das Element b(x) ∈ A auf das Tupel seiner ausgewerteten Beobachter t
ab.
55
Wir fassen zusammen:
• Für jede Konstruktorsignatur Σ ist die Σ-Termalgebra die initiale Σ-Algebra,
• Für jede Destruktorsignatur Σ ist die Σ-Verhaltensalgebra die finale Σ-Algebra.
Die initiale Algebra einer Destruktorsignatur macht genausowenig Sinn wie die finale
Algebra einer Konstruktorsignatur: Erstere hat nur leere Trägermengen, weil es unter
Destruktoren keine Konstanten gibt und deshalb die Basis zum Aufbau von Grundtermen
fehlt; letztere hat nur einelementige Trägermengen, was mit der Charakterisierung der
Gleichheit in finalen Algebren zusammenhängt (s.u.).
Obgleich Destruktoren immer durch totale Funktionen interpretiert werden, können auch
nichtdeterministische Automaten als Algebren einer Destruktorsignatur dargestellt werden. Man könnte z.B. die Übergangsfunktion mit einer natürlichen Zahl parametrisieren,
die angibt, welcher der möglichen Nachfolger ausgewählt werden soll. δ hätte dann einen
Typ der Form δ :: state × nat → state. Eleganter lassen sich nichtdeterministische und
andere Funktionen mit mehreren “Ausgängen” als Destruktoren einführen, die in eine
Summe von Sorten der Signatur abbilden:
f :: sw → s1 + · · · + sn.
Das Verhaltensmodell einer solchen Destruktorsignatur ist aber um einiges komplizierter
als das oben definierte und wird deshalb nicht hier, sondern in meinen LVs über LogischAlgebraischen Systementwurf behandelt.
56
Eine Σ-Algebra A umfasst in der Regel mehrere Automaten, da zunächst kein Startzustand ausgewählt ist. Die Menge der von einem Startzustand st durch wiederholte Anwendung von Destruktoren erreichbaren Zustände von A bildet ebenfalls eine Σ-Algebra,
die von st erzeugte Unteralgebra von A genannt und mit hsti bezeichnet wird.
Aus der Homomorphie von unfold A folgt die Übereinstimmung des Bildautomaten
unfold A(hsti) mit der von unfold A(st) erzeugten Unteralgebra von A. Ist der Automat
hsti endlich, dann ist sein Bild unter unfold A natürlich auch endlich, mehr noch: es ist der
minimale Automat unter allen Automaten mit Startzustand und gleichem Verhalten,
d.h. gleichem unfold A-Bild des jeweiligen Startzustandes.
57
` Erkennende Automaten als Coalgebren
Spracherkennende Automaten (language acceptors) sind Algebren der folgenden Destruktorsignatur:
Accept = ( S, F, BΣ, BA ) =
( {state, symbol, Bool},
{δ :: state symbol → state,
(sorts)
(transition function)
accepting :: state → Bool,
({symbol, Bool},
Boolean operators, ∅, ∅),
(base signature)
({Z, {True, False}},
standard Boolean arithmetic ) (base algebra)
58
Lang ist Accept-isomorph zur Accept-Verhaltensalgebra BehAccept
Wir haben Lang als Algebra der Konstruktorsignatur Reg eingeführt. Jetzt wird wir
Lang zur Accept-Algebra erweitert:
Langstate
= ℘(Z ∗),
Langsymbol = Z,
LangBool
Sei x ∈ Z, w ∈ Z ∗ und L, L0 ∈ ℘(Z ∗).
= {True, False}.
= {w ∈ Z ∗ | xw ∈ L},
(
True falls ∈ L,
accepting Lang (L) =
False sonst.
Tatsächlich ist Lang isomorph zu BehAccept: Jeder state-Beobachter hat die Form
δ Lang (L, x)
accepting(δ(. . . (δ(x, a1), . . . ), an),
ist also eindeutig durch ein Wort (hier: a1 . . . an) beschrieben. Sein Wert liegt in Bool.
Umgekehrt lässt sich jedes Wort w ∈ Z ∗ eindeutig als state-Beobachter repräsentieren.
Es gibt also eine Bijektion zwischen Obsstate und Z ∗. Wegen BABool = {True, False} ist
daher jedes Element von BehAccept,state die charakteristische Funktion einer Wortmenge,
m.a.W.:
BehAccept,state ∼
(2)
= ℘(Z ∗) = Langstate.
59
Die Accept-Homomorphie dieser Bijektion ergibt sich ebenfalls aus der zwischen Obsstate
und Z ∗.
Lang ist also die finale Accept-Algebra, so dass sich die obigen Aussagen über die Verhaltensalgebra einer beliebigen Destruktorsignatur vermöge der Bijektion (2) auf Lang
übertragen lassen: Sei A ein erkennender Automat, st ∈ Astate und L ⊆ Z ∗.
• unfold A(st) ist die von A erkannte Sprache, wenn A im Zustand st losläuft.
• unfold A(hsti) ist der minimale Automat, der diese Sprache erkennt.
• Die Unteralgebra hLi von Lang ist der minimale Automat, der L erkennt.
Charakterisierung der Zustände von hLi
Regulär ist eine Sprache L laut GTI genau dann, wenn sie von einem endlichen Automaten
erkannt wird, was nach dem oben Gesagten genau dann gilt, wenn hLi endlich ist.
Daraus folgt der in GTI behandelte Satz von Myhill und Nerode: L ist genau dann regulär, wenn ∼L⊆ Z ∗ × Z ∗ endlich viele Äquivalenzklassen hat, wobei ∼L folgendermaßen
definiert ist:
v ∼L w ⇐⇒def ∀ u ∈ Z ∗ : vu ∈ L ⇔ wu ∈ L.
Wie man leicht nachrechnet, sind die Äquivalenzklassen von ∼L nämlich gerade die Zustände von hLi.
60
Zum Rechnen und Beweisen in Coalgebren
Das Rechnen in einer Algebra wird wesentlich vom dort verwendeten Gleichheitsbegriff bestimmt. In einer initialen bedeutet Gleichheit Termidentität, in einer finalen bedeutet sie Verhaltensgleichheit. Auch hier gibt es wieder eine Dualität: Termidentität ist
die kleinste, Verhaltensgleichheit die größte Σ-Kongruenz (binäre Relation, die mit den
Funktionen von Σ verträglich ist. Im Verhaltensfall werden Kongruenzen auch Bisimulationen genannt. Auf den Trägermengen der Basissorten ist Gleichheit natürlich immer
Identität.
Die beiden Charakterisierungen der Gleichheit werden verwendet, um die Gültigkeit von
Gleichungen in einer initialen bzw. finalen Algebra zu zeigen. Die entsprechenden Beweisregeln heißen Induktion bzw. Coinduktion.
Die Charakterisierung der Gleichheit in der finalen Σ-Algebra A als größte Σ-Kongruenz
begründet auch die o.g. Behauptung, dass A nur einelementige Trägermengen hat, falls
Σ eine Konstruktorsignatur ist: Nach Definition ist ∼ ⊆ A2 eine Σ-Kongruenz, wenn für
alle f :: w → s ∈ Σ und a, b ∈ Aw
a ∼ b ⇒ f A(a) ∼ f A(b)
(3)
gilt. Ist s keine Basissorte, dann gilt (3) für die Allrelation ∼s= {(a, b) | a, b ∈ As}.
Da die Gleichheit in A mit der größten Σ-Kongruenz auf A übereinstimmt und es keine
größere Σ-Kongruenz als die Allrelation geben kann, sind alle Elemente von As gleich.
61
Beispiel Da δ und accepting die Destruktoren von Accept sind, ist ∼ ⊆ ℘(Z ∗)2 genau
dann eine Accept-Kongruenz auf einer Accept-Algebra, wenn diese die folgende Implikation erfüllt:
∀ x, y, z : (x ∼ y =⇒ δ(x, z) ∼ δ(y, z) ∧ accepting(x) = accepting(y))
(4)
Da Lang die finale Accept-Algebra ist, sind zwei Sprachen L und L0 genau dann gleich,
wenn sie in der größten Accept-Kongruenz liegen. Da die Vereinigung von Kongruenzen
selbst eine solche ist (Beweis!), ist sie die größte und wir folgern, dass L und L0 genau
dann übereinstimmen, wenn (L, L0) zu irgendeiner Accept-Kongruenz auf ℘(Z ∗) gehört.
Diese zu finden ist das Ziel eines Beweises durch Coinduktion.
Man definiert ∼ zunächst als die Menge genau der Paare von Sprachen, deren Gleichheit
gezeigt werden soll. Ist ∼ keine Kongruenz, dann werden Sprachpaare hinzugefügt, die sich
im Laufe des Beweises ergeben, bis eine Kongruenzrelation gefunden ist. Die Termination
des Verfahrens ist nicht gesichert.
Die Vergrößerung von ∼ entspricht der Verallgemeinerung einer Behauptung, die durch
Induktion bewiesen werden soll. Auch dort kann nicht garantiert werden, dass jemals eine
gültige Behauptung erreicht wird.
o
62
Beispiel Seien x, y, z Variablen. Durch Coinduktion wollen wir zeigen, dass die Interpretation von | in Lang kommutativ ist, m.a.W., dass die Gleichung x|y = y|x in Lang
gilt. Benutzt werden sollen hierzu nur die beiden in Lang gültigen Gleichungen:
(eq1)
δ(x|y, z) = δ(x, z)|δ(y, z),
(eq2)
accepting(x|y) = accepting(x) ∨ accepting(y),
nicht jedoch die Definition von |Lang als Mengenvereinigung!
Wir setzen ∼= {(L|L0, L0|L) | L, L0 ∈ ℘(Z ∗)} und zeigen, dass ∼ eine Accept-Kongruenz
ist (siehe (4)).
Sei L1, L2 ∈ ℘(Z ∗).
L1 ∼ L2 ⇐⇒ ∃ L, L0 : L1 = L|L0 ∧ L2 = L0|L
eq1
eq1
=⇒ ∃ L, L0 ∀ a ∈ Z : δ(L1, a) = δ(L, a)|δ(L0, a) ∼ δ(L0, a)|δ(L, a) = δ(L2, a) ∧
eq2
accepting(L1) = accepting(L) ∧ accepting(L0)
eq2
= accepting(L0) ∧ accepting(L) = accepting(L2)
⇐⇒ ∀ a ∈ Z : δ(L1, a) ∼ δ(L2, a) ∧ accepting(L1) = accepting(L2).
∼ ist also in der größten Accept-Kongruenz enthalten, die – wie oben ausgeführt – mit der
Gleichheit in der finalen Accept-Algebra Lang übereinstimmt, d.h. für alle L, L0 ∈ ℘(Z ∗)
gilt tatsächlich L|L0 = L0|L.
o
63
` Parser für reguläre Ausdrücke
Bool
T
accepting Reg
Automat A
Zustandsmenge S
accepting
Lang
TReg
(3)
(reguläre Ausdrücke)
unfold
inclusion
A
eval
Lang
∪w∈Z* δ*A(st,w)
evalLang
δLang
<st>
(2)
TReg
(4)
(Sprachen)
erreichbarer Teilautomat <st>
Zustandsmenge
δTReg
T
unfold Reg
(1)
unfold
Lang
Lang
inclusion
nf
evalΣLang
minimization
64
TParse/~ParseE
nat
TParse
Automat <L(<st>)>
≅
Blaue Pfeile bezeichnen Reg-Homomorphismen.
Rote Pfeile bezeichnen Accept-Homomorphismen.
Schwarze Pfeile bezeichnen Funktionen der Accept-Algebren TReg bzw. Lang.
Wir fassen zusammen:
• TReg ist die initiale Reg-Algebra,
• Lang ist eine Reg-Algebra und gleichzeitig die finale Accept-Algebra.
Die Finalität von Lang impliziert die Kommutativität der Diagramme (1) und (2).
Im Folgenden wollen wir zeigen, dass
• TReg eine Accept-Algebra ist und
• evalLang Accept-homomorph ist (Kommutativität der Diagramme (3) und (4)).
Wegen der Eindeutigkeit eines Accept-Homomorphismus von TReg in die finale AcceptAlgebra Lang folgt daraus die Gleichheit von evalLang und unfold TReg .
Im Abschnitt Reguläre Ausdrücke... haben wir Bool als Reg-Algebra definiert und darauf hingewiesen,
Bool
Lang
dass evalreg
(R) genau dann True ist, wenn zu L(R) = evalreg
(R) gehört, was wiederum genau dann
Lang
Lang
Bool
gilt, wenn accepting
(evalreg (R)) = True ist. Aus (3) folgt daher die Übereinstimmung von evalreg
mit accepting TReg .
Umgekehrt lässt sich jede auf einer Termalgebra (induktiv) definierte Funktion (wie z.B. accepting TReg )
als Auswertungsfunktion in eine passende Algebra darstellen!
65
Die Signatur Parse
Wir führen eine dritte Signatur ein und verwenden etwas Termersetzungstheorie (siehe z.B. [?, ?, P] Kapitel 5), um die Accept-Homomorphie von evalLang zu zeigen. Da
Termersetzung - neben dem λ-Kalkül - die Grundlage funktionaler, gleichungsbasierter
Programme bildet, ist es nützlich, auch in diese Theorie ein wenig hineinzuschnuppern.
Parse besteht aus den Sorten und Konstruktoren von Reg, den Sorten und Destruktoren
von Accept (wobei state mit reg gleichgesetzt wird) sowie den Funktionssymbolen bzw.
Konstanten
parse :: state word → Bool
δ ∗ :: state word → state
a :: → symbol
für alle a ∈ Z
True, False :: → Bool
= :: symbol symbol → Bool
∨, ∧ :: Bool Bool → Bool
if _then_else_ :: Bool Bool Bool → Bool
Die Basissignatur von Parse ist leer. Damit ist Parse eine Konstruktorsignatur und die
Termalgebra TParse die initiale Σ-Algebra.
66
Die Menge ParseE folgender Gleichungen zwischen Parse-Termen definiert einen Parser
für reguläre Ausdrücke:
Seien R, R0, w, x, y, z Variablen.
parse(R, w)
δ ∗(R, x : w)
δ ∗(R, [])
δ(∅, x)
δ(, x)
δ(x, y)
δ(R|R0, x)
δ(R · R0, x)
δ(plus(R), x)
accepting(∅)
accepting()
accepting(a)
accepting(R|R0)
accepting(R · R0)
accepting(plus(R))
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
accepting(δ ∗(R, w))
δ ∗(δ(R, x), w)
R
∅
∅
if x = y then else ∅
δ(R, x)|δ(R0, x)
δ(R, x) · R0|(if accepting(R) then δ(R0, x) else ∅)
δ(R, x)|δ(R, x) · plus(R)
False
True
False für alle a ∈ Z
accepting(R) ∨ accepting(R0)
accepting(R) ∧ accepting(R0)
accepting(R)
67
a=a
a=b
x ∨ True
x ∨ False
x ∧ True
x ∧ False
if True then y else z
if False then y else z
=
=
=
=
=
=
=
=
True für alle a ∈ Z
False für alle a, b ∈ Z mit a 6= b
True
x
x
False
y
z
Die kleinste Kongruenzrelation auf TParse , die alle Grundterminstanzen von ParseE enthält, heißt ParseE-Äquivalenz und wird mit ∼ParseE bezeichnet. Der daraus gebildete
Quotient
TParse /∼ParseE
ist initial in der Klasse aller Parse-Algebren A, die ParseE erfüllen, m.a.W. Existenz
und Eindeutigkeit eines Parse-Homomorphismus von TParse /∼ParseE nach A können nur
garantiert werden, wenn A ParseE erfüllt.
68
Hier kommt Termersetzung ins Spiel: Durch wiederholte Links-Rechts-Anwendung von
Gleichungen aus ParseE läßt sich jeder Parse-Grundterm in genau einen Reg-Term
transformieren. Zum Beweis von Existenz und Eindeutigkeit solcher Normalformen stellt
die Termersetzungstheorie sog. Konfluenzkriterien zur Verfügung. Für ParseE gelten
die folgenden drei:
• Jede Folge von Links-Rechts-Anwendungen von Gleichungen aus ParseE auf einen
Parse-Grundterm terminiert, d.h. irgendwann sind keine Gleichungen mehr anwendbar.
• Die linken Seiten der Gleichungen von ParseE sind paarweise überlappungsfrei.
• Auf einen Parse-Grundterm sind genau dann keine Gleichungen anwendbar, wenn t
aus Reg-Symbolen und Bool- oder symbol-Konstanten besteht.
Die letzten beiden Bedingungen lassen sich allein durch Ansehen von ParseE überprüfen.
Für die erste gibt es ein in Beweisen der Programmtermination oft verwendetes Kriterium,
das in unserem Fall folgendermaßen lautet: Die Argumente von nicht zu Reg gehörigen
Funktionen müssen auf der linken Seite jeder Gleichung von ParseE größer sein als auf
der rechten.
Wozu eindeutige Normalformen? Nun, wenn es sie gibt, dann bilden sie eindeutige Repräsentanten der Äquivalenzklassen von ∼ParseE und damit eine zu TParse /∼ParseE isomorphe
Parse-Algebra, die Normalformalgebra NF ParseE .
69
Der Parser parseTReg für reguläre Ausdrücke ist korrekt
Da das Reg-Redukt von NF Parse,ParseE mit TReg übereinstimmt, können auch die Funktionssymbole von Parse \ Reg in TReg interpretiert werden und zwar so, dass ParseE in
TReg gilt. Damit ist TReg eine Parse-Algebra, also insbesondere eine Accept-Algebra.
Bleibt zu zeigen, dass evalLang : TReg → Lang Accept-homomorph ist.
Die Gleichungen für parse und δ ∗ und lassen sich direkt in eine Interpretation dieser
Symbole in Lang überführen, die ParseE erfüllt. Damit ist die Funktion
Lang
evalParse
: TParse → Lang,
die jeden Parse-Grundterm in Lang auswertet, Parse-homomorph. Die surjektiven Funktionen nat : TParse → TParse /∼ParseE und nf : TParse → TReg , die jeden Parse-Grundterm
auf seine ParseE-Äquivalenzklasse bzw. seine eindeutige Normalform abbilden, sind
ebenfalls Parse-homomorph.
70
Lang
entDa Lang ParseE erfüllt, ist die ParseE-Äquivalenzrelation im Kern von evalParse
0
halten, d.h. für alle t, t ∈ TΣ gilt:
t ∼ParseE t0
=⇒
Lang
Lang 0
evalParse
(t) = evalParse
(t ).
Außerdem ist jeder Parse-Grundterm zu seiner Normalform ParseE-äquivalent. Also gilt
Lang
Lang
evalParse
(t) = evalParse
(nf (t)) = evalLang (nf (t)),
Lang
kurz: evalLang ◦ nf = evalParse
.
Lang
Die Anwendung des folgenden Lemmas auf f = evalParse
, g = evalLang und h = nf
liefert schließlich die Parse-Homomorphie und damit auch die Accept-Homomorphie von
evalLang .
Lemma Für alle Funktionen f, g, h gilt:
f = g ◦ h ∧ f, h homomorph ∧ h surjektiv
71
=⇒
g homomorph.
Damit können wir die Korrektheit von ParseE zeigen:
Sei R ∈ TReg und w = a1 . . . an ∈ Z ∗.
parseTReg (R, w) = True
⇐⇒
w ∈ evalLang (R) = L(R).
Beweis: Sei T = TReg .
unfold TBool =id
True = parse (R, w)
=
unfold T (parseT (R, w))
= unfold T (accepting T (δ ∗T (R, w))) = unfold T (accepting T (δ T (. . . (δ T (R, a1), . . . ), an))
T
unfold T Accept−hom.
=
accepting Lang (δ Lang (. . . (δ Lang (unfold T (R), a1), . . . ), an))
= accepting Lang (δ ∗Lang (unfold T (R), w)) = parseLang (unfold T (R), w)
unfold T =evalLang
=
parseLang (evalLang (R), w) ⇔ w ∈ evalLang (R).
o
Wie wir später sehen werden, führt ParseE direkt zu einer Haskell-Implementierung von
parseTReg .
72
Jeder Leseschritt des Parsers führt eine Transition des Automaten TReg aus. Diese ist jedoch nicht besonders effizient, da sie ihren Folgezustand nicht wie üblich in einer Tabelle
abliest, sondern aus dessen Vorgänger konstruiert. Deshalb werden endliche Automaten
bevorzugt, deren Zustände einfache Namen haben, z.B. natürliche Zahlen, so dass die
zugehörige Transitionsfunktion als Tabelle dargestellt und die Transitionen als Tabellenzugriffe implementiert werden können.
Um zu einem solchen, L(R) erkennenden Automaten zu gelangen, konstruiert man zunächst durch Auswertung von R in der Algebra Aut einen nichtdeterministischen Automaten, macht ihn deterministisch (z.B. mit der Haskell-Funktion makeDet; s.u.) und
minimiert die determininistische Version. Letzteres geht am schnellsten mit dem in Abschnitt 2.3 meines Übersetzerbau-Skripts beschriebenen Paull-Unger-Verfahren.
73
Das Prinzip, komplexe Daten durch Tabellenindizes zu ersetzen und eine ursprünglich rekursiv definierte Funktion als Tabelle zu implementieren, nennt man dynamische Programmierung. Es reduziert die Komplexität einer rekursiven Funktion oft erheblich und
zeichnet Automatenmodelle vor anderen aus. Auch wenn es am Ende nur auf endliche
Datenmengen anwendbar ist, kann am Anfang der Entwicklung auch eine unendliche
Menge stehen wie hier die Menge der regulären Ausdrücke. Ob der Übergang von einer
unendlichen zu einer endlichen Menge möglich ist, hängt von der jeweils zu realisierenden
Funktion ab. Hat ihr Kern, d.i. die Menge aller Argumentpaare mit gleichem Wert, nur
endlich viele Äquivalenzklassen, dann lässt sie sich als Tabelle implementieren. So ergeben
sich z.B. auch die Tabellen, die von LR-Parsern benutzt werden (s.u.).
74
In der Algebra spricht man von Quotientenbildung, wenn (viele) Daten durch (wenige)
Äquivalenzklassen ersetzt und Funktionen dementsprechend umformuliert werden. In der
Systemmodellierung ist Quotientenbildung das Mittel zur Abstraktion.
In diesem Sinne ist z.B. die kleinste Unteralgebra einer Σ-Algebra A eine Abstraktion
von TΣ und evalA : TΣ → A die Funktion, aus deren Kern der Quotient gebildet wird.
Man nennt den Quotienten auch die Faktorisierung von TΣ nach dem Kern von evalA.
Für das rechnergestützte Rechnen mit Abstraktionen ist entscheidend, ob und wie die
zugrundeliegende Äquivalenzrelation axiomatisiert wird, z.B. wie oben durch die Gleichungen von ParseE.
Der zur Abstraktion duale Entwicklungsschritt ist die Restriktion: Eine Datenmenge
A wird zunächst eingeschränkt auf eine Teilmenge B von A und dann abgeschlossen
unter Funktionsanwendungen, d.h. erweitert um alle Elemente von A, die durch endliche
Folgen von Konstruktor- bzw. Destruktoranwendungen auf Elemente von B aus diesen
entstehen. Restriktion heißt Bildung einer Unteralgebra wie z.B. der oben behandelten
Extraktion der von einem Startzustand erreichbaren Elemente einer Coalgebra.
75
Aus Sicht der Modellierung mittels Quotienten von Termalgebren stellt sich die Frage, ob
sich ParseE so zu einer Gleichungsmenge ParseE 0 erweitern lässt, dass TParse /∼ParseE 0
zu der Unteralgebra von Lang, die aus allen regulären Sprachen über Z besteht, isomorph ist. Dann wäre evalLang die natürliche Abbildung, die Parse-Terme auf ihre EÄquivalenzklassen abbildet. Um aus diesem Modell einen Parser (für reguläre Ausdrücke)
abzuleiten, bräuchten wir allerdings eine zu TParse / ∼ParseE 0 isomorphe Normalformalgebra. Sie müsste für jede reguläre Sprache L genau einen regulären Ausdruck R mit
L(R) = L enthalten.
Der entsprechende Parser müsste einen gegebenen Ausdruck zunächst in seine Normalform überführen und würde dann wie der obige weiterarbeiten, d.h. die Gleichungen von
ParseE anwenden. Die coalgebraische Charakterisierung von Lang wäre damit vollständig umgangen.
Leider gibt es aber nicht für jede reguläre Sprache eine eindeutige Normalform in TReg .
Selbst wenn es sie gäbe, wäre der Nachweis der Eindeutigkeit weitaus komplizierter als
der analoge, oben skizzierte, dass ∼ParseE -Äquivalenzenklassen eindeutige Repräsentanten in TReg haben. Einige Gleichungen von ParseE 0 müssten nämlich in Lang gültige
Eigenschaften regulärer Operatoren ausdrücken wie z.B. die - oben durch Coinduktion
gezeigte - Kommutativität von |. Solche Gleichungen erfüllen aber i.d.R. nicht die drei
o.g. Konfluenzkriterien.
76
` Vom LL-Parser zum LL-Compiler
parseTReg lässt sich auch für die Syntaxanalyse bewachter CFGs oder ECFGs (s.u.) verwenden. Sei G = (N, Z, P, BΣ, BA) eine bewachte CFG. Man braucht nur
• die Nichtterminale von G als Konstanten der Sorte reg zu den Signaturen Reg und
Σ sowie
• eine Destruktorversion des Gleichungssystems von G zu ParseE
hinzufügen. Reg(N ) und Parse(N ) bezeichnen die um N erweiterten Signaturen Reg
bzw. Parse.
Um die Korrektheit von parseTReg(N ) ähnlich der von parseTReg begründen zu können, ist
es nicht ratsam, GLS (G) in unveränderter Form zu ParseE hinzuzunehmen. Die obige
Argumentation basiert nämlich u.a. auf der Endlichkeit aller Grundtermreduktionen mit
Hilfe von ParseE.
Da G bewacht ist, kann aber GLS (G) so in Gleichungen für δ und accepting überführt
werden, dass die o.g. Kriterien für Existenz und Eindeutigkeit von Normalformen und die
Gültigkeit in Lang erhalten bleiben. Zusammen mit ParseE bilden sie das Gleichungssystem ParseE(G). Sie lauten wie folgt:
77
Sei x eine Variable.
δ(A, a) = δ(A, a) = ∅
δ(A, x)
= δ(R, x)
accepting(A) = accepting(R)
für alle A ∈ BS und a ∈ BAA
“a ∈ BAA” zu prüfen ist Aufgabe des Scanners.
für alle A ∈ BS und a ∈ Z \ BAA
für alle A = R ∈ GLS (G)
Die Bewachtheit von G garantiert die Termination (der Interpretationen) von δ und
accepting (in TReg(N )): Zwar vergrößert die Anwendung einer der letzten beiden Gleichungen den regulären Ausdruck, auf den δ rekursiv angewendet wird. Die im Folgenden anwendbaren Gleichungen von ParseE reduzieren aber die Argumente von δ bzw.
accepting auf , ∅ oder solche der Form xR mit x < A.
78
Beispiel SAB Aus
GLS (SAB) = (S = aB | bA | ∧
A = aS | bAA
∧
B = bS | aBB)
ergibt sich die Gleichungsmenge ParseE(SAB):
{δ(S, x) = δ(aB|bA|, x) | x ∈ {a, b}} ∪ {accepting(S) = accepting(aB|bA|)} ∪
{δ(A, x) = δ(aS|bAA, x) | x ∈ {a, b}} ∪ {accepting(A) = accepting(aS|bAA)} ∪
{δ(B, x) = δ(bS|aBB, x) | x ∈ {a, b}} ∪ {accepting(B) = accepting(bS|aBB)}
o
Wie wir später sehen werden, kann bei der Haskell-Implementierung von ParseE(G)
auf die Umformung von GLS (G) in Gleichungen für δ verzichtet werden, da die lazyevaluation-Strategie, nach der Haskell Terme auswertet, sicherstellt, dass auch die
Gleichungen von GLS (G) nicht unendlich oft angewendet werden.
79
Man sieht sofort, dass die Konstruktion von ParseE(G) nicht funktioniert, wenn G unbewacht ist. Eine besonders unangenehme mögliche Konsequenz von Unbewachtheit ist
+
die Linksrekursion, d.h. die Existenz von Ableitungen der Form A −→G Av. Sie kann
dazu führen, dass die Auswertung eines Terms parse(A, w) nicht terminiert, weil dieser
Term in einem Zwischenergebnis seiner eigenen Auswertung vorkommt.
Zum Glück kann jede linksrekursive Grammatik G durch Einführung neuer Nichtterminale folgendermaßen in eine nicht-linksrekursive transformiert werden:
Ordne N total. Eliminiere alle Regeln der Form A → A.
• Ersetze jede Regel A → Bα mit A > B, für die eine Regel B → β existiert, durch
die Regel A → βα. Wiederhole diesen Schritt, solange seine Bedingung erfüllt ist.
+
Am Ende gibt es für alle A ∈ N mit Ableitungen der Form A −→G Av eine Regel
der Form A → Aα.
• Ersetze jedes Regelpaar (A → Aα, A → β), bei dem A kein Präfix von β ist, durch
die drei Regeln A → βA0, A0 → αA0, A0 → , wobei A0 ein neues Nichtterminal ist.
Ähnlich lassen sich auch andere Verletzungen der Bewachtheit eliminieren. Man braucht
dabei nicht bis zur Greibach-Normalform gehen, die nur ein einziges Terminal in jedem Summanden der rechten Seite einer Regel zulässt. Bewachtheit ist eine schwächere
Forderung: Jeder Summand muss lediglich mit einem Terminal beginnen oder mit übereinstimmen.
80
Der Parser parseTReg(N ) für bewachte CFGs korrekt
Sei β : N → ℘(Z ∗) die kleinste Lösung von GLS (G) in Lang mit β(A) = BAA für
alle A ∈ BS. Durch ALang =def β(A) für alle A ∈ N wird Lang zu einer Reg(N )Algebra, die die Gleichungen von GLS (G), also auch ParseE(G) erfüllt. Die Gültigkeit
von ParseE in Lang wurde im vorigen Abschnitt gezeigt. Für die restlichen Gleichungen
von ParseE(G) ist die Gültigkeit schnell gezeigt:
Sei A ∈ BS. Dann gilt für alle a ∈ Z,
evalLang (δ(A, a)) = δ Lang (evalLang (A), a) =δ Lang (β(A), a) =
= evalLang () falls a ∈ BAA,
Lang
∗
δ
(BAA, a) = {w ∈ Z | aw ∈ BAA} =
∅ = evalLang (∅) sonst.
Sei A = R ∈ GLS (G). Dann gilt für alle a ∈ Z,
evalLang (δ(A, a)) = δ Lang (evalLang (A), a) = δ Lang (ALang , a) =
δ Lang (β(A), a) = δ Lang (evalLang (β)(R), a) = evalLang (β)(δ(R, a))
und
evalLang (accepting(A)) = accepting Lang (evalLang (A)) = accepting Lang (ALang ) =
accepting Lang (β(A)) = accepting Lang (evalLang (β)(R)) = evalLang (β)(accepting(R)).
81
Die Erweiterung von Reg, Parse und ParseE zu Reg(N ), Parse(N ) bzw. ParseE(G) erhält also alle Eigenschaften von Lang, die im vorigen Abschnitt zur Accept-Homomorphie
von evalLang und damit zur Korrektheit des Parsers parseTReg für reguläre Ausdrücke geführt haben.
Also ist auch parseTReg(N ) korrekt:
Sei R ∈ TReg(N ) und w ∈ Z ∗.
parseTReg(N ) (R, w) = True
⇐⇒
w ∈ evalLang (R) = L(R).
Daraus folgt nämlich insbesondere für alle A ∈ N :
parseTReg(N ) (A, w) = True
w ∈ evalLang (A) = β(A) = L(G)A.
⇐⇒
parseTReg(N ) ist ein LL-Parser. LL-Parser lesen ein Wort w von links nach rechts und
versuchen dabei (implizit) eine Linksableitung von w zu konstruieren. Da dies dem schrittweisen, mit der Wurzel (dem “Startsymbol”) beginnenden Aufbau eines Ableitungsbaums
entspricht, werden LL-Parser auch top-down-Parser genannt.
82
Erweiterte kontextfreie Grammatiken
Der Fixpunktsatz für CFGs bleibt gültig, wenn man ihn mit einer entsprechend angepassten Bewachtheitsbedingung auf erweiterte kontextfreie Grammatiken (ECFGs)
verallgemeinert, deren rechte Regelseiten beliebige reguläre Ausdrücke sein dürfen. ECFGs
werden für Dokumenttypdefinitionen (DTDs) verwendet und kommen dort u.a. als Baumgrammatiken vor, deren rechten Regelseiten Terme sind.
ECFGs mögen sich zur reinen Syntaxdefinition von Dokumenttypen eignen und damit
auch für Parser, die auf die Erkennung syntaktischer Korrektheit beschränkt sind. Als
Grundlage eines echten Übersetzers, der seine Eingaben nicht nur prüft, sondern im Erfolgsfall in eine mehr oder weniger komplexe Datenstruktur transformiert, sind sie jedoch
wenig hilfreich, weil sie keine abstrakte Syntax haben und damit das Bindeglied zwischen
Quell- und Zielsprache fehlt, ohne das die semantische Äquivalenz zwischen Quell- und
Zielobjekten nicht formalisierbar, also auch nicht überprüfbar ist.
83
Die Frage, warum Dokumenttypen überhaupt durch ECFGs und nicht durch CFGs und
deren adäquate algebraische Interpretation dargestellt werden, lässt sich einfach beantworten: Weil die meisten Leute, die XML und DTDs geschaffen haben, in der Theorie
formaler Sprachen (und der Datenbanktheorie) zuhause sind und den - auch aus Sicht
der programmiersprachlichen Realisierung - adäquateren algebraischen Zugang schlichtweg ignorieren. Eine Folge davon ist die unpräzise Typisierung von Daten, die entsteht,
wenn konkrete und abstrakte Syntax gleichgesetzt werden, indem Typkonstruktoren (wie
z.B. die Bildung des Typs der Listen von Elementen eines anderen Typs) auf reguläre
Operatoren reduziert (wie z.B. den Sternoperator) reduziert werden.
84
LL-Compiler mit Backtracking
Sei G = (N, Z, P, BΣ, BA) eine bewachte CFG.
parseTReg(N ) bildet Eingabewörter auf Boolesche Werte ab. Demgegenüber wurde parseG
im Abschnitt Korrektheit monadischer Parser als eine Funktion nach M (TΣ(G)) eingeführt. Dort hatten wir bereits bemerkt, dass die zwei Schritte eines Compilers:
• die rekursive Konstruktion von Syntaxbäumen mit parseG und
• deren rekursive Auswertung in einer G-Algebra Ziel mit evalZiel
zu einer N -sortierten Funktion
parse
G
∗
compileZiel
:
Z
−→
M (TΣ(G))
G
(returnZiel ◦ evalZiel )∗
−→
M (Ziel),
die Eingabewörter direkt in Elemente von Ziel übersetzt, zusammengezogen werden können.
Da diese Funktion ihre Argumente schrittweise liest, also auch schrittweise ihre Werte
erzeugt, definieren wir sie mit Hilfe von Schleifenfunktionen
compStepZiel
: Z ∗ → M (ZielA × Z ∗),
A
A ∈ N ∪ Z \ BA
die nach dem Lesen eines akzeptierbaren Präfixes der Eingabe neben Zielobjekten die in
der Eingabe verbliebenen Restwörter ausgibt.
Für alle x ∈ Z \ BA definieren wir Zielx als {x}.
85
compileG wird wie folgt auf compStep zurückgeführt: Für alle A ∈ N ,
∗
Ziel
compileZiel
G,A = (λ(a, w).return(a)) ◦ guard(λ(a, w).w = ) ◦ compStepA .
Scheitert die Ausführung von compStepZiel (w), d.h. endet sie mit dem Wert mzero,
diesen Wert zurück, weil f ∗(mzero) = mzero für alle f :
dann gibt auch compileZiel
G
A → M (B) gilt. Letzteres passiert auch, wenn compStepZiel nicht scheitert, aber nur
Elemente (a, v) ∈ Ziel × Z ∗ mit v = liefert. Aus den von compStepZiel berechneten
Paaren (a, v) ∈ Ziel × Z ∗ mit v 6= filtert compileZiel
die Ziel-Komponenten heraus.
G
Sei
P = { p11 = (A1 → v11), . . . , p1n1 = (A1 → v1n1 ),
...
pk1 = (Ak → vk1), . . . , pknk = (Ak → vknk )}.
Für alle 1 ≤ i ≤ k und w ∈ Z ∗,
compStepZiel
Ai (w) =def mplus(trypi1 (w),
mplus(trypi2 (w),
...,
mplus(trypi(n −1) (w),
i
trypini (w)))),
86
wobei für alle 1 ≤ j ≤ ni trypij : Z ∗ → M (ZielAi × Z ∗) folgendermaßen definiert ist:


return(fpZiel
, w)
falls vij = 
ij



(λ(a1, w).
falls ∃ n > 0 :




(λ(a2, w).
vij = x1 . . . xn ∈ (N ∪Z \BA)n





...
i1 < · · · < im




(λ(an−1, w).
{i1, . . . , im} = {i | ai ∈ N \Z}



(λ(an, w).
trypij (w) =
(ai1 , . . . , aim ), w)))∗
return(fpZiel


ij


∗

(compStepZiel

xn (w)))


∗
Ziel

(w)))
(compStep

x

n−1



...



∗

(compStepZiel

x2 (w)))


(compStepZiel
x1 (w)).
Für alle x ∈ BS ∪Z \BA, z ∈ Z und w ∈ Z ∗,
if z ∈ BAA then return(z, w) else mzero falls A ∈ BS,
compStepZiel
(zw)
=
def
x
if z = x then return(z, w) else mzero
sonst,
Ziel
compStepx () =def mzero.
87
compStepAi (w) prüft sequentiell alle Regeln für Ai auf ihre Anwendbarkeit auf w: Gibt es
ein Präfix von w, das aus der rechten Seite einer Regel für Ai ableitbar ist? Da alle Funktionen trypij auf dieselbe Eingabe w angewendet werden, handelt es sich hier um einen
Backtrack-Parser. Demgegenüber sind klassische LL-Parser auf LL(k)-Grammatiken
zugeschnitten, die verlangen, dass bei jedem Anwendungsschritt die ersten k Zeichen von
w bestimmen, ob und, wenn ja, welche - einzig (!) mögliche - Regel angewendet werden
muss. Diese findet der Parser in der sog. Aktionstabelle. Im nächsten Abschnitt werden
wir eine entsprechende Aktionstabelle für LR-Parser definieren.
Sei p = (A → x1 . . . xn). tryp(w) ruft sequentiell compStepx1 (w1), . . . , compStepxn (wn)
auf, wobei w1 = w gilt und für alle 1 < i ≤ n wi das von compStepxi−1 (wi−1) nicht
verarbeitete Suffix von wi−1 ist. Gibt es kein aus xi ableitbares (bzw. mit xi übereinstimmendes Präfix von wi, dann scheitert compStepxi (wi) und gibt mzero aus, was aufgrund
der Gültigkeit der Gleichung f ∗(mzero) = mzero (siehe Abschnitt Korrektheit monadischer Parser) dazu führt, dass tryp selbst diesen Wert zurückgibt. Andernfalls berechnet
tryp alle Argumente ai1 , . . . , aim der Interpretation fpZiel des aus p gebildeten Funktionssymbols fp und gibt das Element fpZiel (ai1 , . . . , aim ) von ZielA zurück.
compStep arbeitet nach dem Prinzip des recursive descent, d.h. steigt - implizit - im
Ableitungsbaum ab, baut sein Ergebnis aber rekursiv von den Blättern her auf.
88
T
Der Parser parseG = compileGΣ(G) ist korrekt.
Dazu ist zu zeigen, dass die Funktionsdiagramme (1) und (2) im Abschnitt Korrektheit
monadischer Parser kommutieren, d.h.
(1)
∀ w ∈ Z ∗ \L(G) : parseG(w) = mzero,
(2)
parseG ◦ evalWord (G) = return.
Da die Definition von parseG auf den Hilfsfunktionen cst =def compStepTΣ(G) und try
aufbaut, müssen wir Eigenschaften dieser Funktionen formulieren und beweisen, aus denen (1) und (2) folgen. Alle drei Funktionen verwenden die monadischen Konstanten
bzw. Operatoren bind, return, mplus und mzero. Um Aussagen über die Funktionen zu
beweisen, müssen wir deshalb neben den Gleichungen für die Extension _∗ (siehe Korrektheit monadischer Parser) einen logischen Operator voraussetzen, der Eigenschaften
von Elementen einer Menge A in Eigenschaften von Elementen von M (A) überführt. Da
er den modallogischen Box-Operator 2 verallgemeinert, wollen wir ihn ebenso nennen:
2A : (A → Bool) → (M (A) → Bool)
Er muss die folgenden Gleichungen erfüllen: Für alle f : A → Bool, m, m1, m2 ∈ M (A)
89
und m3, m4 ∈ M (B),
2A(f )(m) ⇒ (λx.(if f (x) then m3 else m4)∗(m) = (λx.m3)∗(m),
2A(f )(mzeroA) = False,
2A(f )(mplusA(m1, m2)) = 2A(f )(m1) ∨ 2A(f )(m2).
Mit Hilfe von _∗- und 2-Gleichungen und der Definition von parseG:
parseG = (λ(a, w).return(a))∗ ◦ guard(λ(a, w).w = ) ◦ cst
lassen sich (1) und (2) auf die folgenden Eigenschaften von cst zurückführen:
(3)
∀ w ∈ Z ∗ \L(G) : 2(λ(a, w).w 6= )(cst(w)),
(4)
∀ t ∈ TΣ(G) : cst(evalWord (G)(t)) = return(t).
********
90
Beispiel SAB Die Regeln
p1 = S → aB, p2 = S → bA, p3 = S → ,
p4 = A → aS, p5 = A → bAA, p6 = B → bS, p7 = B → aBB
von SAB liefern nach obigem Schema den folgenden LL-Compiler von L(SAB) nach Ziel:
compStep_S(w) = mplus(try_p1(w),mplus(try_p2(w),mplus(try_p3(w))))
where try_p1(w) = bind(compStep_a(w),
λ(x,w).bind(compStep_B(w),
λ(c,w).return(f_p1^Ziel(c),w)))
try_p2(w) = bind(compStep_b(w),
λ(x,w).bind(compStep_A(w),
λ(c,w).return(f_p2^Ziel(c),w)))
try_p3(w) = return(f_p3^Ziel,w)
compStep_A(w) = mplus(try_p4(w),try_p5(w))
where try_p4(w) = bind(compStep_a(w),
λ(x,w).bind(compStep_S(w),
λ(c,w).return(f_p4^Ziel(c),w)))
try_p5(w) = bind(compStep_b(w),
λ(x,w).bind(compStep_A(w),
λ(c,w).bind(compStep_A(w),
λ(d,w).return(f_p5^Ziel(c,d),w))))
91
compStep_B(w) = mplus(try_p6(w),compStep_p7(w))
where try_p6(w) = bind(compStep_b(w),
λ(x,w).bind(compStep_S(w),
λ(c,w).return(f_p6^Ziel(c),w)))
try_p7(w) = bind(compStep_a(w) of
λ(x,w).bind(compStep_B(w),
λ(c,w).bind(compStep_B(w),
λ(d,w).return(f_p7^Ziel(c,d),w))))
compStep_a(zw) = if z = a then return(a,w) else mzero
compStep_b(zw) = if z = b then return(b,w) else mzero
compStep_a() = mzero
compStep_b() = mzero
92
` LR-Parser und -Compiler
LR-Parser lesen ein Wort w von links nach rechts und versuchen dabei (implizit) eine Rechtsreduktion von w, d.h. die Umkehrung einer Rechtsableitung zu konstruieren.
Da dies dem schrittweisen, mit den Blättern (die das Eingabewort bilden) beginnenden
Aufbau eines Ableitungsbaums entspricht, werden LR-Parser auch bottom-up-Parser
genannt.
Im Gegensatz zur obigen Funktion compile ist die entsprechende Übersetzungsfunktion
eines LL-Compilers iterativ. Die Umkehrung der Ableitungsrichtung (Reduktion statt
Ableitung) macht es möglich, die Compilation durch einen Automaten, also eine iterativ
definierte Funktion, zu steuern.
Auf der anderen Seite kann compile alle nicht-linksrekursiven CFGs verarbeiten, ein
LR-Compiler aber nur LR(k)-Grammatiken (s.u.). In eine bottom-up-Analyse lässt sich
auch nicht so einfach Backtracking einbauen wie in eine top-down-Analyse, so dass die
Möglichkeit, auf diese Weise die Klasse der von LR-Parsern analysierbaren Sprachen zu
erweitern, praktisch entfällt.
93
Außerdem ist die Konstruktion des o.g. Automaten, der die LR-Analyse steuert, – wie
wir unten sehen werden – nicht ganz trivial. Da gebräuchliche LR-Parsergeneratoren wie
yacc ihren Benutzern diese Arbeit abnehmen, ist das natürlich nur dann ein Argument
gegen die LR-Analyse, wenn die jeweils vorliegende CFG keine LR(k)-Grammatik ist.
Voraussetzungen
Sei G = (N, Z, P, BΣ, BA) eine CFG. Wir setzen voraus, dass N \ BS ein Startsymbol
S enthält und dieses nicht auf der rechten Seite einer Regel von G auftritt.
∗
Sei −→ der reflexiv-transitive Abschluss von
{(wAv, wϕv) | w, v ∈ (N ∪ Z)∗ ∧ (A → ϕ ∈ P ∨ (A ∈ BS ∧ ϕ ∈ BAA))}.
Ablauf der LR-Analyse auf Ableitungsbäumen
S
φ
ε
vw
Ableitungsbäume Eingabe
v
w
Ableitungsbäume Eingabe
94
vw
ε
Ableitungsbaum Eingabe
first- und follow-Wortmengen
Sei k > 0, α ∈ (N ∪ Z)∗ und A ∈ N .
first k (α) bezeichnet die k-elementigen Präfixe der aus α ableitbaren terminalen Wörter.
follow k (A) bezeichnet die k-elementigen Präfixe der in einem aus S ableitbaren Wort auf
A folgenden terminalen Wörter.
∗
∗
first k (α) = {w ∈ Z k | ∃v ∈ Z ∗ : α −→ wv} ∪ {w ∈ Z <k | α −→ w}
∗
follow k (A) = {w ∈ Z k | ∃u, v ∈ Z ∗ : S −→ uAwv} ∪
∗
{w ∈ Z <k | ∃u ∈ Z ∗ : S −→ uAw}
first(α) = first 1(α)
follow (A) = follow 1(A)
Induktive Definition von first(α), α ∈ (N ∪ Z)∗
∈ first()
(1)
x ∈ Z ⇒ x ∈ first(xα) (2)
(A → α) ∈ P ∧ x ∈ first(αβ) ⇒ x ∈ first(Aβ) (3)
95
Eine CFG G, die die o.g. Voraussetzungen erfüllt, heißt LR(k)-Grammatik, wenn das
Vorauslesen von k noch nicht verarbeiteten Eingabesymbolen genügt, um zu entscheiden,
ob ein weiteres Zeichen gelesen oder eine Reduktion durchgeführt werden soll und, wenn
ja, welche.
Beispiel Die CFG G = ({S, A}, {∗, b, c}, P, ∅, ∅) mit den Regeln
S
A
A
A
→
→
→
→
A
A∗A
b
c
ist für kein k eine LR(k)-Grammatik.
Nach der Reduktion der Eingabe auf A ∗ A liegt ein shift-reduce-Konflikt vor. Soll
man zuerst weiterlesen (das Präfix ∗b der Resteingabe) und dann b zu A reduzieren oder
sofort die Regel A ∗ A zu A reduzieren? Das Vorauslesen von k Zeichen der Resteingabe
nimmt uns diese Entscheidung nicht ab.
o
Der folgende LR-Parser liefert genau dann eindeutige Ergebnisse, wenn G eine LR(1)Grammatik ist.
96
Der LR-Parser in zwei Schritten
Schritt 1
parse1 : (N ∪ Z)∗ × Z ∗ → Bool
Anforderung
∗
parse1(ϕ, w) = True ⇐⇒ S → ϕw,
Definition
Sei γ, α ∈ (N ∪ Z \ BA)∗, x ∈ Z und w ∈ Z ∗.
∗
parse1(γα, xw) = parse1(γαx, w) falls S → γAv, A → αβ ∈ P,
shift
β 6= , x ∈ first(βfollow (A))
∗
parse1(γα, w) = parse1(γA, w) falls S → γAv, A 6= S,
reduce
A → α ∈ P, first(w) ∈ follow (A)
parse1(ϕ, )
= True
falls S → ϕ ∈ P
accept
parse1(ϕ, w)
= False
sonst
error
97
Sei ϕ ∈ (N ∪ Z)∗, M = (N ∪ Z \ BA)∗ und die vierstellige Relation
state(ϕ) ⊆ N × (M ∪ BA) × M × (Z ∪ {})
definiert durch:
∗
(A, α, β, first(w)) ∈ state(γα) ⇐⇒ S → γAw ∧ A → αβ ∈ P
Die Bedingungen in der Definition von parse1 werden durch Abfragen der Form
(A, α, β, x) ∈ state(ϕ) ersetzt:
parse1(γα, xw) = parse1(γαx, w) falls (A, α, β, y) ∈ state(γα),
shift
β 6= , x ∈ first(βy)
parse1(γα, w) = parse1(γA, w) falls (A, α, , first(w)) ∈ state(γα), reduce
A 6= S
parse1(ϕ, )
= True
falls (S, α, , ) ∈ state(ϕ)
accept
parse1(ϕ, w)
= False
sonst
error
98
Schritt 2
Die Menge Q = {state(ϕ) | ϕ ∈ (N ∪Z)∗} wird zur Zustandsmenge des LR-Automaten
für G.
N ∪ Z ist sein Eingabealphabet, state() ist sein Startzustand, alle Zustände state(ϕ)
∗
mit S −→ ϕ bilden die Menge seiner Endzustände und seine Übergangsfunktion ist für
alle x ∈ N ∪ Z wie folgt definiert:
δ(state(ϕ), x) ⇐⇒ state(ϕx)
Im Parser wird ϕ durch eine Liste von Zuständen ersetzt, die er wie einen Keller verwaltet:
parse1 wird so zu
parse2 : Q∗ × Z ∗ → Bool
umgeformt, dass für alle x1, . . . , xn ∈ N ∪ Z
parse1(x1 . . . xn, w) = parse2(state(x1 . . . xn)state(x1 . . . xn−1) . . . state(x1)state(), w)
gilt.
99
Sei q, qi ∈ Q, qs ∈ Q∗, x ∈ Z und w ∈ Z ∗.
parse2(q : qs, xw)
parse2(q1 : · · · : q|α|
parse2(q : qs, )
parse2(qs, w)
= parse2(δ(q, x) : q : qs, w)
shift
falls (A, α, β, y) ∈ q, β 6= ,
x ∈ first(βy)
: q : qs, w) = parse2(δ(q, A) : q : qs, w)
reduce
falls A 6= S, (A, α, , first(w)) ∈ q1
= True
falls (S, α, , ) ∈ q
accept
= False
sonst
error
100
Induktive Definition von Q und δ
S → α ∈ P ⇒ (S, , α, ) ∈ q0
(1)
(A, α, Bβ, x) ∈ q ∧ B → γ ∈ P ∧ y ∈ first(βx) ⇒ (B, , γ, y) ∈ q
(2)
(A, α, xβ, y) ∈ q ⇒ (A, αx, β, y) ∈ δ(q, x) (3)
Aktionstabelle
actLR : Q × (Z ∪ {}) → P ∪ {shift, error }

falls (A, α, β, y) ∈ q, β 6= , x ∈ first(βy)
 shift
actLR (q, x) =
A → α falls A 6= S, (A, α, , x) ∈ q

error sonst
S → α falls (S, α, , ) ∈ q
actLR (q, ) =
error sonst
101
Unter Verwendung von δ und actLR erhält man schließlich eine kompakte Definition von
parse2:
Sei q, qi ∈ Q, qs ∈ Q∗, x ∈ Z und w ∈ Z ∗.
parse2(q : qs, xw)
parse2(q1 : · · · : q|α|
parse2(q : qs, )
parse2(q : qs, w)
= parse2(δ(q, x) : q : qs, w)
falls actLR (q, x) = shift
: q : qs, w) = parse2(δ(q, A) : q : qs, w)
falls actLR (q1, first(w)) = A → α
= True
falls actLR (q, ) = S → α
= False
falls actLR (q, first(w)) = error
102
Beispiel SAB2
G = ({S, A, B}, {c, d, ∗}, P, ∅, ∅)
P = {S → A, A → A ∗ B, A → B, B → c, B → d}
LR-Automat für G (δ wird auch goto-Tabelle genannt)
q0 = {(S, , A, ), (A, , A ∗ B, ), (A, , B, ), (A, , A ∗ B, ∗), (A, , B, ∗),
(B, , c, ), (B, , d, ), (B, , c, ∗), (B, , d, ∗)}
q1 = δ(q0, A) = {(S, A, , ), (A, A, ∗B, ), (A, A, ∗B, ∗)}
q2 = δ(q0, B) = {(A, B, , ), (A, B, , ∗)}
q3 = δ(q0, c) = {(B, c, , ), (B, c, , ∗)} = δ(q5, c)
q4 = δ(q0, d) = {(B, d, , ), (B, d, , ∗)} = δ(q5, d)
q5 = δ(q1, ∗) = {(A, A∗, B, ), (A, A∗, B, ∗),
(B, , c, ), (B, , d, ), (B, , c, ∗), (B, , d, ∗)}
q6 = δ(q5, B) = {(A, A ∗ B, , ), (A, A ∗ B, , ∗)}
A B c d mul
0 1 2 3 4
1
5
5
6 3 4
103
Aktionstabelle
c
d
∗
q0
q1
q2
q3
q4
shift error
error error error
shift error
error error error
error shift A → B B → c B → d
error S → A A → B B → c B → d
Ein Parserlauf
parse2(q0, c∗d)
= parse2(q3q0, ∗d)
= parse2(q2q0, ∗d)
= parse2(q1q0, ∗d)
= parse2(q5q1q0, d)
= parse2(q4q5q1q0, )
= parse2(q6q5q1q0, )
= parse2(q1q0, )
= True
wegen
wegen
wegen
wegen
wegen
wegen
wegen
wegen
q5
q6
shift
error
shift
error
error A → A ∗ B
error A → A ∗ B
actLR (q0, c) = shift und δ(q0, c) = q3
actLR (q3, ∗) = B → c und δ(q0, B) = q2
actLR (q2, ∗) = A → B und δ(q0, A) = q1
actLR (q1, ∗) = shift und δ(q1, ∗) = q5
actLR (q5, d) = shift und δ(q5, d) = q4
actLR (q4, ) = B → d und δ(q5, B) = q6
actLR (q6, ) = A → A ∗ B und δ(q0, A) = q1
actLR (q1, ) = S → A
104
LR-Compiler
Wir erweitern parse2 zum LR-Compiler
compile : Q∗ × Z ∗ × alg ∗ → M (alg),
der alle Wörter von L(G) parallel zu ihrer Analyse in Elemente einer beliebigen G-Algebra
alg übersetzt. Im Gegensatz zum LL-Compiler übersetzt der LR-Compiler nur Worte, die
aus dem Startsymbol S ableitbar sind. Er startet mit q0 = state() im ansonsten leeren
Zustandskeller und mit der leeren Liste von alg-Werten.
Anforderung an compile
(
compile([q0], w, []) =
Word (G)
return(evalalg (t)) =⇒ evalS
(t) = w
Word (G)
mzero
=⇒ ∀ t ∈ TΣ(G),S : evalS
(t) 6= w
Definition von compile
Sei q, qi ∈ Q, qs ∈ Q∗, x ∈ Z, w ∈ Z ∗, ai ∈ alg, as ∈ alg ∗ und ||α|| die Anzahl der
Nichtterminale von α ∈ (N ∪ Z \ BA)∗.
compile(q : qs, xw, as) = compile(δ(q, x) : q : qs, w, as) falls actLR (q, x) = shift
105
compile(q1 : · · · : q|α| : q : qs, w, as ++[a1, . . . , a||α||]) =
alg
(a1, . . . , a||α||)])
compile(δ(q, A) : q : qs, w, as ++[fA,α
falls actLR (q1, first(w)) = A → α
Ist alg = TΣ(G), dann ist as ++[a1, . . . , a||α||] eine Baumliste, die ein Reduktionsschritt
folgendermaßen verändert:
a1
a||α||
as
fA -> α
a1
a||α||
as
alg
compile(q : qs, , [a1, . . . , a||α||]) = return(fS→α
(a1, . . . , a||α||)
falls actLR (q, ) = S → α
compile(qs, w, as)
= mzero
sonst
106
Zum Nachweis der obigen Anforderung an compile müssen die Trägermengen der GTermalgebra um Syntaxbäume mit Variablen aus N und die Trägermengen der Wortalgebra von G um Wörter mit Nichtterminalen erweitert werden.
Dann lässt sich zeigen, dass die folgende Bedingung eine Invariante der compile-Schleife
ist und die obige Anforderung an compile impliziert:
compile(state(w0A1w1 . . . Anwn) : qs, w, [a1, . . . , an]) = return(evalalg (t(a1, . . . , an)))
Word (G)
(t(x1, . . . , xn)) = w0A1w1 . . . Anwnw.
=⇒
evalS
107
Beispiel SAB2
Hier ist noch einmal der obige Parserlauf mit schrittweiser Konstruktion des Syntaxbaums
(als Klammerausdruck im drittem Argument von parse). Demnach ist die Zielalgebra hier
die SAB2-Termalgebra.
parse([c,mul,d],[0],
[])
parse([mul,d], [3,0],
[])
parse([mul,d], [2,0],
[f_{B,c}])
parse([mul,d], [1,0],
[f_{A,B}(f_{B,c})])
parse([d],
[5,1,0], [f_{A,B}(f_{B,c})])
parse([],
[4,5,1,0],[f_{A,B}(f_{B,c})])
parse([],
[6,5,1,0],[f_{A,B}(f_{B,c}), f_{B,d}])
parse([],
[1,0],
[f_{A,A*B}(f_{A,B}(f_{B,c}),f_{B,d})])
parse([],
[1,0],
[f_{S,A}(f_{A,A*B}(f_{A,B}(f_{B,c}),f_{B,d}))])
108
Auf den nächsten drei Folien stehen die Aktions- und goto-Tabelle der folgenden Grammatik G für imperative straight-line-Programme (also ohne Konditionale und Schleifen) in
der Form, in der sie der in C geschriebene Compilergenerator yacc (“yet another compilercompiler”) erzeugt.
prog
statems
assign
exp
->
->
->
->
statems exp .
statems assign ; | ID : = exp
exp + exp | exp * exp | (exp) | NUMBER | ID
Auf der vierten Folie werden diese Regeln um C-Code erweitert, der die Funktionen einer
G-Algebra definiert. In diesem Fall handelt es sich um einen Interpreter der durch G definierten Programmiersprache. Die entsprechende Algebra für JavaLight heißt javaState
und wird unten im Abschnitt Ein Haskell-Datentyp für Algebren definiert.
109
110
111
112
113
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)
Die Hierarchie der CFG-Klassen
114
eindeutige kontextfreie
det. kf. Sprachen
=LR(1)-Spr.
=SLR(1)-Spr.
=LALR(1)-Spr.
LL(k)Spr.
Sprachen
Die Hierarchie der entsprechenden Sprachklassen
115
` Haskell: Funktionen
Neben
• Produkten A1 × . . . × An (Haskell-Notation: (A1,...,An)) und
• Summen A1 + · · · + An (Datentypen mit n Konstruktoren; s.u.)
werden in Haskell auch Funktionen als Objekte betrachtet und z.B. als λ-Abstraktionen
\p -> e dargestellt. Programmtechnisch gesprochen ist p der formale Parameter - also
eine einzelne Variable oder, allgemeiner, ein Muster (pattern; s.u.) - der dargestellten
Funktion und e ihr Rumpf. Ihre Definition ist eine Gleichung:
f = \p -> e
Meistens wird jedoch die äquivalente applikative Definition bevorzugt:
f p = e
Damit ist auch f eine Repräsentation der durch sie definierten Funktion.
Funktionen, die andere Funktionen als Argumente oder Werte haben, heißen Funktionen
höherer Ordnung. In Haskell ist schon die Addition eine solche:
116
(+) :: Int -> (Int -> Int)
oder (+) :: Int -> Int -> Int
wegen der Rechtsassoziativität von ->
Sie wird zunächst als Präfixfunktion verwendet:
((+) 5) 6
oder
(+) 5 6
wegen der Linksassoziativität der Applikation (s.u.)
Ohne die runden Klammern kann sie wie üblich infix verwendet werden:
5 + 6
Eine Funktion eines Typs a -> b -> c, deren Name mit einem Buchstaben beginnt,
kann ebenfalls infix verwendet werden, muss dann aber in ‘ eingeschlossen werden, z.B.:
mod :: Int -> Int -> Int
mod 11 5 oder 11 `mod` 5
Die Infixnotation wird auch verwendet, um die in einer Funktion eines Typs
a -> b -> c enthaltenen Sektionen zu benennen. Z.B. sind die Ausdrücke
(+ 5)
(+) 5
(11 +)
mod 5
(`mod` 5)
(11 `mod`)
Funktionen des Typs Int -> Int. Hier sind alle Klammern zwingend!
117
Der Ausdruck, der die Anwendung einer λ-Abstraktion auf ein Argument repräsentiert,
heißt λ-Applikation, z.B. (\p -> e)(e’). Die Applikation führt nur dann zu einem Ergebnis, wenn der Ausdruck e’ das Pattern e matcht (s.u.). Die Applikationsfunktion
($) :: (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)...)))
ist äquivalent zu
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.
Man benutzt ihn u.a., um in einer applikativen Definition Argumentvariablen einzusparen:
f a b = g (h a) b ist äquivalent zu f a = g $ h a ist äquivalent zu f = g . h
f a b = g $ h a b ist äquivalent zu f a = g . h a ist äquivalent zu f = (g.).h
118
Weitere nützliche Funktionsgeneratoren, -transformatoren und kombinatoren:
const :: a -> b -> a
const a _ = a
konstante Funktion
update :: Eq a => (a -> b) -> a -> b -> a -> b
update f a b a' = if a == a' then b else f a'
Funktionsupdate
flip :: (a -> b -> c) -> b -> a -> c
flip f b a = f a b
Vertauschung der Argumente
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
119
(***) :: (a -> b) -> (a -> c) -> a -> (b,c)
(f *** g) x = (f x,g x)
Funktionsprodukt
(&&&), (|||) :: (a -> Bool) -> (a -> Bool) -> a -> Bool
Lifting Boolescher Operationen
(f &&& g) x = f x && g x
(f ||| g) x = f x || g x
Ein Typ mit Typvariablen wie z.B. a -> b -> c heißt polymorph. Andernfalls ist er
monomorph. Eine Funktion mit poly/monomorphem Typ heißt poly/monomorph. Ein
Typ t heißt Instanz eines Typs u, wenn man t durch Ersetzung der Typvariablen aus u
erhält.
120
` Haskell: Listen
data [a] = []
|
a : [a]
(++) :: [a] -> [a] -> [a]
(a:s)++s' = a:(s++s')
_++s
= s
(!!) :: [a] -> Int -> a
(a:_)!!0
= a
(_:s)!!n | n > 0 = s!!(n-1)
head :: [a] -> a
head (a:_) = a
tail :: [a] -> [a]
tail (_:s) = s
init :: [a] -> [a]
init [_]
= []
init (a:s) = a:init s
last :: [a] -> a
last [a]
= a
last (_:s) = last s
121
take
take
take
take
:: Int -> [a] -> [a]
0 _
= []
n (x:s) | n > 0 = x:take (n-1) s
_ []
= []
drop
drop
drop
drop
:: Int -> [a] -> [a]
0 s
= s
n (_:s) | n > 0 = drop (n-1) s
_ []
= []
takeWhile :: (a -> Bool) -> [a] -> [a]
takeWhile f (x:s) = if f x then x:takeWhile f s else []
takeWhile f _
= []
dropWhile :: (a -> Bool) -> [a] -> [a]
dropWhile f s@(x:s') = if f x then dropWhile f s' else s
dropWhile f _
= []
updList :: [a] -> Int -> a -> [a]
updList s i a = take i s++a:drop (i+1) s
122
map :: (a -> b) -> [a] -> [b]
map f (a:s) = f a:map f s
map _ _
= []
[f1 a,f2 a,...,fn a]
ist äquivalent zu
map ($ a) [f1,f2,...,fn]
zipWith :: (a -> b -> c) -> [a] -> [b] -> [c]
zipWith f (a:s) (b:s') = f a b:zipWith f s s'
zipWith _ _ _
= []
zip :: [a] -> [b] -> [(a,b)]
zip = zipWith $ \x y -> (x,y)
repeat :: a -> [a]
repeat a = a:repeat a
replicate :: Int -> a -> [a]
replicate n a = take n $ repeat a
iterate :: (a -> a) -> a -> [a]
iterate f a = a:iterate f $ f a
123
Beispiel Listenteilung
splitAt
splitAt
splitAt
splitAt
:: Int -> [a] -> ([a],[a])
0 s
= ([],s)
_ []
= ([],[])
n (a:s) | n > 0 = (a:s1,s2)
where (s1,s2) = splitAt (n-1) s
span :: (a -> Bool) -> [a] -> ([a],[a])
span f s@(x:s') = if f x then (x:s1,s2) else ([],s)
where (s1,s2) = span f s'
span _ []
= ([],[])
Beispiel Listenintervall
sublist
sublist
sublist
sublist
sublist
:: [a] -> Int -> Int -> [a]
(a:_) 0 0
=
(a:s) 0 j | j > 0
=
(_:s) i j | i > 0 && j > 0 =
_ _ _
=
124
[a]
a:sublist s 0 $ j-1
sublist s (i-1) $ j-1
[]
Beispiel Pascalsches Dreieck = Binomialkoeffizienten
pascal :: Int -> [Int]
pascal 0 = [1]
pascal n = zipWith (+) (0:s) $ s++[0] where s = pascal $ n-1
binom :: Int -> Int -> Int
binom n k = product[1..n]/(product[1..k]*product[1..n-k])
Für alle n ∈ N und n ≤ k gilt:
binom(n)(k) = pascal(n)!!k
125
Strings sind Listen von Zeichen
Strings werden als Listen von Zeichen betrachtet, m.a.W.: die Typen String und [Char]
sind identisch.
words :: String -> [String] und unwords :: [String] -> String zerlegen bzw.
konkatenieren Strings, wobei Leerzeichen, Zeilenumbrüche ('\n') und Tabulatoren (’
’)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.
126
Beschränkte Iterationen (for-Schleifen) entsprechen oft Listenfaltungen.
Faltung einer Liste von links her
f
f
f
f
f
a
b1
b2
b3
b4
b5
foldl :: (a -> b -> a) -> a -> [b] -> a
foldl f a (b:s) = foldl f (f a b) s
foldl _ a _
= a
foldl1 :: (a -> a -> a) -> [a] -> a
foldl1 f (a:s) = foldl f a s
sum
and
minimum
concat
=
=
=
=
foldl (+) 0
foldl (&&) True
foldl1 min
foldl (++) []
product
or
maximum
concatMap
127
=
=
=
=
foldl (*) 1
foldl (||) False
foldl1 max
(concat .) . map
Parallele Faltung zweier Listen von links her
f
f
f
f
f
a
b1
c1
b2
c2
b3
c3
b4
c4
b5
c5
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
listsToFun :: b -> [a] -> [b] -> a -> b
listsToFun = fold2 update . const
Mit listsToFun können funktionale Objekte mit endlichem Wertebereich in Haskell implementiert werden:
bs!!i falls i bzgl. a = as!!i maximal ist,
listsToFun b as bs a =
b
sonst.
128
Faltung einer Liste von rechts her
f
f
f
f
f
b1
b2
b3
b4
foldr :: (b -> a -> a) -> a -> [b] -> a
foldr f a (b:s) = f b $ foldr f a s
foldr _ a _
= a
129
b5
a
Listenlogik
any :: (a -> Bool) -> [a] -> Bool
any = (or .) . map
elem :: a -> [a] -> Bool
elem a = any (a ==)
all :: (a -> Bool) -> [a] -> Bool
all = (and .) . map
notElem :: a -> [a] -> Bool
notElem a = all (a /=)
filter :: (a -> Bool) -> [a] -> [a]
filter f (a:s) = if f a then a:filter f s else filter f s
filter f _
= []
130
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' = [uncurry f a | a <- zip s s']
filter f s
= [a | a <- s, f a]
allgemein:
[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.
Mit der Listenkomprehension lassen sich u.a. sehr einfach Relationen (Teilmengen eines
kartesischen Produktes) A1 × . . . × An definieren, sofern die Ai als Listen gegeben sind,
z.B. die folgende Menge aller Elemente von A1 × A2 × A3, die p : A1 × A2 × A3 → Bool
erfüllen:
rel = [(a,b,c) | a <- a1, b <- a2, c <- a3, p(a,b,c)]
131
` Haskell: Datentypen
Beispiel Arithmetische Ausdrücke
data Expr = Con Int | Var String | Sum [Expr] | Prod [Expr] |
Expr :- Expr | Int :* Expr | Expr :^ Int
Z.B. lautet der Ausdruck 5*11+6*12+x*y*z als Objekt vom Typ Expr folgendermaßen:
Sum [5 :* Con 11,6 :* Con 12,Prod [Var "x",Var "y",Var "z"]]
132
Die Haskell-Programme zum Datentyp Expr stehen auch in der Datei Expr.hs.
Funktionen mit Expr-Argumenten werden in Abhängigkeit von deren Muster (pattern)
definiert:
Beispiel Symbolische Differentiation
diff
diff
diff
diff
diff
:: String -> Expr -> Expr
x (Con _)
= zero
x (Var y)
= if x == y then one else zero
x (Sum es) = Sum $ map (diff x) es
x (Prod es) = Sum $ map f [0..length es-1]
where f i = Prod $ updList es i $ diff x $ es!!i
diff x (e :- e') = diff x e :- diff x e'
diff x (i :* e) = i :* diff x e
diff x (e :^ i) = i :* Prod [diff x e,e:^(i-1)]
zero = Con 0
one = Con 1
133
Beispiel Expr-Interpreter
type Store = String -> Int (Belegung der Variablen)
evalE
evalE
evalE
evalE
evalE
evalE
evalE
evalE
:: Expr -> Store -> Int
(Con i) _
= i
(Var x) st
= st x
(Sum es) st = sum $ map (flip evalE st) es
(Prod es) st = product $ map (flip evalE st) es
(e :- e') st = evalE e st - evalE e' st
(i :* e) st = i * evalE e st
(e :^ i) st = evalE e st ^ i
134
Beispiel Parser für reguläre Ausdrücke
infixr 7 :*
infixr 6 :+
data Reg = Empty | Eps | Sym String | Reg :+ Reg | Reg :* Reg |
Plus Reg
Die Gleichungen für parse, δ ∗, δ und accepting im Abschnitt Ein Parser für reguläre
Ausdrücke liefern Haskell-Definitionen dieser Funktionen:
parse :: Reg -> [String] -> Bool
parse r = accepting . dstar r
dstar :: Reg -> [String] -> Reg
dstar r (x:w) = dstar (d r x) w
dstar r _
= if accepting r then Eps else Empty
135
d
d
d
d
d
d
d
:: Reg -> String -> Reg
Empty _
= Empty
Eps _
= Empty
(Sym x) y = if x == y then Eps else Empty
(r1:+r2) x = d r1 x:+d r2 x
(r1:*r2) x = d r1 x:*r2:+if accepting r1 then d r2 x else Empty
(Plus r) x = d r x:+d r x:*Plus r
accepting
accepting
accepting
accepting
accepting
accepting
accepting
:: Reg -> Bool
Empty
= False
Eps
= True
(Sym _) = False
(r1:+r2) = accepting r1 || accepting r2
(r1:*r2) = accepting r1 && accepting r2
(Plus r) = accepting r
136
Um die durch Anwendungen von d erzeugten regulären Ausdrücke nicht zu groß werden
zu lassen, reduzieren wir sie nach jeder Anwendung gemäß in Lang gültiger Gleichungen:
dstar :: Reg -> [String] -> Reg
dstar r (x:w) = dstar (reduce $ d r x) w
dstar r _
= if accepting r then Eps else Empty
reduce
reduce
reduce
reduce
reduce
reduce
reduce
reduce
:: Reg -> Reg
(r:+Empty) = r
(Empty:+r) = r
(r:*Eps)
= r
(Empty:*r) = Empty
(r:*Empty) = Empty
(Eps:*r)
= r
r
= r
137
Im Abschnitt Vom LL-Parser zum LL-Compiler haben wir das Gleichungssystem einer
bewachten CFG G in weitere Gleichungen für δ und accepting überführt und damit parse
zu einem Parser für L(G) gemacht. Wenn wir ihn in Haskell implementieren, können wir
uns die Umformung der Gleichungen sparen. GLS (G) ist nämlich selbst eine HaskellDefinition, nicht von Funktionen, sondern von Konstanten. Da man ihre Gleichungen
unendlich oft anwenden kann, repräsentieren sie unendliche Objekte.
Beispiel SAB
s,a,b :: Reg
s = Sym "a":*b:+Sym "b":*a:+Eps
a = Sym "a":*s:+Sym "b":*a:*a
b = Sym "b":*s:+Sym "a":*b:*b
Z.B. wird mit dem Aufruf
parse s $ words "a b a b b a b a a b"
geprüft, ob das Wort ababbabaab zu L(SAB)s gehört.
138
(1)
Da die Auswertung von (1) von links nach rechts voranschreitet, werden zunächst nicht
die Gleichungen für parse, dstar und d angewendet, was u.a. zum Teilausdruck
d s $ Sym "a".
(2)
führt. Da es keine Gleichung für d gibt, die auf diesen Teilausdruck anwendbar ist, wendet der Haskell-Interpreter - seiner lazy-evaluation-Strategie folgend - erst jetzt die
Gleichung für s an und ersetzt damit (2) durch
d (Sym "a":*b:+Sym "b":*a:+Eps) $ Sym "a".
(3)
Auf (3) können wieder Gleichungen für d angewendet werden.
Da nur in solchen Fällen Gleichungen von GLS (SAB) angewendet werden, terminiert
z.B. die Auswertung der folgenden Ausdrücke mit dem jeweils rechts stehenden Ergebnis:
parse
parse
dstar
dstar
s
s
s
s
$
$
$
$
words
words
words
words
"a
"a
"a
"a
b
b
b
b
a
a
a
a
b
b
b
b
b
b
b
b
a
a
a
a
b
b
b
b
a
b
a
b
a
a
a
a
b"
b"
b"
b"
;
;
;
;
True
False
Eps
Empty
Diese und verwandte Haskell-Programme stehen in der Datei Lazy.hs.
139
` Typklassen
stellen Bedingungen an die Instanzen einer Typvariablen. Die Bedingungen bestehen in
der Existenz bestimmter Funktionen und bestimmten Beziehungen zwischen ihnen.
class Eq a where (==), (/=) :: a -> a -> Bool
(/=) = (not .) . (==)
Eine Instanz einer Typklasse besteht aus den Instanzen ihrer Typvariablen sowie Definitionen der von ihr geforderten 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!
140
Beispiele
insert :: Eq a => a -> [a] -> [a]
insert x s@(y:s') = if x == y then s else y:insert x s'
insert x _
= [x]
remove :: Eq a => a -> [a] -> [a]
remove x = filter (/= x)
union :: Eq a => [a] -> [a] -> [a]
union = foldl $ flip insert
Mengenvereinigung
diff :: Eq a => a -> [a] -> [a]
diff = foldl $ flip remove
Mengendifferenz
inter :: Eq a => [a] -> [a] -> [a]
inter s = filter (`elem` s)
Mengendurchschnitt
unionMap :: Eq a => (a -> [b]) -> [a] -> [b]
unionMap = (foldl union [] .) . map
concatMap für Mengen
141
Unterklassen
Typklassen können wie Objektklassen in OO-Sprachen andere Typklassen erben. Die
jeweiligen Oberklassen werden vor dem Erben vor dem Pfeil => aufgelistet.
class Eq a => Ord a where (<), (<=), (>=), (>) :: a -> a -> Bool
max, min :: a -> a -> a
max x y = if x >= y then x else y
min x y = if x <= y then x else y
class Ord a => Bounded a where minBound, maxBound :: a
quicksort :: Ord a => [a] -> [a]
quicksort (x:s) = quicksort [z | z <- s, z <= x]++x:
quicksort [z | z <- s, z > x]
quicksort s
= s
142
Beispiel Mergesort
mergesort :: Ord a => [a] -> [a]
mergesort (x:y:s) = merge (mergesort $ x:s1) (mergesort $ y:s2)
where (s1,s2) = split s
mergesort s
= s
split :: [a] -> ([a],[a])
split (x:y:s) = (x:s1,y:s2) where (s1,s2) = split s
split s
= (s,[])
merge :: Ord a => [a] -> [a] -> [a]
merge s1@(x:s3) s2@(y:s4) = if x <= y then x:merge s3 s2
else merge s3 s2
merge [] s
= s
merge s _
= s
143
Beispiel Binäre Bäume
data Bintree a = Empty | Branch (Bintree a) a (Bintree a)
subtrees :: Bintree a -> [Bintree a]
subtrees (Branch left _ right) = [left,right]
subtrees _
= []
leaf :: a -> Bintree a
leaf = flip (Branch Empty) Empty
insertTree :: Ord a => a -> Bintree a ->
insertTree a t@(Branch t1 b t2) | a == b
| a < b
| True
insertTree a _
Bintree a
= t
= Branch (insertTree a t1) b t2
= Branch t1 b $ insertTree a t2
= leaf a
instance Eq a => Ord (Bintree a) where
Empty <= _
= True
_ <= Empty
= False
Branch t1 a t2 <= Branch t3 b t4 = t1 <= t3 && a == b && t2 <= t4
144
Vor der Ein- bzw. Ausgabe von Daten eines Typs T wird automatisch die T-Instanz der
Funktion read bzw. show aufgerufen, die zur Typklasse Read a bzw. Show a gehört:
type ReadS a = String -> [(a,String)]
type ShowS
= String -> String
class Read a where
read :: String -> a
read s = case [x | (x,t) <- reads s, ("","") <- lex t] of
[x] -> x
[] -> error "PreludeText.read: no parse"
_
-> error "PreludeText.read: ambiguous parse"
reads :: ReadS a
reads = readsPrec 0
readsPrec :: Int -> ReadS a
class Show a where
show :: a -> String
show x = shows x ""
145
shows :: a -> ShowS
shows = showsPrec 0
showsPrec :: Int -> a -> ShowS
Das Argument einer Funktion vom Typ ReadS a ist der jeweilige Eingabestring s. Ihr
Wert ist eine Liste von Paaren, bestehend aus dem als Objekt vom Typ a erkannten
Präfix von s und der jeweiligen Resteingabe (= Suffix von s).
Das Argument einer Funktion vom Typ ShowS ist der an die Ausgabe eines Objektes vom
Typ a anzufügende String. Ihr Wert ist die dadurch entstehende Gesamtausgabe.
lex :: ReadS String ist eine Standardfunktion, die ein evtl. aus mehreren Zeichen bestehendes Symbol erkennt, vom Eingabestring abspaltet und das Paar (Symbol,Resteingabe)
ausgibt (siehe PreludeHugs.hs).
Wird deriving Read bzw. deriving Show ans Ende der Definition eines Datentyps T
gesetzt, dann werden T-Objekte in der Darstellung eingelesen bzw. ausgegeben, in der
sie auch in Programmen vorkommen.
Will man das Ein- bzw. Ausgabeformat von T-Objekten selbst bestimmen, dann muss
die T-Instanz von readsPrec bzw. show oder showsPrec definiert werden.
146
Beispiel Binäre Bäume
readTree :: Read a => ReadS (Bintree a)
readTree s = [(Branch left a right,s6) | (a,s1) <- reads s,
("(",s2) <- lex s1,
(left,s3) <- readTree s2,
(",",s4) <- lex s3,
(right,s5) <- readTree s4,
(")",s6) <- lex s5] ++
[(Branch left a Empty,s4) | (a,s1) <- reads s,
("(",s2) <- lex s1,
(left,s3) <- readTree s2,
(")",s4) <- lex s3] ++
[(leaf a,s1) | (a,s1) <- reads s]
showTree0
showTree0
showTree0
showTree0
:: Show
(Branch
(Branch
(Branch
a => Bintree a
Empty a Empty)
left a Empty)
left a right)
-> String
= show a
= show a++'(':showTree0 left++")"
= show a++'(':showTree0 left++',':
showTree0 right++")"
147
oder effizienter, weil iterativ:
showTree :: Show a => Bintree a -> ShowS
showTree (Branch Empty a Empty) = shows a
showTree (Branch left a Empty) = shows a . ('(':) .
showTree left . (')':)
showTree (Branch left a right) = shows a . ('(':) .
showTree left . (',':) .
showTree right . (')':)
showTree0 und showTree sind partielle Funktionen, die nur auf den mit read eingelesenen Bäumen definiert sind!
instance Read a => Read (Bintree a) where readsPrec _ = readTree
instance Show a => Show (Bintree a) where show = showTree0
oder
showsPrec _ = showTree
rb1 = read "5(7(3, 8),6( 2) ) " :: Bintree Int
; 5(7(3,8),6(2))
rb2 = reads "5(7(3, 8),6( 2) ) " :: [(Bintree Int,String)]
; [(5(7(3,8),6(2))," "),(5,"(7(3, 8),6( 2) ) ")]
rb3 = read "5(7(3,8),6(2))hh"
:: Bintree Int
; Exception: ...: no parse
rb4 = reads "5(7(3,8),6(2))hh"
:: [(Bintree Int,String)]
; [(5(7(3,8),6(2)),"hh"),(5,"(7(3,8),6(2))hh")]
148
Als Listen dargestellte Mengen
newtype Set a = Set {list :: [a]}
newtype kann data ersetzen, falls der
Datentyp genau einen Konstruktor hat
list = Zugriffsfunktion auf das Argument von Set
instance Eq a => Eq (Set a)
instance Eq a => Ord (Set a)
where s == s' = s <= s' && s' <= s
where s <= s' = all (`elem` list s')
$ list s
instance Show a => Show (Set a) where show = show . list
Set[1,2,3] <= Set[3,4,2,5,1,99]
Set[1,2,3] >= Set[3,4,2,5,1,99]
;
;
mkSet :: Eq a => [a] -> Set a
mkSet = Set . union []
True
False
eliminiert Mehrfachvorkommen
149
Beispiel
Vom nichtdeterministischen (erkennenden) Automaten mit -Übergängen zum
deterministischen Automaten ohne -Übergänge
Wir transformieren Automaten mit ganzzahligen Zuständen, Startzustand 0 und Endzustand 1, wie sie z.B. die Auswertung regulärer Ausdrücke in der Reg-Algebra NDA (s.u.)
liefert.
makeDet :: (Int -> String -> [Int])
-> ([Int], [Int] -> String -> [Int], [Int] -> Bool)
makeDet delta = (epsHull [0], delta', (1 `elem`))
where delta' qs x = epsHull $ unionMap (flip delta x) qs
epsHull qs = if all (`elem` qs) qs'
then qs else epsHull $ qs `union` qs'
where qs' = unionMap (flip delta "eps") qs
150
` Rund um Expr
Eine Expr-Instanz von Show
instance Show Expr where showsPrec _ = showE
showE :: Expr -> ShowS
showE (Con i)
= (show i++)
showE (Var x)
= (x++)
showE (Sum es) = foldShow '+' es
showE (Prod es) = foldShow '*' es
showE (e :- e') = ('(':) . showE e . ('-':) . showE e' . (')':)
showE (n :* e) = ('(':) . (show n++) . ('*':) . showE e . (')':)
showE (e :^ n) = ('(':) . showE e . ('^':) . (show n++) . (')':)
foldShow :: Char -> [Expr] -> ShowS
foldShow op (e:es) = ('(':) . showE e . foldr trans id es . (')':)
where trans e h = (op:) . showE e . h
foldShow _ _
= id
Prod[Con 3,Con 5,x,Con 11]
; (3*5*x*11)
Sum [11 :* (x :^ 3),5 :* (x :^ 2),16 :* x,Con 33] ; ((11*(x^3))+(5*(x^2))+(16*x)+33)
151
Beispiel Normalisierung arithmetischer Ausdrücke
Die folgende Funktion reduce wendet die folgenden Gleichungen auf einen arithmetischen
Ausdruck an:
0+e=e
e+e=2∗e
(m ∗ e) + (n ∗ e) = (m + n) ∗ e
m ∗ (n ∗ e) = (m ∗ n) ∗ e
reduce
reduce
reduce
reduce
reduce
reduce
reduce
reduce
reduce
reduce
:: Expr -> Expr
(e :- e')
(0 :* e)
(1 :* e)
(i :* Con j)
(i :* (j :* e))
(e :^ 0)
(e :^ 1)
(Con i :^ j)
((e :^ i) :^ j)
=
=
=
=
=
=
=
=
=
0∗e=0
e ∗ e = e2
em ∗ en = em+n
(em)n = em∗n
reduce $ Sum [e,(-1):*e']
zero
reduce e
Con $ i*j
reduce $ (i*j) :* e
one
reduce e
Con $ i^j
reduce $ e :^ (i*j)
152
1∗e=e
e0 = 1
e1 = e
reduce (Sum es)
= mkSum $ map mkScal dom ++ if c == 0 then [] else [Con c]
where (c,dom,sc) = foldl f (0,[],const 0) $ map reduce es
f st (Con 0)
= st
f (c,dom,sc) (Con i) = (c+i,dom,sc)
f (c,dom,sc) (i:*e) = (c,insert e dom,update sc e $ sc e+i)
f st e
= f st (1:*e)
mkScal e = reduce $ sc e :* e
mkSum [] = zero
mkSum [e] = e
mkSum es = Sum es
reduce (Prod es) = mkProd $ map mkExpo dom ++ if c == 1 then [] else [Con c]
where (c,dom,ex) = foldl f (1,[],const 0) $ map reduce es
f st (Con 1)
= st
f (c,dom,ex) (Con i) = (c*i,dom,ex)
f (c,dom,ex) (e:^i) = (c,insert e dom,update ex e $ ex e+i)
f st e
= f st (e:^1)
mkExpo e = reduce $ e :^ ex e
mkProd [] = one
mkProd [e] = e
mkProd es = Prod es
reduce e = e
reduce übersetzt Ausdrücke in Ausdrücke und ist damit in der Compiler-Terminologie ein
front-end-Optimierer. Diese optimieren Quell- oder Zwischencode, back-end-Optimierer
optimieren Zielcode.
153
Beispiel Expr-Compiler von Syntaxbäumen in Assemblerprogramme und deren Ausführung in einer Kellermaschine
Das Prinzip der Erzeugung von Zielcode aus Syntaxbäumen lässt sich am Beispiel
des Datentyps Expr (dessen Elemente ja nichts anderes als Syntaxbäume sind) und einer
kleinen Assemblersprache mit Kellerbefehlen ganz gut demonstrieren.
Die Zielkommandos sind durch folgenden Datentyp gegeben:
data StackCom = Push Int | Load String | Sub | Add Int | Mul Int | Pow
Die (virtuelle) Zielmaschine besteht aus einem Keller für ganze Zahlen und einem Speicher (Menge von Variablenbelegungen) wie beim Interpreter arithmetischer Ausdrücke.
Genaugenommen beschreibt ein Typ für diese beiden Objekte nicht diese selbst, sondern
die Menge ihrer möglichen Zustände:
type StateE = ([Int],Store)
154
Die Bedeutung der einzelnen Zielkommandos wird durch einen Interpreter auf State
definiert:
executeCom
executeCom
executeCom
executeCom
executeCom
executeCom
executeCom
:: StackCom -> StateE ->
(Push a) (stack,store) =
(Load x) (stack,store) =
Sub st
=
(Add n) st
=
(Mul n) st
=
Pow st
=
StateE
(a:stack,store)
(store x:stack,store)
executeOp (foldl1 (-)) 2 st
executeOp sum n st
executeOp product n st
executeOp (foldl1 (^)) 2 st
Die Ausführung eines arithmetischen Kommandos besteht in der Anwendung der jeweiligen arithmetischen Operation auf die obersten n Kellereinträge, wobei n die Stelligkeit
der Operation ist:
executeOp :: ([Int] -> Int) -> Int -> StateE -> StateE
executeOp f n (stack,store) = (f (reverse as):bs,store)
where (as,bs) = splitAt n stack
155
Die Ausführung einer Befehlsliste besteht in der Hintereinanderausführung ihrer Elemente:
executeE :: [StackCom] -> StateE -> StateE
executeE = flip $ foldl $ flip executeCom
Die Übersetzung eines arithmetischen Ausdrucks von seiner Baumdarstellung in eine
Befehlsliste erfolgt wie die Definition aller Funktionen auf Expr-Daten induktiv:
compileE
compileE
compileE
compileE
compileE
compileE
compileE
compileE
:: Expr -> [StackCom]
(Con i)
= [Push i]
(Var x)
= [Load x]
(Sum es) = concatMap compileE es++[Add $ length es]
(Prod es) = concatMap compileE es++[Mul $ length es]
(e :- e') = compileE e++compileE e'++[Sub]
(i :* e) = Push i:compileE e++[Mul 2]
(e :^ i) = compileE e++[Push i,Pow]
156
Beginnt die Ausführung des Zielcodes eines Ausdrucks e im Zustand ([],store), dann
endet sie im Zustand ([a],store), wobei a der Wert von e unter der Variablenbelegung
store ist:
executeE (compileE e) ([],store) = ([evalE e store],store).
Die Gleichung lässt sich durch Induktion über den Aufbau von e zeigen. Wählt man
executeE als Zielinterpreter, dann beschreibt sie im Sinne des Abschnitts CompilerKorrektheit die Korrektheit von compileE.
157
Die Ausdrücke 5*11+6*12+x*y*z und 11*x^3+5*x^2+16*x+33 als Expr-Objekte
Z.B. übersetzt compileE die obigen Expr-Objekte in folgende Kommandosequenzen:
0:
1:
2:
3:
4:
5:
6:
7:
Push 5
Push 11
Mul 2
Push 6
Push 12
Mul 2
Load "x"
Load "y"
8: Load "z"
9: Mul 3
10: Add 3
0:
1:
2:
3:
4:
5:
6:
7:
Push 11
Load "x"
Push 3
Pow
Mul 2
Push 5
Load "x"
Push 2
8:
9:
10:
11:
12:
13:
14:
Pow
Mul 2
Push 16
Load "x"
Mul 2
Push 33
Add 4
Eine Testumgebung für evalE, executeE und compileE wird im Abschnitt Monadische
String-Parser vorgestellt.
158
` Datentypen mit Attributen
data DT a1 ... am = Constructor_1 e_11 ... e_1n_1 | ... |
Constructor_k e_k1 ... e_kn_k
e_11,...,e_kn_k sind beliebige Typausdrücke, die aus Typkonstanten (z.B. Int), Typvariablen (a1,...,am) und Typkonstruktoren (Produktbildung, Listenbildung, Datentypen) zusammengesetzt sind. Kommt DT selbst in einem dieser Typausdrücke vor,
spricht man von einem rekursiven Datentyp.
Die Elemente von DT a1 ... am sind alle funktionalen Ausdrücke, die aus den Konstruktoren Constructor_1,...,Constructor_k und Elementen von Instanzen von
a1,...,am zusammengesetzt sind. Als Funktion hat Constructor_i den Typ
e_i1 -> ... -> e_in_i -> DT a1 ... am.
Da Konstruktoren ihre Argumente nicht verändern, sondern nur kapseln, möchte man oft
auf diese direkt zugreifen. Das erreicht man durch Einführung von Attributen, eines für
jede Argumentposition des Konstruktors. In der Definition von DT wird Constructor_i
e_i1 ... e_in_i ersetzt durch
Constructor_i {attribute_i1 :: e_i1, ..., attribute_in_i :: e_in_i}.
159
Z.B. lassen sich die aus anderen Programmiersprachen bekannten Recordtypen und
Objektklassen als Datentypen mit genau einem Konstruktor, aber mehreren Attributen
implementieren.
Wie ein Konstruktor, so ist auch ein Attribut eine Funktion. Als solche hat attribute_ij
den Typ
attribute_ij :: DT a1 ... am -> e_ij
Attribute sind also invers zu Konstruktoren. Man nennt sie deshalb heute auch Destruktoren.
attribute_ij (Constructor_i t1 ... tn_i)
hat den Wert
tj.
Die Objektdefinition
obj = Constructor_i t1 ... tn_i
ist äquivalent zu
obj = Constructor_i {attribute_i1 = t1, ..., attribute_in_i = tn_i}.
Update von attribute_ij:
obj' = obj {attribute_ij = t}
160
Rekursive Definitionen funktionaler Attribute sind verboten. Folglich kann attribute_ij
nur dann innerhalb des Ausdrucks tk verwendet werden, wenn es unter diesem Namen
noch ein anderes Objekt gibt, das nicht als Attribut deklariert wurde.
In O’Haskell gibt es ein eigenes Typkonstrukt (struct) für Recordtypen:
struct Record = attribute_1
...
attribute_n
Objektdefinition:
record struct attribute_1 =
...
attribute_n =
Attributaufruf:
a = record.attribute_i
:: e1
:: en
t1
tn
Auch hier sind rekursive Attributdefinitionen verboten. Allerdings dürfen funktionale
Attribute zumindest applikativ definiert werden wie z.B. in attribute_i p = t.
Derselbe Konstruktor oder dasselbe Attribut darf nicht in mehreren Daten- oder Recordtypen verwendet werden!
161
` Ein Haskell-Datentyp für Algebren
Sei Σ = (S, F, BΣ, BA) eine Signatur, BΣ = (BS, BF, BΣ0, BA0),
S \ BS = {s1, . . . , sk },
F \ BF = { f11 :: w11 → s1, . . . , f1n1 :: w1n1 → s1,
...
fk1 :: wk1 → sk , . . . , fknk :: wknk → sk }.
Jede Σ-Algebra kann als Element des folgenden polymorphen Datentyps implementiert
werden:
data SigAlg s1 . . . sk = SigAlg { (f11 :: w11 → s1)0, . . . , (f1n1 :: w1n1 → s1)0,
...
(fk1 :: wk1 → sk )0, . . . , (fknk :: wknk → sk )0 }
wobei
(f :: → s)0
=def f :: s
(f :: s1 . . . sn → s)0 =def f :: s1 → . . . → sn → s
Die Sorten von S \ BS sind also als Typvariablen implementiert, die durch die (Typen
der) entsprechenden Trägermengen der jeweiligen Algebra instanziiert werden.
Die Funktionssymbole von F \ BF sind als Attribute implementiert, die durch die entsprechenden Operationen der jeweiligen Algebra instanziiert werden.
162
Beispiel Die Klasse der JavaLight-Algebren als Datentyp
data JavaAlg commands command intE sumSect product prodSect factor boolE conjunct
literal =
JavaAlg {seq :: command -> commands -> commands, skip :: command,
embedC :: command -> commands,
assign :: String -> intE -> command,
cond :: boolE -> commands -> commands -> command,
cond1, loop :: boolE -> commands -> command,
sum :: product -> sumSect -> intE,
addSect, subSect :: product -> sumSect -> sumSect,
emptyS :: sumSect,
prod :: factor -> prodSect -> product,
mulSect, divSect :: factor -> prodSect -> prodSect,
emptyP :: prodSect,
embedI :: Int -> factor, var :: String -> factor,
encloseI :: intE -> factor,
or :: conjunct -> boolE -> boolE,
embedA :: conjunct -> boolE,
and :: literal -> conjunct -> conjunct,
embedL :: literal -> conjunct,
embedB :: Bool -> literal,
rel :: String -> intE -> intE -> literal,
not :: literal -> literal,
encloseB :: boolE -> literal}
163
Die JavaLight-Termalgebra
data Commands = Seq Command Commands | EmbedC Command
data Command = Skip | Assign String IntE | Cond BoolE Commands Commands |
Cond1 BoolE Commands | Loop BoolE Commands
data IntE
= Sum Product SumSect
data SumSect = AddSect Product SumSect | SubSect Product SumSect | EmptyS
data Product = Prod Factor ProdSect
data ProdSect = MulSect Factor ProdSect | DivSect Factor ProdSect | EmptyP
data Factor
= EmbedI Int | Var String | EncI IntE
data BoolE
= Or Conjunct BoolE | EmbedA Conjunct
data Conjunct = And Literal Conjunct | EmbedL Literal
data Literal = EmbedB Bool | Rel String IntE IntE | Not Literal | EncB BoolE
javaTerm :: JavaAlg Commands Command IntE SumSect Product ProdSect Factor
BoolE Conjunct Literal
javaTerm = JavaAlg EmbedC Seq Skip Assign Cond Cond1 Loop Sum AddSect SubSect
EmptyS Prod MulSect DivSect EmptyP EmbedI Var EncI EmbedA
Or EmbedL And EmbedB Rel Not EncB
164
Beispiel (siehe Kontextfreie Grammatiken)
165
JavaLight-Interpreter
evalCS :: JavaAlg a b c d e f g h i k -> Commands -> a
evalCS alg (Seq c d) = seq alg (evalC alg c) $ evalCS alg d
evalCS alg (EmbedC c) = embedC alg $ evalC alg c
Analog werden die Interpreter von Termen der anderen Nicht-Basis-Sorten von JavaLight,
also
evalC
evalI
evalST
evalP
evalPT
evalF
evalB
evalCJ
evalL
::
::
::
::
::
::
::
::
::
JavaAlg
JavaAlg
JavaAlg
JavaAlg
JavaAlg
JavaAlg
JavaAlg
JavaAlg
JavaAlg
a
a
a
a
a
a
a
a
a
b
b
b
b
b
b
b
b
b
c
c
c
c
c
c
c
c
c
d
d
d
d
d
d
d
d
d
e
e
e
e
e
e
e
e
e
f
f
f
f
f
f
f
f
f
g
g
g
g
g
g
g
g
g
h
h
h
h
h
h
h
h
h
i
i
i
i
i
i
i
i
i
k
k
k
k
k
k
k
k
k
->
->
->
->
->
->
->
->
->
Command
IntE
SumSect
Product
ProdSect
Factor
BoolE
Conjunct
Literal
->
->
->
->
->
->
->
->
->
b
c
d
e
f
g
h
i
k
induktiv definiert (siehe evalA). Eine Testumgebung für die folgenden JavaLight-Algebren
wird im Abschnitt JavaLight-Parser vorgestellt. Diese und alle anderen Haskell-Programme
zu JavaLight (außer obigem Interpreter) stehen auch in der Datei Java.hs.
166
Die Wortalgebra von JavaLight
javaWord :: JavaAlg String String String String String String String
String String String
javaWord = JavaAlg {seq
= (++), embedC = id, skip = "; ",
assign = \x e -> x++" = "++e++"; ",
cond
= \be c d -> "if "++be++cPars c++
" else"++cPars d,
cond1
= \be c -> "if "
++be++cPars c,
loop
= \be c -> "while "++be++cPars c,
sum
= (++), addSect = (('+':) .) . (++),
subSect = (('-':) .) . (++), emptyS = "",
prod
= (++), mulSect = (('*':) .) . (++),
divSect = (('/':) .) . (++), emptyP = "",
embedI = show, var = id, encloseI = rPars,
or
= (++) . (++" || "), embedA = id,
and
= (++) . (++" && "), embedL = id,
embedB = show,
rel
= ((++) .) . flip (++),
not
= ('!':), encloseB = rPars}
where cPars = (" {"++) . (++"}"); rPars = ('(':) . (++")")
167
Die Ableitungsalgebra von JavaLight
javaDeri :: JavaAlg (Tree String) (Tree String) (Tree String) (Tree String)
(Tree String) (Tree String) (Tree String) (Tree String)
(Tree String) (Tree String)
javaDeri = JavaAlg {seq_
= (F "commands" .) . two,
embedC
= F "command" . one,
skip
= F "command" [leaf ";"],
assign
= \x e -> F "command" [leaf x,leaf "=",e,leaf ";"],
cond
= \be c d -> F "command" $ leaf "if":be:cPars c++
leaf "else":cPars d,
cond1
= \be c -> F "command" $ leaf "if":be:cPars c,
loop
= \be c -> F "command" $ leaf "while":be:cPars c,
sum_
= (F "intE" .) . two,
addSect = (F "sumSect" .) . three (leaf "+"),
subSect = (F "sumSect" .) . three (leaf "-"),
emptyS
= leaf "sumSect",
prod
= (F "product" .) . two,
mulSect = (F "prodSect" .) . three (leaf "*"),
divSect = (F "prodSect" .) . three (leaf "/"),
emptyP
= leaf "prodSect",
embedI
= F "factor" . one . leaf . show,
var
= F "factor" . one . leaf,
encloseI = F "factor" . rPars,
168
or_
= (F "boolE" .) . flip three (leaf "||"),
embedA
= F "boolE" . one,
and_
= (F "conjunct" .) . flip three (leaf "&&"),
embedL
= F "conjunct" . one,
embedB
= F "literal" . single . leaf . show,
rel
= \rel -> (F "literal" .) . flip three (leaf rel),
not_
= F "literal" . two (leaf "!"),
encloseB = F "literal" . rPars}
where one x = [x]
two x y = [x,y]
three x y z = [x,y,z]
leaf = flip F []
cPars e = [leaf "",e,leaf ""]
rPars e = [leaf "(",e,leaf ")"]
169
Beispiel (siehe Term-, Wort- und Ableitungsalgebra)
170
Das Zustandsmodell von JavaLight
javaState :: JavaAlg (Store -> Store) (Store -> Store)
(Store -> Int) (Store -> Int -> Int)
(Store -> Int) (Store -> Int -> Int) (Store -> Int)
(Store -> Bool) (Store -> Bool) (Store -> Bool)
javaState = JavaAlg {seq_
= flip (.), embedC = id, skip = id,
assign
= \x e st -> update st x $ e st,
cond
= \be c d st -> if be st then c st else d st,
cond1
= \be c st -> if be st then c st else st,
loop
= loop,
sum_
= \e sect st -> sect st $ e st,
addSect = \e sect st i -> sect st $ i+e st,
subSect = \e sect st i -> sect st $ i-e st,
emptyS
= const id,
prod
= \e sect st -> sect st $ e st,
mulSect = \e sect st i -> sect st $ i*e st,
divSect = \e sect st i -> sect st $ i`div`e st,
emptyP
= const id,
embedI
= const, var = flip ($), encloseI = id,
or_
= (|||), embedA = id,
and_
= (&&&), embedL = id, embedB = const,
rel
= \rel e e' st -> evalRel rel (e st) $ e' st,
not_
= (not .), encloseB = id}
171
where loop :: (Store -> Bool) -> (Store -> Store) -> Store -> Store
loop be c st = if be st then loop be c $ c st else st
(1)
evalRel :: String -> Int -> Int -> Bool
evalRel rel = case rel of "<" -> (<);
">" -> (>);
"<=" -> (<=)
">=" -> (>=); "==" -> (==); "!=" -> (/=)
Die Auswertung arithmetischer Ausdrücke (ohne Skalarmultiplikation und Exponentiation) im Zustandsmodell von JavaLight liefert dieselben Ergebnisse wie die Funktion
evalE :: Expr -> (Store -> Store)
im Abschnitt Haskell: Datentypen, d.h. evalE = evalI javaState.
loop ist eine partielle Funktion, denn bekanntlich terminieren Schleifen nicht immer. Um
zu zeigen, dass der im nächsten Abschnitt definierte JavaLight-Compiler korrekt bzgl.
javaState ist, muss die mathematische Definition von loop, die hinter der Gleichung
(1) steckt, genauer betrachtet werden. Wir werden das im Abschnitt Korrektheit des
JavaLight-Compilers nachholen.
172
` Attributierte Grammatiken
Nach traditioneller Terminologie sind die Funktion evalE und evalCS javaState Interpreter, weil sie neben dem Quellprogramm eine “Eingabe”, hier: eine Variablenbelegung
vom Typ Store, verlangen. An den Typen von evalE und evalCS javaState wird jedoch
deutlich, dass die Unterscheidung zwischen Compiler und Interpreter keine substanzielle
ist. Die Argumente dieser Funktionen sind nämlich keine Paare, bestehend aus Syntaxbaum und Variablenbelegung, sondern es sind - wie immer bei einem Compiler - allein die
Syntaxbäume (oder Quellprogramme), die abgebildet werden, hier auf Funktionen von
Store nach Store. Die Typen sind also Instanzen des polymorphen Typs a -> b -> c.
Jede Funktion dieses Typs kann aber in eine Funktion des Typs (a,b) -> c umgewandelt
werden und umgekehrt:
uncurry :: (a -> b -> c) -> (a,b) -> c
uncurry f (a,b) = f a b
curry :: ((a,b) -> c) -> a -> b -> c
curry f a b = f (a,b)
(siehe Haskell: Funktionen). curry macht demnach aus dem Interpreter von a mit Eingabetyp b und Zielsprache c einen Compiler von a mit Zielsprache b -> c.
173
Parameter wie die Eingaben vom Typ b bezeichnet man auch als vererbte Attribute
(inherited attributes) des Compilers. Allgemein bildet der Definitionsbereich einer funktionalen Zielalgebra alg = (b1 -> ... -> bk -> c) die Wertebereiche der vererbten
Attribute des Compilers nach alg. Auch der Wertebereich von alg kann neben der Menge c der eigentlichen Zielobjekte weitere Komponenten enthalten, die dann abgeleitete
Attribute (derived, synthesized attributes) genannt werden. Die Trägermengen von alg
haben dann die Form
b1 -> ... -> bk -> (c,c1,...,cn).
Abgeleitete Attribute sind z.B. der Typ eines Ausdrucks, der Platzbedarf für seinen Wert,
die Größe eines Zielobjekts c, etc. Ihre jeweiligen Werte werden bei der Berechnung von
c aus Attributen von Komponenten von c ermittelt.
Vererbte Attribute sind z.B. Positionen, Adressen, Schachtelungstiefen, etc. Diese müssen
umgekehrt an das Zielobjekt oder an einzelne Komponenten desselben übergeben werden.
Transiente Attribute sind gleichzeitig vererbt und abgeleitet. Ihre jeweiligen Werte
bilden die Zustände, die einen Compiler zur Übergangsfunktion eines Automaten machen.
In den folgenden Grammatikinterpretationen werden die Attributkomponenten der Trägermengen und ihre jeweilige Bedeutung besonders herausgestellt.
174
Beispiel Binärdarstellung rationaler Zahlen
konkrete Syntax G
rat → nat. | rat0 | rat1
abstrakte Syntax Σ(G)
mkRat :: nat → rat
app0, app1 :: rat → rat
0, 1 :: nat
app0, app1 :: nat → nat
nat → 0 | 1 | nat0 | nat1
G-Algebra A
Arat enthält ein abgeleitetes Attribut mit Wertebereich R, 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.
Arat = R × R
Anat = N
mkRatA : Anat → Arat
mkRatA (val) = (1, val)
app0A : Arat → Arat
app0A (inc, val) = (inc/2, val)
0A : Anat
0A = 0
app0A : Anat → Anat
app0A (n) = n ∗ 2
app1A : Arat → Arat
app1A (inc, val)) = (inc/2, val + inc/2)
1A : Anat
1A = 1
app1A : Anat → Anat
app1A (n) = n ∗ 2 + 1
175
Beispiel Strings mit Hoch- und Tiefstellungen
konkrete Syntax G
text → text box |
text ↑ box |
text ↓ box |
box → (text)|a|b|c
abstrakte Syntax Σ(G)
app :: text box → text
up :: text box → text
down :: text box → text
empty :: text
mkBox :: text → box
a, b, c :: box
G-Algebra A
Atext 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 werden soll,
• ein abgeleitetes Attribut mit Wertebereich N3, das die Länge sowie - auf eine feste
Grundlinie bezogen - Höhe und Tiefe des Rechtecks liefert.
176
Atext = Abox = N2 → N3 × String
appA : Atext × Abox → Atext
appA(f, g)(x, y) = ((l + l0, max h h0, max t t0), str · str0)
where ((l, h, t), str) = f (x, y)
((l0, h0, t0), str0) = g(x + l, y)
upA : Atext × Abox → Atext
0
upA(f, g)(x, y) = ((l + l0, h + h0 − 1, max t (t0 − h + 1)), str·str )
where ((l, h, t), str) = f (x, y)
((l0, h0, t0), str0) = g(x + l, y + h − 1)
downA : Atext × Abox → Atext
downA(f, g)(x, y) = ((l + l0, max h (h0 − t − 1), t + t0 − 1), str·str0 )
where ((l, h, t), str) = f (x, y)
((l0, h0, t0), str0) = g(x + l, y − t + 1)
empty A : Atext
mkBoxA : Atext → Abox xA : Abox
x ∈ {a, b, c}
empty A(x, y) = ((0, 0, 0), t) mkBoxA = id
xA(x, y) = ((1, 2, 0), x)
177
Beispiel Übersetzung regulärer Ausdrücke in erkennende Automaten
Die folgende Graphgrammatik beschreibt die Übersetzung eines regulären Ausdrucks
R in einen nichtdeterministischen Automaten, der L(R) erkennt:
s
∅
R
0
s'
s
R
1
Regel 1
s'
Regel 2
s'
Regel 3
R
s
R|R'
s'
s
R'
s
R.R'
s'
s
R
R'
Regel 4
s'
ε
s
plus(R)
s'
s
ε
R
178
ε
s'
Regel 5
Zunächst wird die erste Regel auf den gegebenen Ausdruck R angewendet. Es entsteht
ein Graph mit zwei Knoten und einer mit R markierten Kante. Dieser wird nun mit
den anderen Regeln schrittweise vergrößert, bis an allen seinen Kanten nur noch Alphabetsymbole stehen. Dann stellt er den Transitionsgraphen eines Automaten dar, der R
erkennt.
Mit obiger Graphgrammatik aus dem regulären Ausdruck R = ((a+|(bc)+)∗
konstruierter Automat zur Erkennung von L(R)
179
Während die obige Graphgrammatik von einer einzelnen (Übergangs-)Kante ausgeht und
daraus schrittweise den Automaten erzeugt, beginnt die Auswertung eines regulären Ausdrucks in der folgenden Reg-Algebra mit der leeren Übergangsfunktion und fügt schrittweise Übergänge hinzu bzw. fasst die Übergangsfunktionen für Teilausdrücke zur Übergangsfunktion für ihre Verknüpfung mit einem regulären Operator zusammen.
Reg-Algebra NDA (siehe Reguläre Ausdrücke...)
Zustände werden als natürliche Zahlen dargestellt, Z ist das Eingabealphabet und
δ : N × Z ∪ {} → [N]
die Übergangsfunktion des Automaten.
NDAreg enthält
• die Übergangsfunktion als transientes Attribut, das mit der konstanten Funktion
const[] 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.
180
NDAreg
= (N × Z ∪ {} → [N]) × N3 → N × (N × Z ∪ {} → [N])
NDAword = Z ∗
NDAsymbol = Z
∅NDA : NDAreg
∅NDA(s, s0, next, δ) = (next, delete(δ, s, s0))
(siehe Regel 2)
NDA : NDAreg
NDA(s, s0, next, δ) = (next, add(δ, s, , s0))
_NDA : NDAsymbol → NDAreg
_NDA(a)(s, s0, next, δ) = (next, add(δ, s, a, s0))
_|NDA_ : NDAreg × NDAreg → NDAreg
(f |NDAg)(s, s0, next, δ) = g(s, s0, next0, δ 0)
where (next0, δ 0) = f (s, s0, next, δ)
(siehe Regel 3)
_ ·NDA _ : NDAreg × NDAreg → NDAreg
(f ·NDA g)(s, s0, next, δ) = g(next, s0, next0, δ 0)
(siehe Regel 4)
where (next0, δ 0) = f (s, next, next + 1, δ)
181
plusNDA : NDAreg → NDAreg
plusNDA(f )(s, s0, next, δ) = (next2, add(δ3, next1, , s0))
(siehe Regel 5)
where next1 = next + 1
(next2, δ1) = f (next, next1, next1 + 1, δ)
δ2 = add(δ1, s, , next)
δ3 = add(δ2, next1, , next)
x
add(δ, s, x, s0) fügt den Übergang s → s0 zur Übergangsfunktion δ hinzu.
delete(δ, s, s0) beseitigt alle Übergange von s nach s0.
Haskell-Implementierung:
add :: (Eq a,Eq b,Eq c) => (a -> b -> [c]) -> a -> b -> c
-> a -> b -> [c]
add f a b c = update f a $ update (f a) b $ insert c $ f a b
delete :: (Eq a,Eq b) => (a -> b -> [c]) -> a -> b -> a -> b -> [c]
delete f a b = update f a $ update (f a) b []
182
Darstellung von Σ(G)-Termen als hierarchische Listen
Beispiel
Mit der unten definierten JavaLight-Algebra javaList wird der Syntaxbaum javaTerm des Programms
fact = 1; while x > 0 {fact = fact*x; x = x-1;}
in folgende Form gebracht:
(Seq (Assign "fact" (Sum (Prod (EmbedI 1))
EmptyP)
EmptyS))
(EmbedC (Loop (EmbedA (EmbedL (Rel ">" (Sum (Prod (Var "x"))
EmptyP)
EmptyS)
(Sum (Prod (EmbedI 0))
EmptyP)
EmptyS)))
(Seq (Assign "fact" (Sum (Prod (Var "fact"))
(MulSect (Var "x"))
EmptyP)
EmptyS))
(EmbedC (Assign "x" (Sum (Prod (Var "x"))
EmptyP)
(SubSect (Prod (EmbedI 1))
EmptyP)
EmptyS)))
Alle Trägermengen von javaList enthalten zwei vererbte Attribute vom Typ Bool bzw. Int. Der Boolesche Wert gibt an, ob der jeweilige Teilstring str hinter bzw. unter den vorangehenden geschrieben wird.
Im zweiten Fall wird str um den Wert des zweiten Attributs eingerückt.
183
type BIS = Bool -> Int -> String
javaList :: JavaAlg BIS BIS BIS BIS BIS BIS BIS BIS BIS BIS
javaList = JavaAlg {seq_
= indent2 "Seq", embedC = indent1 "EmbedC",
skip
= indent0 "Skip",
assign
= indent1 . ("Assign "++) . show,
cond
= indent3 "Cond",
cond1 = indent2 "Cond1",
loop
= indent2 "Loop",
sum_ = indent2 "Sum",
addSect = indent2 "AddSect", subSect = indent2 "SubSect",
emptyS
= indent0 "EmptyS", prod = indent2 "Prod ",
mulSect = indent2 "MulSect", divSect = indent2 "DivSect",
emptyP
= indent0 "EmptyP",
embedI
= indentA "EmbedI", var = indentA "Var",
encloseI = indent1 "EncloseI",
or_
= indent2 "Or",
embedA = indent1 "EmbedA",
and_
= indent2 "And",
embedL = indent1 "EmbedL ",
embedB
= indentA "EmbedB",,
rel
= indent2 . ("Rel "++) . show,
not_
= indent1 "Not",
encloseB = indent1 "EncloseB"}
where indent0 x
= blanks x []
indentA x a
= indent0 $ enclose x $ show a
indent1 x f
= blanks x [f]
indent2 x f g
= blanks x [f,g]
indent3 x a b c = blanks x [a,b,c]
enclose x y
= '(':x++' ':y++")"
184
blanks :: String -> [BIS] -> BIS
blanks x fs b n = if b then str else '\n':replicate n ' '++str
where str = case fs of
[] -> x
[f] -> first f
[f1,f2] -> first f1++f2 False k
[f1,f2,f3] -> first f1++f2 False k++f3 False k
first f = enclose x $ f True k
k = n+length x+2
Eine Testumgebung der Listenalgebra wird im Abschnitt JavaLight-Parser vorgestellt.
185
Beispiel Übersetzung von JavaLight in eine Assemblersprache
Wir erweitern zunächst die Zielsprache StackCom des Expr-Compilers compileE um einen
Speicherbefehl (Save), arithmetische und Boolesche Befehle sowie Sprungbefehle:
data StackCom = Push Int | Load String | Save String | Add | Sub |
Mul | Div | Or_ | And_ | Inv | Cmp String | Jump Int |
JumpT Int | JumpF Int
Zur Ausführung der Sprungbefehle muss der Zustandstyp der Kellermaschine um einen
Befehlszähler (program counter) erweitert werden:
type State = ([Int],Store,Int)
Die Bedeutung der einzelnen Zielkommandos wird wieder durch einen Interpreter auf
State definiert:
186
executeCom :: StackCom -> State -> State
executeCom com (stack,store,pc) =
case com of Push a
-> (a:stack,store,pc)
Load x
-> (store x:stack,store,pc)
Save x
-> (s,update store x a,pc) where a:s = stack
Add
-> (a+b:s,store,pc) where a:b:s = stack
Sub
-> (a-b:s,store,pc) where a:b:s = stack
Mul
-> (a*b:s,store,pc) where a:b:s = stack
Div
-> (a`div`b:s,store,pc) where a:b:s = stack
Or_
-> (signum (a+b):s,store,pc) where a:b:s = stack
And_
-> (a*b:s,store,pc) where a:b:s = stack
Inv
-> ((a+1)`mod`2:s,store,pc) where a:s = stack
Cmp rel
-> (c:s,store,pc)
where a:b:s = stack
c = if evalRel rel a b then 1 else 0
Jump lab -> (stack,store,lab)
JumpT lab -> (stack,store,if a == 1 then lab else pc)
where a:s = stack
JumpF lab -> (stack,store,if a == 0 then lab else pc)
where a:s = stack
187
Der jeweilige Inhalt des Befehlszählers bestimmt die Reihenfolge, in der die Elemente
einer Befehlsliste ausgeführt und ggf. wiederholt werden:
execute :: [StackCom] -> State -> State
execute cs = cycle
where cycle state@(stack,store,pc) =
if pc < length cs
then cycle $ executeCom (cs!!pc) (stack,store,pc+1)
else state
Um Sprungziele (die Int-Parameter der Sprungbefehle) korrekt berechnen zu können,
muss der Compiler die Position jedes erzeugten Befehls innerhalb des gesamten Zielprogramms kennen. Dieses erzeugt er durch Interpretation des Quellprogramms in der
folgenden JavaLight-Algebra, deren Trägermengen
• ein vererbtes Attribut vom Typ Int für die Nummer des ersten Befehls des Zielcodes
und
• ein abgeleitetes Attribut vom Typ Int für die Länge des Zielcodes
enthalten:
type ISI = Int -> ([StackCom],Int)
188
javaStack :: JavaAlg ISI ISI ISI (ISI -> ISI) ISI (ISI -> ISI) ISI ISI ISI ISI
javaStack = JavaAlg {seq_
= concatCode, embedC = id,
skip
= const ([],0),
assign
= append . Save,
cond
= \be c d lab
-> let ((code1,lg1),exit) = cond1 1 be c lab
(code2,lg2) = d exit
in (code1++Jump (exit+lg2):code2,lg1+lg2+1),
cond1
= \be c -> fst . cond1 0 be c,
loop
= \be c lab
-> append (Jump lab) (fst . cond1 1 be c) lab,
sum_
= flip ($),
addSect = \e -> (. flip (appendF Add) e),
subSect = \e -> (. flip (appendF Sub) e),
emptyS
= id, prod = flip ($),
mulSect = \e -> (. flip (appendF Mul) e),
divSect = \e -> (. flip (appendF Div) e),
emptyP
= id, embedI = \i -> const ([Push i],1),
var
= \x -> const ([Load x],1), encloseI = id,
or_
= appendF Or_, embedA = id,
and_
= appendF And_, embedL = id,
embedB
= \b -> const ([Push $ if b then 1 else 0],1),
rel
= appendF . Cmp,
not_
= append Inv,
encloseB = id}
189
where append :: StackCom -> ISI -> ISI
append com f lab = (code++[com],lg+1)
where (code,lg) = f lab
appendF :: StackCom -> ISI -> ISI -> ISI
appendF com = (append com .) . concatCode
concatCode :: ISI -> ISI -> ISI
concatCode f g lab = (code1++code2,lg1+lg2)
where (code1,lg1) = f lab
(code2,lg2) = g $ lab+lg1
cond1 :: Int -> ISI -> ISI -> Int -> (([StackCom],Int),Int)
cond1 n f g lab = ((code1++JumpF exit:code2,lg1+lg2+1),exit)
where (code1,lg1) = f lab
lab1 = lab+lg1+1
(code2,lg2) = g lab1
exit = lab1+lg2+n
Eine Testumgebung des Compilers wird im Abschnitt JavaLight-Parser vorgestellt.
190
` Haskell: Monaden
class Monad m where
(>>=) :: m a -> (a -> m b) -> m b sequentielle Komposition
(>>)
:: m a -> m b -> m b
sequentielle Komposition ohne Wertübergabe
return :: a -> m a
Einbettung in monadischen Typ
fail
:: String -> m a
Wert im Fall eines Matchfehlers
m >> m' = m >>= const m'
(m >>= f) >>= g = m >>= \x -> f x >>= g
m >>= return
= m
return a >>= f
= f a
(1)
class Monad m => MonadPlus m where steht im Modul Monad.hs
mzero :: m a
scheiternde Berechnung heißt zero in hugs
mplus :: m a -> m a -> m a
parallele Komposition heißt (++) in hugs
mzero >>= f = mzero
(2)
m >>= \x -> mzero = mzero
mzero `mplus` m = m
m `mplus` mzero = m
191
Instanzen von Monad bzw. MonadPlus sollen die aufgeführten Gleichungen erfüllen. Sie
wurden - mit _∗ anstelle von >>= bereits im Abschnitt Korrektheit monadischer Parser
genannt.
Alle bisher deklarierten Typen sind Typen erster Ordnung und haben den Kind ∗.
Kinds sind Typen von Typen. Alle Kinds sind aus ∗ und → gebildet.
Die Typvariable m (s.o.) und der Ausdruck Bintree (siehe Typklassen) sind Typen
zweiter Ordnung und haben daher den Kind ∗ → ∗. Sie bezeichnen also Funktionen,
die Typen erster Ordnung auf Typen erster Ordnung abbilden.
JavaAlg bildet Typen erster Ordnung auf Typen siebenter Ordnung ab und hat deshalb
den Kind
∗ → ∗ → ∗ → ∗ → ∗ → ∗ → ∗ → ∗.
192
Die do-Notation
Ein Ausdruck
m >>= \x -> m'
vom Typ m a wird ersetzt durch
do x <- m; m'
do x <- m
m'
oder
Wird die “Ausgabe” x von m nicht benötigt, dann genügt m anstelle der Zuweisung
x <- m. Außerdem schreibt man
do x <- m; y <- m'; m''
do x <- m; (do y <- m'; m'')
anstelle von
Der return-Operator ist im Sinn der Gültigkeit folgender Gleichungen neutral bzgl. des
Komposition ; (siehe (1)):
do x <- m; return x
do x <- return a; m
=
=
m
m[a/x] (a substituiert für x)
193
Monaden-Kombinatoren
(<-) :: Monad m => a -> m a -> m ()
(;) :: Monad m => m a -> m b -> m b
when :: Monad m => Bool -> m () -> m ()
when b m = if b then m else return ()
guard :: MonadPlus m => Bool -> m ()
guard b = if b then return () else mzero
(1)
(2)
⇒
⇒
(do guard True; m1; ...; mn) = (do m1; ...; mn)
(do guard False; m1; ...; mn) = mzero
msum :: MonadPlus m => [m a] -> m a
msum = foldr mplus mzero
194
heißt concat in hugs
sequence :: Monad m => [m a] -> m [a]
sequence (m:ms) = do a <- m; as <- sequence ms; return $ a:as
sequence _
= return []
heißt accumulate in hugs
sequence_ :: Monad m => [m a] -> m ()
sequence_ = foldr (>>) $ return ()
heißt sequence in hugs
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
zipWithM :: Monad m => (a -> b -> m c) -> [a] -> [b] -> m [c]
zipWithM f as = sequence . zipWith f as
zipWithM_ :: Monad m => (a -> b -> m c) -> [a] -> [b] -> m ()
zipWithM_ f as = sequence_ . zipWith f as
lookupM :: (Eq a,MonadPlus m) => a -> [(a,b)] -> m b
lookupM a ((a',b):s) = if a == a' then return b `mplus` lookupM a s
else lookupM a s
lookupM _ _
= mzero
195
Beispiel
Der LL-Compiler für SAB (siehe Vom LL-Parser zum LL-Compiler lautet unter Verwendung von Monaden-Kombinatoren und der do-Notation wie folgt:
data SABalg s a b = SABalg {f_p1 :: b -> s, f_p2 :: a -> s, f_p3 :: s,
f_p4 :: s -> a, f_p5 :: a -> a -> a,
f_p6 :: s -> b, f_p7 :: b -> b -> b}
compStep_S :: MonadPlus m => Alg s a b -> String -> m s
compStep_S alg w = msum [try_p1(w), try_p2(w), try_p3(w)]
where try_p1 w = do (_,w) <- compStep_a w; (c,w) <- compStep_B w
return (f_p1 alg c,w)
try_p2 w = do (_,w) <- compStep_b w; (c,w) <- compStep_A w
return (f_p2 alg c,w))
try_p3 w = (f_p3 alg,w)
196
compStep_A :: MonadPlus m => Alg s a b -> String -> m a
compStep_A(w) = msum [try_p4(w), try_p5(w)]
where try_p4 w = do (_,w) <- compStep_a w; (c,w) <- compStep_S w
return (f_p4 alg c,w)
try_p5 w = do (_,w) <- compStep_b w; (c,w) <- compStep_A w
(d,w) <- compStep_A w; return (f_p5 alg c d,w)
compStep_B :: MonadPlus m => Alg s a b -> String -> m b
compStep_B w = msum [try_p6 w, try_p7 w ]
where try_p6 w = do (_,w) <- compStep_b w; (c,w) <- compStep_S w
return (f_p6 alg c,w)
try_p7 w = do (_,w) <- compStep_a w; (c,w) <- compStep_B w
(d,w) <- compStep_B w; return (f_p7 alg c d),w)
compStep_a(z:w)
compStep_b(z:w)
compStep_a[]
compStep_b[]
=
=
=
=
do guard $ z == a; return (a,w)
do guard $ z == b; return (b,w)
mzero
mzero
197
Die Identitätsmonade
newtype Id a = Id {entry :: a}
instance Monad Id where Id a >>= f = f a
return = Id
Mit dieser Monade kann man nicht allzuviel anfangen, aber ganz gut die Verwandtschaft
zwischen monadischer und imperativer Programmierung erkennen. So lautet z.B. eine
monadische - imperative Programmierer würden sagen: prozedurale - Version der Funktion
sumTree :: Bintree Int -> Int
sumTree (Branch left a right) = a+sumTree left+sumTree right
sumTree _
= 0
folgendermaßen:
sumTree = entry . f where f (Branch left a right) = do b <- f left
c <- f right
return $ a+b+c
f _
= return 0
198
Die Maybe-Monade
data Maybe a = Just a | Nothing
instance Monad Maybe where Just a >>= f = f a
_ >>= _
= Nothing
return = Just
fail _ = mzero
erfüllt (1)
instance MonadPlus Maybe where mzero = Nothing
erfüllt (2)
Nothing `mplus` m = m
m `mplus` _
= m
199
Verwendung der Maybe-Monade
Rechnen mit partiellen Funktionen
Eine partielle Funktion f : A → B wird in Haskell durch f :: A -> Maybe B implementiert. Das Ergebnis der Komposition zweier partieller Funktionen f und g wird durch
f ‘comp‘ g implementiert:
comp f g :: (a -> Maybe b) -> (b -> Maybe c) -> a -> Maybe c
comp f g = do b <- f a; g b ist äquivalent zu: f a >>= \b -> g b
Nach Definition von >>= in der Maybe-Monade ist (f ‘comp‘ g) a genau dann definiert
(hat also einen Wert der Form Just c), wenn f (a) und f (g(a) definiert sind. c ist dann
gleich f (g(a), m.a.W.
case f a of Just b -> g b; _ -> Nothing
ist äquivalent zu: do b <- f a; g b
Nach Definition von mzero bzw. mplus in der Maybe-Monade gilt z.B.:
case f a of Just b | p b -> g b; _ -> Nothing
ist äquivalent zu: do b <- f a; guard $ p b; g b
case f a of Nothing -> g a; b -> b
ist äquivalent zu: f a `mplus` g a
200
Beispiel
Die folgende Variante split2 von filter wendet zwei Boolesche Funktionen f und g
auf die Elemente einer Liste s und ist genau dann definiert, wenn für jedes Listenelement
x f (x) oder g(x) gilt. Im definierten Fall liefert split2 das Listenpaar, bestehend aus
f ilter(f )(s) und f ilter(g)(s):
split2 :: (a -> Bool) -> (a -> Bool) -> [a] -> Maybe ([a],[a])
split2 f g (x:s) = do (s1,s2) <- split2 f g s
if f x then Just (x:s1,s2)
else do guard $ g x; Just (s1,x:s2)
split2 _ _ _
= Just ([],[])
201
Eine Exception-Monade für differenzierte Fehlermeldungen (vom Typ a)
data Either a b = Left a | Right b
instance (Ord err,Bounded err) => Monad (Either a)
where Left a >>= _ = Left a
Right b >>= f = f b
return = Right
fail _ = mzero
erfüllt (1)
instance (Ord err,Bounded err) => MonadPlus (Either a)
where mzero = Left minBound
erfüllt (2)
Left a `mplus` Left a' = Left $ max a a'
Left _ `mplus` m
= m
m `mplus` _
= m
202
Die Listenmonade
instance Monad [ ] where as >>= f = concatMap f as
return a = [a]
fail _ = mzero
instance MonadPlus [ ] where mzero = []
mplus = (++)
203
erfüllt (1)
erfüllt (2)
Verwendung der Listenmonade
Rechnen mit nichtdeterministischen Funktionen
Eine nichtdeterministische Funktion f : A → ℘(B) kann in Haskell durch f :: A ->
[B] implementiert werden. Das Ergebnis der Komposition zweier nichtdeterministischer
Funktionen f und g wird durch f ‘comp‘ g implementiert:
comp f g :: (a -> [b]) -> (b -> [c]) -> a -> [c]
comp f g = do b <- f a; g b ist äquivalent zu: f a >>= \b -> g b
Nach Definition von >>= in der Listenmonade gilt:
(f `comp` g) a
ist äquivalent zu:
concat [g b | b <- f a]
Die Listeninstanz der Monadenfunktion sequence :: [m a] -> m [a] hat den Typ
[[a]] -> [[a]] und liefert das - als Liste von Listen dargestellte - kartesische Produkt
seiner Argumentlisten as1, . . . , asn:
sequence[as1, . . . , asn] = [[a1, . . . , an] | ai ∈ asi, 1 ≤ i ≤ n] = as1 × · · · × asn.
204
Nichtmonadisch ist sequence demnach folgendermaßen definiert:
sequence (as:bss) = [a:bs | a <- as, bs <- sequence bss]
sequence _
= [[]]
Beispiel sequence $ replicate(3)[1..4] ;
[[1,1,1],[1,1,2],[1,1,3],[1,1,4],[1,2,1],[1,2,2],[1,2,3],[1,2,4],
[1,3,1],[1,3,2],[1,3,3],[1,3,4],[1,4,1],[1,4,2],[1,4,3],[1,4,4],
[2,1,1],[2,1,2],[2,1,3],[2,1,4],[2,2,1],[2,2,2],[2,2,3],[2,2,4],
[2,3,1],[2,3,2],[2,3,3],[2,3,4],[2,4,1],[2,4,2],[2,4,3],[2,4,4],
[3,1,1],[3,1,2],[3,1,3],[3,1,4],[3,2,1],[3,2,2],[3,2,3],[3,2,4],
[3,3,1],[3,3,2],[3,3,3],[3,3,4],[3,4,1],[3,4,2],[3,4,3],[3,4,4],
[4,1,1],[4,1,2],[4,1,3],[4,1,4],[4,2,1],[4,2,2],[4,2,3],[4,2,4],
[4,3,1],[4,3,2],[4,3,3],[4,3,4],[4,4,1],[4,4,2],[4,4,3],[4,4,4]]
Nach Definition von mzero bzw. mplus in der Listenmonade gilt z.B.:
do b <- f a; guard (p b); g b
ist äquivalent zu:
concat [g b | b <- f a, p b]
do a <- s; let b = f a; guard (p b); [h b]
ist äquivalent zu: [h b | a <- s, let b = f a, p b]
205
` Transitionsmonaden
Trans state a
state -> (a,state)
total
functions
Parser m
nondeterministic
functions
String/state
[ ]/m
TransM state m a
state -> m (a,state)
Either a/m
Maybe/m
partial
functions
functions with
exceptions of type a
206
Die Monade totaler Transitionsfunktionen
newtype Trans state a = T {trans :: state -> (a,state)}
instance Monad (Trans state)
where T m >>= f = T $ \st -> let (a,st') = m st
in trans (f a) st'
return a = T $ \st -> (a,st)
Die IO-Monade
type IO a = Trans STATE a
STATE ist hier der Typ interner Systemzustände.
207
Verwendung der IO-Monade
Ein/Ausgabe von Funktionsargumenten bzw. -werten
test :: (Read a,Show b) => (a -> b) -> IO()
test f = do str <- readFile "source"
writeFile "target" $ show $ f $ read str
readFile :: String -> IO String
readFile "source" liest den Inhalt der
Datei source und gibt ihn als String zurück.
read :: Read a => String -> a
read übersetzt einen String in ein Objekt
vom Typ a.
show :: Show b => b -> String
show übersetzt ein Objekt vom Typ b
in einen String.
writeFile :: String -> String -> IO ()
writeFile "target" schreibt einen String
in die Datei target.
208
Ein/Ausgabe-Schleifen
loop :: IO ()
loop = do putStrLn "Hello!"
putStrLn "Enter an integer x!"
str <- getLine
let x = read str
lokale Definition, gilt in allen darauffolgenden
Kommandos. Nicht mit einer lokalen Definition
let a = e in e' verwechseln, die nur in e' gilt!
if x < 5 then do putStr "x < 5"
else do putStrLn ("x = "++show x)
loop
then und else müssen bzgl. der Spalte ihres
ersten Zeichen hinter if stehen!
putStr :: String -> IO ()
putStr str schreibt str ins Konsolenfenster.
putStrLn :: String -> IO ()
putStrLn str schreibt str ins Konsolenfenster
und geht in die nächste Zeile.
getLine :: IO String
getLine liest den eingebenen String
und geht in die nächste Zeile.
209
Datei lesen mit Fehlerbehandlung und Inhaltsübergabe an Folgeprozedur
readFileAndDo :: String -> (String -> IO ()) -> IO ()
readFileAndDo file continue =
do str <- readFile file `catch` const (return "")
if null str then putStrLn $ file++" does not exist"
else continue str
test f = readFileAndDo "source" $ writeFile "target" . show . f . read
210
Die Funktionen der Graphikprogramme PainterHGL.hs und PainterSVG.hs rufen die
folgende Schleife auf, die bei jedem Durchlauf ein Objekt vom Typ a mit interaktiv
eingegebenen Skalierungsfaktoren zeichnet:
readFileAndDraw :: Read a =>
String -> (Float -> Float -> a -> (Window -> IO (),Pos)) -> IO ()
readFileAndDraw file draw = readFileAndDo file $ scale . read
where scale a =
do putStrLn "Enter a horizontal and a vertical scaling factor!"
str <- getLine; let strs = words str
when (length strs == 2) $
do let [hor,ver] = map read strs
(act,size) = draw hor ver a
runGraphics $
bzw.
writeFile (file++".svg")
do w <- openWindow file size
$ svg act size
back w white
scale a
frame w size; act w
getLBP w; closeWindow w
scale a
211
Die Huckepack-Monade monadenabhängiger Transitionsfunktionen
hat in Haskell-Bibliotheken den Namen StateT (state transformer) und unterscheidet
sich von der Zustandsmonade dadurch, das deren Wertetyp (a,state) in eine Monade
eingebettet wird:
newtype TransM state m a = TM {transM :: state -> m (a,state)}
instance MonadPlus m => Monad (TransM state m)
where TM m >>= f = TM $ \st -> m st >>=
\(a,st) -> transM (f a) st
return a = TM $ \st -> return (a,st)
fail _ = mzero
instance MonadPlus m => MonadPlus (TransM state m)
where mzero = TM $ const mzero
TM m `mplus` TM m' = TM $ \st -> m st `mplus` m' st
212
TransM state m liftet die Operationen >>=, mplus, return, fail und mzero von m zum
Funktionenraum state -> m (a,state) und zwar so, dass die Gültigkeit der Gleichungen (1) und (2) von m auf TransM state m übertragen werden.
m >>= f führt zunächst die Transition m aus. Ihr Endzustand und ihre Ausgabe(n) werden
zum Anfangszustand bzw. zur Eingabe der zweiten Transition f.
Das Ergebnis von m ‘mplus‘ m’ hängt von der Instanz der Typvariablen m ab:
Bei Instanziierung durch die Maybe- oder Exception-Monade arbeitet ‘mplus‘ deterministisch: Liefert m ein definiertes Ergebnis, so ist dieses auch das Ergebnis von m ‘mplus‘
m’. Scheitert m jedoch, dann wird die Ausgabe von m’ zur Ausgabe von m ‘mplus‘ m’.
Bei Instanziierung durch die Listenmonade arbeitet ‘mplus‘ nichtdeterministisch: die
Ausgabe von m ‘mplus‘ m’ ist die Konkatenation der Ausgaben von m und m’.
Demnach lassen sich mit der Huckepackmonade deterministische wie auch nichtdeterministische Automaten implementieren: Die Zustandsmenge ist eine Instanz von state,
Übergangs- und Ausgabefunktion sind durch fst . transM bzw. snd . transM gegeben.
213
Weitere TransM-Kombinatoren
sat trans f (“satisfies”) bildet jeden Zustand, der bei Anwendung von trans eine Ausgabe produziert, welche die Bedingung f verletzt, auf das Nullelement mzero von m ab:
sat :: MonadPlus m => TransM state m a -> (a -> Bool)
-> TransM state m a
sat trans f = do a <- trans; guard $ f a; return a
some trans und many trans wiederholen trans solange wie diese Funktion ein Ergebnis ungleich mzero liefert. Alle durch die aufeinanderfolgenden Anwendungen von trans
erzeugten Ausgaben werden aufgelistet. some trans scheitert (liefert mzero), falls schon
die erste Anwendung von trans scheitert. many trans gibt in diesem Fall den Ausgangszustand und die leere Ausgabeliste zurück.
some, many :: MonadPlus m => TransM state m a -> TransM state m [a]
some trans = do a <- trans; as <- many trans; return $ a:as
many trans = some trans `mplus` return []
214
` Monadische String-Parser
Ein String-Parser liest eine Zeichenfolge von links nach rechts, übersetzt das jeweils gelesene Teilwort in ein Objekt eines Typs a und gibt daneben das jeweilige Restwort aus.
Betrachtet man ihn als Automaten, dann bilden die Eingabewörter seine Zustände und
die Objekte vom Typ a seine Ausgaben. Demzufolge kann er als Objekt der Instanz
type Parser = TransM String
der Huckepack-Monade implementiert werden. Ein vollständiger Parser wird aufgebaut
aus den o.g. Monadenkonstanten bzw. -kombinatoren wie return, mzero, >>=, mplus,
mapM, sat und many sowie den folgenden, die typische Scannerfunktionen realisieren. Alle
diese Komponenten sind polymorph in der (Ausgabe-)Monade m, d.h. erst die spätere
Instanziierung durch Maybe, Either oder [ ] legt fest, ob der Parser deterministisch oder
nichtdeterministisch arbeitet, also höchstens ein korrektes Ergebnis berechnet oder alle
(s.o).
215
Read-Instanzen einbinden
Wir formulieren den Parser readTree :: Read a => ReadS (Bintree a) für binäre
Bäume aus dem Abschnitt Typklassen monadisch:
bintree :: Read a => Parser [ ] (Bintree a)
bintree = do a <- TM reads
msum [do "(" <- TM lex; left <- bintree
msum [do "," <- TM lex; right <- bintree
")" <- TM lex
return $ Branch left a right,
do ")" <- TM lex
return $ Branch left a Empty],
do return $ leaf a]
instance Read a => Read (Bintree a) where readsPrec _ = transM bintree
rb1 = read "5(7(3, 8),6( 2) ) " :: Bintree Int
; 5(7(3,8),6(2))
rb2 = reads "5(7(3, 8),6( 2) ) " :: [(Bintree Int,String)]
; [(5(7(3,8),6(2))," "),(5,"(7(3, 8),6( 2) ) ")]
rb3 = read "5(7(3,8),6(2))hh"
:: Bintree Int
; Exception: ...: no parse
rb4 = reads "5(7(3,8),6(2))hh"
:: [(Bintree Int,String)]
; [(5(7(3,8),6(2)),"hh"),(5,"(7(3,8),6(2))hh")]
216
Monadische Scanner
Standard-Parse-Funktionen wie reads und lex sind auf nichtdeterministische Parser zugeschnitten. Im Folgenden definieren und verwenden wir nur generische Parser, deren
Ausgabemonade – wie oben erwähnt – beliebig instanziiert werden kann. Die ersten hier
angegebenen Parser sind insofern Scanner, als sie Symbole oder Elemente von Basistypen
erkennen und ggf. weiterreichen, aber daraus keine komplexeren Zielobjekte konstruieren.
getChr liest das erste Zeichen eines Strings und gibt es als Ergebnis aus:
getChr :: MonadPlus m => Parser m Char
getChr = TM $ \str -> do c:str <- return str; return (c,str)
Ein Matchfehler bei der Zuweisung führt zum Wert von fail, der von der Instanziierung
von m abhängt (s.o.).
char chr und string str erwarten das Zeichen chr bzw. den String str:
char :: MonadPlus m => Char -> Parser m Char
char = sat getChr . (==)
string :: MonadPlus m => String -> Parser m String
string = mapM char
217
token p erlaubt vor und hinter des von p erkannten Strings Leerzeichen, Zeilenumbrüche
oder Tabulatoren:
MonadPlus m => Parser m a -> Parser m a
token p = do space; a <- p; space; return a
where space = many $ sat getChr isDelim
tchar :: MonadPlus m => Char -> Parser m Char
tchar = token . char
isDelim
isSpecial
isDigit
isLetter
=
=
=
=
(`elem`
(`elem`
(`elem`
(`elem`
" \n\t")
"();=!>+-*^")
['0'..'9'])
['a'..'z']++['A'..'Z'])
218
Es folgen vier Scanner, die Elemente von Basistypen (Wahrheitswerte, natürliche Zahlen,
ganze Zahlen bzw. Identifier) erkennen und in entsprechende Haskell-Typen übersetzen.
bool :: MonadPlus m => Parser m Bool
bool = msum [do token $ string "True"; return True,
do token $ string "False"; return False]
nat,int :: MonadPlus m => Parser m Int
nat = do ds <- some $ sat getChr isDigit; return $ read ds
int = nat `mplus` do char '-'; n <- nat; return $ -n
identifier :: MonadPlus m => Parser m String
identifier = do first <- sat getChr isLetter
rest <- many $ sat getChr
$ not . (isSpecial ||| isDelim)
let x = first:rest
guard
$ x `notElem` words "true false if then else while"
return x
219
Ein monadischer Parser für reguläre Ausdrücke
Aus Monaden-Kombinatoren und o.a. Scannerfunktionen lässt sich sehr leicht ein monadischer Parser für reguläre Ausdrücke - z.B. über Char - zusammensetzen. Wir erweitern
den im Abschnitt Haskell: Datentypen im dortigen Parser verwendeten Datentyp Reg um
einen Konstruktor Sat, dessen Parser wie Chr ein Zeichen liest, dieses aber nur dann
akzeptiert, wenn es die Bedingung im Argument von Sat erfüllt:
data RegM = Eps | Chr Char | Sat (Char -> Bool) | Reg :+ Reg |
Reg :* Reg | Plus Reg | Star Reg | Refl Reg
reg
reg
reg
reg
reg
reg
reg
:: MonadPlus m => RegM -> Parser m String
Eps
= return []
(Chr c)
= do char c; return [c]
(Sat f)
= do c <- sat getChr f; return [c]
(e :+ e') = reg e `mplus` reg e'
(e :* e') = do str <- reg e; str' <- reg e'; return $ str++str'
(Plus e) = do str <- reg e
msum [do str' <- reg $ Star e; return $ str++str',
return str]
reg (Star e) = reg (Plus e) `mplus` return []
reg (Refl e) = reg e `mplus` return []
220
Expr-Parser
parseExpr str übersetzt, falls möglich, den String str in das durch ihn bezeichnete
Expr-Objekt:
parseExpr :: MonadPlus m => Parser m Expr
parseExpr = do a <- summand
msum [do tchar '-'; b <- summand; return $ a :- b,
do as <- many $ do tchar '+'; summand
return $ if null as then a else Sum $ a:as]
summand :: MonadPlus m => Parser m Expr
summand = do a <- factor
msum [do tchar '^'; i <- int; return $ a :^ i,
do as <- many $ do tchar '*'; factor
return $ if null as then a else Prod $ a:as]
factor :: MonadPlus m => Parser m Expr
factor = msum [do x <- token identifier; return $ Var x,
do i <- token int; scalar i,
do char '('; a <- parseExpr; tchar ')'; return a]
221
scalar :: MonadPlus m => Int -> Parser m Expr
scalar i = msum [do tchar '*'; a <- summand; return $ i :* a,
return $ Con i]
Die vom Parser implizit vorgenommene Auszeichnung von Summanden bzw. Faktoren
innerhalb der Menge aller Expr-Objekte dient nicht nur der Berücksichtigung von Operatorprioritäten (+ und - vor * und ^), sondern auch der Vermeidung linksrekursiver
Aufrufe des Parsers: Zwischen je zwei aufeinanderfolgenden Aufrufen muss mindestens
ein Zeichen gelesen werden, damit der zweite Aufruf ein kürzeres Argument hat als der
erste und so die Termination des Parsers gesichert ist.
222
Testumgebung für Expr
import PainterSVG (drawTerm,readFileAndDo)
evalFromFile :: Int -> IO ()
evalFromFile n = readFileAndDo "source" $ eval n . read
eval :: Int -> Expr -> IO ()
eval n exp = case n of 1 -> do writeFile "term" $ show exp
drawTerm "term"
2 -> do writeFile "norm" $ show $ reduce exp
drawTerm "norm"
3 -> loop1 exp
_ -> do let code = compileE exp
writeFile "code" $ showCode code
loop2 code
compile :: Int -> IO ()
compile n = readFileAndDo "source" act
where act str = case transM parseE str of
Just (exp,rest) -> do unparsedSuffix rest
eval n exp
_ -> putStrLn "syntax error"
223
input :: IO ([String],Store,Bool)
input = do putStrLn "Enter variables!"; str <- getLine
let vars = words str
putStrLn "Enter values!"; str <- getLine
let vals = map read $ words str
vals' = vals++replicate (length vars-length vals) 0
return (vars, listsToFun 0 vars vals', nonempty vars)
loop1 :: Expr -> IO ()
loop1 exp = do (_,store,proceed) <- input
let result = evalE exp store
when proceed $ do putStrLn $ "result = "++show result
loop1 exp
loop2 :: [StackCom] -> IO ()
loop2 code = do (_,store,proceed) <- input
let (result:_,_) = executeE code ([],store)
when proceed $ do putStrLn $ "result = "++show result
loop2 code
224
unparsedSuffix :: String -> IO ()
unparsedSuffix rest = when (nonempty rest) $ putStrLn
$ "unparsed suffix: "++rest
showCode :: [StackCom] -> String
showCode cs = fold2 f "" [0..length cs-1] cs
where f str n c = str++'\n':replicate (5-length lab) ' '
++lab++": "++show c where lab = show n
evalFromFile n erwartet ein Expr-Objekt exp in der Datei source und führt eval n
darauf aus. eval 1 zeichnet exp und die Normalform von exp in ein Fenster mit dem Titel
term bzw. norm. eval 2 und eval 3 starten eine Schleife, die in jedem Durchlauf eine
Variablenbelegung einliest und den entsprechenden Wert von exp ausgibt. eval 2 ruft
dazu den Expr-Interpreter evalE auf, eval 3 übersetzt exp mit compileE in Zielcode
und führt diesen mit executeE aus.
compile n erwartet Quellcode für ein Expr-Objekt exp in der Datei source, parsiert ihn
und führt eval n darauf aus.
readFileAndDo wurde im Abschnitt Transitionsmonaden definiert.
225
` Monadische Compiler
Mit Hilfe monadischer Operationen lässt sich das Schema eines LL-Compilers ein weiteres
Mal vereinfachen und sogar noch insofern verallgemeinern, als durch die Wahl der Zielmonade gesteuert werden kann, ob er deterministisch oder nichtdeterministisch arbeitet,
ob zu jeder Eingabe höchstens ein Übersetzungsresultat oder alle möglichen Zielobjekte
ausgegeben werden sollen, ob Fehlermeldungen hinzugefügt werden oder nicht.
Monadischer LL-Compiler mit Backtracking
Sei G = (N, Z, P, BΣ, BA) eine bewachte CFG,
P = { p11 = (A1 → v11), . . . , p1n1 = (A1 → v1n1 ),
...
pk1 = (Ak → vk1), . . . , pknk = (Ak → vknk )}
und alg eine G-Algebra. Wir implementieren die Funktion compStep (siehe Vom LLParser zum LL-Compiler) unter Verwendung der Parser-Monade:
226
compStep_Ai :: MonadPlus m => Alg a1 ... ak -> Parser m ai 1 ≤ i ≤ k
compStep_A :: MonadPlus m => Parser m A
A ∈ BS
Compiler, der die Elemente von BA_A erkennt und übersetzt
compStep_x :: MonadPlus m => Parser m String
x ∈ Z \BA
compStep_Ai alg = msum [try_pi1,...,try_pin_i]
1 ≤i ≤k
where try_pij = return f_pij alg
1 ≤ j ≤ ni mit vij = try_pij = do a1 <- compStep_x1
1 ≤ j ≤ ni mit vij 6= a2 <- compStep_x2
x1 . . .xn = vij
. . .
an <- compStep_xn
{i1, . . . , im} = {i|ai ∈ N \Z}
return f_pij alg ai1 ... aim
i1 < . . . < im
compStep_x = token $ string x
x ∈ Z \BA
Wird außerhalb dieser Definitionen nur der Compiler für das Startsymbol von G, sagen
wir: S, aufgerufen, dann können die Compiler für die anderen Nichtterminale zu lokalen Funktionen von compStep_S gemacht werden, womit sich der alg-Parameter dieser
Funktionen vermeiden lässt.
227
Beispiel SAB Die Regeln
p1 = S → aB, p2 = S → bA, p3 = S → ,
p4 = A → aS, p5 = A → bAA, p6 = B → bS, p7 = B → aBB
von SAB liefern nach obigem Schema den folgenden LL-Compiler von L(SAB) nach alg:
compStep_S alg = msum
where try_p1
try_p2
try_p3
compStep_A alg = msum
where try_p4
try_p5
[try_p1, try_p2, try_p3}]
= do compStep_a; c <- compStep_B alg; return f_p1 alg
= do compStep_b; c <- compStep_A alg; return f_p2 alg
= return f_p3 alg
[try_p4, try_p5]
= do compStep_a; c <- compStep_S alg; return f_p4 alg
= do compStep_b; c <- compStep_A alg; d <- compStep_A
return f_p5 alg c d
compStep_B alg = msum [try_p6, try_p7]
where try_p6 = do compStep_b; c <- compStep_S alg; return f_p6 alg
try_p7 = do compStep_a; c <- compStep_B alg; d <- compStep_B
return f_p7 alg c d
compStep_a = token $ string a; compStep_b = token $ string b
c
c
c
alg
c
alg
Dieser Compiler steht auch am Ende von Java.hs, zusammen mit einer Testalgebra
sabCounter, deren Trägermengen vom Typ (Int,Int) sind. Die erste Komponente eines
Paares ist die Anzahl der a’s, die zweite die der b’s im jeweiligen Eingabewort.
228
Beispiel JavaLight-Parser
Wir wollen den JavaLight-Parser nicht nur mit verschiedenen Zielsprachen (Algebren)
instanziieren, sondern auch mit unterschiedlichen Eingabetypen parametrisieren. Deshalb gehen wir von Parser = TransM String zurück zur allgemeinen Huckepackmonade TransM state m und definieren eine Unterklasse StateParser von MonadPlus mit
einer Funktion toState, die String-Parser in Parser mit einem anderen Eingabetyp (=
state-Instanz) übersetzt:
class MonadPlus m => StateParser state m
where toState :: Parser Maybe a -> TransM state m a
StateParser hat zwei Typvariablen: state und m. Die Instanzen von m sind auf solche
der Typklasse MonadPlus beschränkt. Die Verwendung von Typklassen mit mehreren
Typvariablen erfordert die Option -fglasgow-exts beim Aufruf des ghci.
Die folgende Funktion wendet toState auf Parser von Zeichen an, die von einem anderen
Parser erkannte Objekte einschließen.
enclosed :: StateParser state m
=> String -> TransM state m a -> TransM state m a
enclosed [open,close] p = do toState $ char open; a <- p
toState $ char close; return a
229
Die StateParser-Instanz für (deterministische) String-Parser ist durch den Parserkombinator token (s.o.) gegeben:
instance Token String Maybe where toState = token
Die zweite StateParser-Instanz, mit der wir den JavaLight-Parser aufrufen wollen, lautet
wie folgt:
type Pos = (Int,Int)
instance StateParser (Pos,String) (Either Pos)
where toState p = TM token
where token (_,"")
= mzero
token ((i,_),'\n':cs) = line ((i+1,1),cs)
token (pos,c:cs) | isDelim c = token (pos,cs)
token ps
= line ps
line (pos,c:cs) | isDelim c = line (pos,cs)
line (pos@(i,j),cs)
=
case transM p cs of
Just (x,ds) -> Right (x,((i,j+length cs-length ds),ds))
_ -> Left pos
Eine Eingabe vom Typ (Pos,String) besteht aus einem String cs und dessen Anfangsposition (i, j) innerhalb der gesamten Eingabe: cs beginnt in der j-ten Spalte der i-ten
Zeile der Eingabe. Anstelle eines Zielobjektes gibt ein (Pos,String)-Parser im Fall der
scheiternden Parsierung von cs den Ausdruck Left (i,j) zurück.
230
Die (Either Pos)-Instanz von MonadPlus m (siehe Monaden) bewirkt, dass im Fall des
Scheiterns mehrerer - mit msum verknüpfter - Parser nur die Fehlermeldung desjenigen
Parsers weitergegeben wird, der das längste Eingabepräfix verarbeitet hat. Damit auch
im Erfolgsfall nur dieser Parser sein Ergebnis weitergibt, müssen alle mit msum verknüpfte Parserlisten absteigend bzgl. der Länge der jeweils erkannten Eingabepräfixe sortiert
werden!
Bis auf die Verallgemeinerung des Eingabetyps folgt die Definition des JavaLight-Parsers
der Version 3 des LL-Compiler-Schemas:
compJava :: StateParser state m
=> JavaAlg a b c d e f g h i k -> state -> m (a,state)
compJava alg = transM commands
where commands = do c <- command
msum [do cs <- commands
return $ seq_ alg c cs,
return $ embedC alg c]
231
command = msum [do toState $ char ';'; return $ skip alg,
do x <- toState $ identifier
e <- enclosed "=;" intE
return $ assign alg x e,
do toState $ string "if"; be <- boolE
cs <- enclosed "" commands
msum [do toState $ string "else"
ds <- enclosed "" commands
return $ cond alg be cs ds,
return $ cond1 alg be cs],
do toState $ string "while"; be <- boolE
cs <- enclosed "" commands
return $ loop alg be cs]
intE = do e <- product; e' <- sumSect; return $ sum_ alg e e'
sumSect = msum [do toState $ char '+'; e <- intE
return $ addSect alg e,
do toState $ char '-'; e <- intE
return $ subSect alg e,
return $ emptyS alg]
product = do e <- factor; e' <- prodSect; return $ prod alg e e'
prodSect = msum [do toState $ char '*'; e <- product
return $ mulSect alg e,
do toState $ char '/'; e <- product
return $ divSect alg e,
return $ emptyP alg]
232
factor = msum [do i <- toState int; return $ embedI alg i,
do x <- toState identifier; return $ var alg x,
do e <- enclosed "()" intE
return $ encloseI alg e]
boolE = do be <- conjunct
msum [do toState $ string "||"; be' <- boolE
return $ or_ alg be be',
return $ embedA alg be]
conjunct = do be <- literal
msum [do toState $ string "&&"; be' <- conjunct
return $ and_ alg be be',
return $ embedL alg be]
literal = msum [do b <- toState bool; return $ embedB alg b,
do e <- intE; x <- toState relation
e' <- intE; return $ rel alg x e e',
do toState $ char '!'; be <- literal
return $ not_ alg be,
do be <- enclosed "()" boolE
return $ encloseB alg be]
233
Beginnt die Ausführung des Zielcodes einer Kommandofolge cs im Zustand ([],store,0),
dann endet sie im Zustand ([],store’,length cs), wobei store’ die von compJava
javaState aus der initialen Variablenbelegung store berechnete finale Variablenbelegung ist:
execute (compJava javaStack cs) ([],store,0)
= ([],compJava javaState cs store,length cs).
Wählt man execute als Zielinterpreter, dann beschreibt die Gleichung im Sinne des
Abschnitts Compiler-Korrektheit die Korrektheit von compJava. Für ihren induktiven
Beweis müssten compJava durch den Interpreter evalCS und cs durch den Syntaxbaum
compJava javaTerm cs ersetzt werden.
Da compJava dem im Abschnitt Vom LL-Parser zum LL-Compiler angebenen Schema
eines LL-Compilers folgt, ist die Korrektheit des Parseranteils von compJava ohne weitere
Überprüfung gewährleistet.
234
Testumgebung für JavaLight
import PainterSVG (Pos,Tree(F),drawTermC,readFileAndDo)
compile :: Int -> IO ()
compile n = readFileAndDo "source" $ act
where act str = case n of 1 -> case compJava javaTerm str of
Just (t,rest)
-> do unparsedSuffix rest
writeFile "term" $ show t; drawTermC "term"
_ -> putStrLn "syntax error"
2 -> case compJava javaWord str of
Just (str,_) -> writeFile "word" str
_ -> putStrLn "syntax error"
3 -> case compJava javaDeri str of
Just (t,_) -> do writeFile "deri" $ show t
drawTermC "deri"
_ -> putStrLn "syntax error"
4 -> case compJava javaList str of
Just (f,_) -> writeFile "list" $ f True 0
_ -> putStrLn "syntax error"
5 -> case compJava javaState str of
Just (trans,_) -> loop3 trans
_ -> putStrLn "syntax error"
6 -> case compJava javaStack str of
235
Just (f,_) -> do let (code,_) = f 0
writeFile "code" $ showCode code
loop2 code
_ -> putStrLn "syntax error"
_ -> case compJava javaTerm ((1,1),str)
:: Either Pos (Commands,(Pos,String)) of
Right (t,(_,rest))
-> do unparsedSuffix rest
writeFile "term" $ show t; drawTermC "term"
Left (-2147483648,-2147483648)
minBound
-> putStrLn "symbol missing at final position"
Left pos -> putStrLn $ "wrong symbol at position "
++show pos
loop1 :: (Store -> Store) -> IO ()
loop1 trans = do (vars,initial,proceed) <- input
let f x = putStrLn $ x++" = "++show (trans initial x)
when proceed $ do mapM_ f vars; loop1 trans
loop2 :: [StackCom] -> IO ()
loop2 code = do (vars,initial,proceed) <let (_,final,_) = execute
f x = putStrLn $ x++"
when proceed $ do mapM_ f
input
code ([],initial,0)
= "++show (final x)
vars; loop2 code
236
compile n erwartet ein Quellprogramm vom Typ Commands in der Datei source und
übersetzt ihn in den Fällen n = 1 und n = 7 in einen Syntaxbaum. In den Fällen
n = 2 bis n = 6 übersetzt compile n den Quellcode in die Algebra javaWord, javaDeri,
javaList, javaState bzw. javaStack. Zusätzlich starten compile 5 und compile 6
eine Schleife, welche in jedem Durchlauf eine Variablenbelegung einliest und die durch
Interpretation des Quellprogramms in javaState bzw. durch Ausführung des Zielcodes
berechnete Variablenbelegung ausgibt.
In den Fällen n = 1 bis n = 6 wird der Parser mit der ersten, im Fall n = 6 mit der
zweiten StateParser-Instanz aufgerufen (s.o.)
Beispiele von compile 7 erzeugter Fehlermeldungen:
"while true {x=5;};y=;"
"while true {x=5+;}"
"x==5;"
;
;
;
unparsed suffix: y=;
wrong symbol at position (1,16)
wrong symbol at position (1,3)
readFileAndDo wurde im Abschnitt Transitionsmonaden definiert. Die Definitionen von
unparsedSuffix, showCode und input stehen in der Testumgebung für Expr.
237
Beispiel Alignments
Auch im folgenden Beispiel ist die Eingabe mehrdimensional. Sie besteht hier aus einem
Paar von Stringlisten xs und ys, das in die Menge alis(xs, ys) der Alignments von xs
und ys übersetzt werden soll. Vorausgesetzt wird eine Funktion
compl : String → String → Bool,
die angibt, welche Strings zueinander “komplementär” sind und deshalb aneinander “andocken” können.
Sei A = String ] {Nothing} und h : A∗ → String ∗ die Funktion, die aus einem Wort
alle Vorkommen von Nothing streicht. Dann ist für alle xs, ys ∈ String ∗
S
alis(xs, ys) =def n∈N{(a1 . . . an, b1 . . . bn, c1 . . . cn) ∈ An × An × {0, 1, 2}n |
h(a1 . . . an) = xs ∧ h(b1 . . . bn) = ys ∧
∀1≤i≤n:
(compl(ai, bi) ∧ ci = 2) ∨ (ai = bi ∧ ci = 1) ∨ ci = 0}.
Die Elemente von alis(xs, ys) lassen sich eindeutig als Terme beschreiben:
data Align = Compl String String Align | Equal String Align |
Ins String Align | Del String Align | End
Die Programme dieses Beispiels stehen auch in der Datei Align.hs.
238
Align ist die Termalgebra folgender Algebrenklasse:
data AlignAlg ali =
AlignAlg {compl :: String -> String -> ali -> ali,
equal,ins,del :: String -> ali -> ali, end :: ali}
Die Tripeldarstellung von Alignments in obiger Definition lässt sich durch folgende Algebra implementieren:
type Triple = ([Maybe String],[Maybe String],[Int])
tripAli :: AlignAlg Triple
tripAli = AlignAlg compl equal ins del end
where compl a b (s1,s2,s3) = (Just a:s1,Just b:s2,2:s3)
equal a (s1,s2,s3) = (Just a:s1,Just a:s2,1:s3)
ins a (s1,s2,s3) = (Nothing:s1,Just a:s2,0:s3)
del a (s1,s2,s3) = (Just a:s1,Nothing:s2,0:s3)
end = ([],[],[])
239
Der Parser von Stringlisten soll nicht alle Alignments ausgeben, sondern nur die mit maximalen Matches, das sind die Tripel (as, bs, cs) mit den längsten aus Einsen und Zweien
bestehenden Teilwörtern von cs. Um sie herauszufiltern, benutzen wir die Tripeldarstellung von Alignments. maxmatch t liefert die Länge des längsten Matches von t:
maxmatch :: Triple -> Int
maxmatch = f False 0 0
where f False _ n (_:s1,_:s2,i:s3) | i /= 0 = f True 1 n (s1,s2,s3)
f _ m n (_:s1,_:s2,i:s3) | i /= 0
= f True (m+1) n (s1,s2,s3)
f _ m n (_:s1,_:s2,0:s3)
= f False 0 (max m n) (s1,s2,s3)
f _ m n _
= max m n
matchcount :: Triple -> Int
matchcount (_,_,s) = length $ filter (/= 0) s
maxima :: Ord b => (a -> b) -> [a] -> [a]
maxima f s = [x | x <- s, f x == maximum (map f s)]
matchcount t zählt die Matches der Länge 1 von t. maxima f s liefert die Teilliste aller
Elemente von s, die unter f den größten Wert haben.
240
Für jede Liste ts von Alignments kann der Parser den Wert von maxima matchcount
ts induktiv während der parallelen Erzeugung der einzelnen Elemente von ts berechnen.
Den korrekten Wert von maxima maxmatch ts kann er hingegen erst ermitteln, wenn alle
Elemente von ts vollständig vorliegen.
Der Parser berechnet die Alignments durch Aufruf der Funktion align : N2 → T riple∗
an der Stelle (0, 0). align erzeugt sie in wechselseitiger Rekursion mit drei weiteren Funktionen desselben Typs. Jede der vier Funktionen liest die beiden Stringlisten xs und ys
parallel und gibt an der Stelle (i, j) ∈ N2 Alignments des (|xs| − i)-elementigen Suffixes
von xs und des (|ys| − j)-elementigen Suffixes von ys zurück, also an der Stelle (0, 0)
Alignments von xs bzw. ys selbst.
Um Mehrfachberechnungen von Funktionswerten zu vermeiden, werden die vier Funktionen als Felder dargestellt. Ist A eine Instanz der Typklasse Ix für Indexmengen, dann
wird die Einschränkung einer Funktion f : A → B auf das Intervall [m . . . n] mit mkArray
in ein Feld umgewandelt. Die Applikation f (i) wird zum Feldzugriff mkArray f!i.
mkArray :: Ix a => (a,a) -> (a -> b) -> Array a b
mkArray bounds@(m,n) f = array bounds [(x,f x) | x <- [m..n]]
241
Hier ist der vollständige Alignment-Parser:
mkAlign :: [String] -> [String] -> (String -> String -> Bool) -> [Triple]
mkAlign xs ys c = maxima maxmatch $ align!(0,0)
where alg = tripAli
align' = maxima matchcount . (align!)
lg1 = length xs; lg2 = length ys
bds = ((0,0),(lg1,lg2))
align = mkArray bds f
where f ij@(i,j) = if i == lg1
then if j == lg2 then [end alg] else ts
else if j == lg2 then us else match!ij++ts++us
where ts = insert!ij; us = delete!ij
match = mkArray bds f
where f (i,j) = if x == y then map (equal alg x) ts
else if c x y || c y x
then map (compl alg x y) ts else []
where x = xs!!i; y = ys!!j; ts = align' (i+1,j+1)
insert = mkArray bds f
where f (i,j) = map (ins alg $ ys!!j) $ align' (i,j+1)
delete = mkArray bds f
where f (i,j) = map (del alg $ xs!!i) $ align' (i+1,j)
242
Testumgebung für Alignments
import PainterSVG (drawLGraph,red,green,blue,light,readFileAndDo)
drawAlis :: String -> IO ()
drawAlis file = readFileAndDo file f
where f str = do writeFile "alignments" $ show $ concat $ zipWithIndices mkPaths alis
drawLGraph "alignments"
where (str1,str2) = break (== '\n') str
(xs,ys) = (words str1,words str2)
compl x y = (x == "a" && y == "t") ||
genetischer Code
(x == "c" && y == "g")
alis = mkAlign xs ys compl
type LPath = ([(String,Int,Int)],RGB)
mkPaths :: Int -> Triple -> [LPath]
mkPaths j (s1,s2,s3) = concat (zipWithIndices f s3) ++
[(fold2 (g 0) [] is s1,light blue),
(fold2 (g 1) [] is s2,light blue)]
where is = [0..length s1-1]
f _ 0 = []; f i 1 = [(arc i,green)]; f i _ = [(arc i,red)]
arc i = [("",i,j+j),("",i,j+j+1)]
g n path i (Just a) = path++[(a,i,j+j+n)]
g _ path _ _
= path
zipWithIndices f s = zipWith f [0..length s-1] s
243
drawAlis "file" liest zwei (untereinander geschriebene) Stringlisten aus der Datei file
und schreibt ihre Alignments als Objekte vom Typ [LPath] in die Datei alignments,
wo sie drawLGraph liest und zeichnet.
Die zwei Alignments von a c t a c t g c t und a g a t a g mit maximalen
Matches
Das Alignment von a d f a a a a a a und a a a a a a d f a mit maximalem
Match
244
` Mehrpässige Compiler
Zurück zum Abschnitt Attributierte Grammatiken. Die Trägermengen der dort vorgestellten Algebren haben folgende Form:
Inh1 × . . . × Inhm → Der1 × . . . × Dern.
Inh1, . . . , Inhm und Der1, . . . , Dern bezeichnen die Wertebereiche der jeweiligen vererbten (inherited) bzw. abgeleiteten (derived) Attribute. Für eine Signatur Σ und eine
Σ-Algebra A können wir o.B.d.A. dieselbe Trägermenge für alle Sorten von Σ voraussetzen.
Sei c : s1 × . . . × sk → s ein Konstruktor von Σ. Schaut man sich die Beispiele im
Abschnitt Attributierte Grammatiken an, dann lautet das Schema einer Interpretation
von cA : Ak → A von c in A offenbar wie folgt:
cA(f1, . . . , fk )(x1, . . . , xm) = (e1, . . . , en) where (x11, . . . , x1n) = f1(e11, . . . , e1m)
...
(1)
(xk1, . . . , xkn) = fk (ek1, . . . , ekm)
245
Ist s → w0s1w1 . . . sk wk die Grammatikregel, aus der c hervorgeht, dann wird (1) üblicherweise als Liste von Attributzuweisungen an die Regel geschrieben:
s → w0s1w1 . . . sk wk s.der1 := e1 . . . s.dern := en s1.inh1 := e11 . . . s1.inhm := e1m
...
sk .inh1 := ek1 . . . sk .inhm := ekm
Für jede Belegung der Variablen f1, . . . , fk , x1, . . . , xm bilden die lokalen Definitionen
hinter der where-Klausel von (1) ein Gleichungssystem. cA ist genau dann wohldefiniert,
wenn es in den Variablen xij , 1 ≤ i ≤ k, 1 ≤ j ≤ n, eindeutig lösbar ist. Nach dem
Fixpunktsatz von Knaster und Tarski ist das der Fall, wenn alle Funktionen h, aus denen
die Ausdrücke eij , 1 ≤ i ≤ k, 1 ≤ j ≤ m, zusammengesetzt sind, monoton bezüglich
der flachen Verbände
Inh1 ∪ {⊥}, . . . , Inhm ∪ {⊥}, Der1 ∪ {⊥}, . . . , Dern ∪ {⊥}
sind, wenn also x = ⊥ aus h(x) = ⊥ folgt.
246
⊥ stellt hier den Wert jedes Ausdrucks dar, dessen Auswertung nicht terminiert. Die
Monotonie von h hängt also von der Strategie ab, nach der ein Ausdruck h(e) ausgewertet
wird, insbesondere im Fall, dass die Auswertung von (Teilen von) e nicht terminiert.
Die Strategie muss aber nicht nur Monotonie gewährleisten, sie muss vor allem eine
Fixpunktstrategie sein, d.h. genau die Lösung von (1) berechnen, die der Fixpunktsatz
liefert. Die von Haskell verwendete lazy-evaluation-Strategie hat diese Eigenschaft,
eine innermost-Strategie, die eine Auswertung von h(e) mit der Auswertung von e beginnt,
hat sie nicht!
Wir wollen die theoretischen Grundlagen dieser Ergebnisse hier nicht weiter vertiefen,
aber noch anmerken, dass die Betrachtung flacher Verbände nicht ausreicht, obwohl die
Lösungen von (1) in solchen Verbänden liegen. Kommen nämlich in den Ausdrücken eij
nicht nur Standardfunktionen, sondern auch rekursiv definierte vor, dann gehen deren
Rekursionsgleichungen in die Strategie und die Monotonieforderung ein. Die Lösungen
dieser Gleichungen liegen jedoch in (nichtflachen) Funktionsverbänden! Lazy evaluation
ist allerdings auch dann eine Fixpunktstrategie.
247
Es gibt also immer eine Lösung von (1). Da sie aus Elementen flacher Verbände besteht,
ist sie sogar - fast - eindeutig: Gibt es mehrere Lösungen, dann sind alle bis auf eine gleich
⊥. Das wäre jedoch schlecht, denn ist ⊥ eine Lösung, dann liefert jede Fixpunktstrategie
genau diese, was bedeutet, dass die Lösungssuche nicht terminiert! Wir brauchen also
Bedingungen an (1), deren Gültigkeit ausschließt, dass ⊥ eine Lösung von (1) ist.
Eine solche Bedingung ist die Zyklenfreiheit der Benutzt-Relation der Variablen von
(1). Um die Benutzt-Relation zu ermitteln, zerlegen wir (1) in ein System regulärer Gleichungen, deren linke Seiten Variablen der Menge X = {xij | 1 ≤ i ≤ k, 1 ≤ j ≤ n}
sind:
x11 = π1(f1(e11, . . . , e1m))
...
x1n = πn(f1(e11, . . . , e1m))
...
xij = πj (fi(ei1, . . . , eim))
...
xk1 = π1(fk (ek1, . . . , ekm))
...
xkn = πn(fk (ek1, . . . , ekm))
πi liefert die i-te Komponente eines m-Tupels.
248
(2)
xij ∈ X benutzt x ∈ X, wenn x in πj (fi(ei1, . . . , eim)) vorkommt. Ist die BenutztRelation azyklisch, dann kann man die Gleichungen von (2) so anordnen, dass die Ausdrücke auf ihren rechten Seiten in der Reihenfolge ihres Auftretens im (umgeordneten)
Gleichungssystem ausgewertet werden können, die Lösungsberechnung also terminiert
und damit einen Wert ungleich ⊥ liefert.
Enthält die Benutzt-Relation einen Zyklus, dann kann man versuchen, die parallele Berechnung und Verwendung von Werten aller abgeleiteten bzw. vererbten Attributmenge
so zu sequentialisieren, dass in jedem Schritt zwar nur eine Teilmenge berechnet bzw.
verwendet wird, die Komposition der Schritte jedoch eine äquivalente Definition von cA
liefert. Wir wollen die einzelnen Attribute über Indizes identifizieren und stellen daher
die Trägermenge von A folgendermaßen dar:
Inh1 × . . . × Inhm → Derm+1 × . . . × Dern.
Es gibt also insgesamt n Attribute. Die ersten m sind vererbt, die restlichen n − m sind
abgeleitet. Der Wertebereich eines Attributs i ≤ m ist Inhi, der eines Attributs i > m
ist Deri.
249
Wir passen (1) an diese Notation an:
cA(f1, . . . , fk )(x01, . . . , x0m) = (e(k+1)(m+1), . . . , e(k+1)n)
where (x1(m+1), . . . , x1n) = f1(e11, . . . , e1m)
...
(xk(m+1), . . . , xkn) = fk (ek1, . . . , ekm)
(3)
Der Index i einer Variablen xai oder eines Ausdrucks edi bezeichnet also das Attribut, in
dessen Wertebereich xai bzw. edi liegt (a für Anwendungsstelle, d für Definitionsstelle).
Der Attributabhängigkeitsgraph für c,
depgraph(c)(i, j) ⊆ {0, . . . , k} × . . . × {1, . . . , k + 1},
liefert für je zwei Attribute i und j die Abhängigkeiten bei ihrer Berechnung bzw. Verwendung in (3):
depgraph(c)(i, j) =def {(a, d) | xa,i kommt in ed,j vor}.
Man sieht sofort, dass (3) eine eindeutige Lösung hat, die durch sequentielle Auswertung
der Ausdrücke in (3) berechnet werden kann, wenn
für alle Attribute i, j und (a, d) ∈ depgraph(c)(i, j) a < d
gilt.
250
(4)
Ist (4) verletzt, dann versuchen wir, die gesamte Attributmenge At = {1, . . . , n} so in
Teilmengen At1, . . . , Atr zu zerlegen, dass für alle Attribute i, j und Pässe 1 ≤ p, q ≤ r
(4) nur dann nicht gilt, wenn i in einem früheren Pass als j berechnet wird:
i ∈ Atp ∧ j ∈ Atq
=⇒
p < q ∨ (p = q ∧ ∀ (a, d) ∈ depgraph(c)(i, j) : a < d). (5)
Gilt (5), dann lassen sich die Attributberechnungen und -verwendungen in (3) so umordnen, dass (3) auch in diesem Fall eine eindeutige Lösung hat, die man durch sequentielle
Auswertung der Ausdrücke in (3) erhält. Für alle Pässe 1 ≤ p ≤ r und
f : Inh1 × . . . × Inhm → Derm+1 × . . . × Dern
bezeichne π p die Projektion eines Attributwertetupels auf das Teiltupel der Komponenten,
deren Indizes zu Atp gehören, und
f p : π p(Inh1 × . . . × Inhm) → π p(Derm+1 × . . . × Dern)
die entsprechende Einschränkung von f , d.h. f 1 × . . . × f r = f .
251
Das obige Schema einer Konstruktorinterpretation
cA(f1, . . . , fk )(x01, . . . , x0m) = (e(k+1)(m+1), . . . , e(k+1)n)
where (x1(m+1), . . . , x1n) = f1(e11, . . . , e1m)
...
(xk(m+1), . . . , xkn) = fk (ek1, . . . , ekm)
(3)
wird zu:
cA(f1, . . . , fk )(x01, . . . , x0m) = (e(k+1)(m+1), . . . , e(k+1)n)
where π 1(x1(m+1), . . . , x1n) = f11(π 1(e11, . . . , e1m))
...
π 1(xk(m+1), . . . , xkn) = fk1(π 1(ek1, . . . , ekm))
...
π r (x1(m+1), . . . , x1n) = f1r (π r (e11, . . . , e1m))
...
π r (xk(m+1), . . . , xkn) = fkr (π r (ek1, . . . , ekm))
252
Der LAG-Algorithmus (Left-to-right-Attributed-Grammar) berechnet die kleinste Zerlegung der Attributmenge At, die (5) erfüllt, sofern eine solche existiert.
Sei constrs die Menge aller Konstruktoren und ats die Menge aller Attribute der
gegebenen Grammatik. Ausgehend von der einelementigen Zerlegung [ats] verändert
check_partition die ersten beiden Elemente der jeweils aktuellen Zerlegung (next und
curr), bis entweder next leer ist und damit eine Zerlegung gefunden wurde, die (5) erfüllt,
oder curr leer ist, was bedeutet, dass keine solche Zerlegung existiert.
Bei depgraph c wird der Attributabhängigkeitsgraphen für den Konstruktor c berechnet.
Beachte auch die drei Aufrufe von foldl, die drei ineinandergeschachtelten Schleifen
entsprechen!
253
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 gefunden
(_,[]) -> []
keine Zerlegung möglich
_ -> check_partition [] (next':curr':partition) Zerlegung erweitern
where (next',curr',changed) =
foldl check_constr (next,curr,False) constrs
check_constr state c = foldl (check_atpair dep) state
[(a,d) | a <- ats, d <- ats, not (null dep (a,d))]
where dep = depgraph c
check_atpair dep state atp = foldl (check_dep atpair) state (dep atpair)
check_dep (a,d) (next,curr,changed) (i,j) =
if (a `elem` next || (a `elem` curr && i>=j)) && d `elem` curr
Die aktuelle Zerlegung next:curr:... verletzt (5).
then (d:next,curr`minus`[d],True) else (next,curr,changed)
d wird vom vorletzten zum letzten Zerlegungselement next verschoben.
254
` Korrektheit des JavaLight-Compilers*
In den Abschnitten Rund um Expr bzw. Transitionsmonaden haben wir die Korrektheit
des Expr- bzw. JavaLight-Compilers durch direkten Bezug auf die Kommutativität des
Diagramms
TΣ
evalSem
evalZiel
-
(1)
execute
?
?
Sem
Ziel
-
encode
M ach
aus dem Abschnitt Compiler-Korrektheit plausibel gemacht. In diesem Abschnitt wurde
aber auch ein Kriterium für die Kommutativität genannt, das sich aus der Initialität
der Termalgebra TΣ ergibt: Sind alle vier Abbildungen von (1) Σ-homomorph, dann
kommutiert (1). Gilt das für (die Signatur von) JavaLight?
255
Für die Korrektheitsskizze im Abschnitt Transitionsmonaden haben wir javaState (siehe
Ein Haskell-Datentyp für Algebren) als Semantik A der Quellsprache, javaStack als
Zielsprache, State −◦→ State mit
State = Int ∗ × Store × Int
(Keller, Speicher, Befehlszähler)
als Semantik B der Zielsprache und execute als Zielinterpreter gewählt.
Für zwei Mengen A und B bezeichnet A −◦→ B die Menge der partiellen Funktionen von
A nach B. Bekanntermaßen ist die funktionale Interpretation eines Programms an den
Stellen, an denen es nicht terminiert, undefiniert. Deshalb müssen die Assemblerprogramme von javaStack als partielle Funktionen interpretiert werden. Das Gleiche gilt für die
(abstrakten) Quellprogramme von JavaLight. Sie können den Konstruktor loop enthalten, für den wir in der Definition von javaState nur die folgende Gleichung angegeben
haben:
loop be c st = if be st then loop be c $ c st else st
(3)
Mit dem in der Definition von javaState verwendeten Haskelltyp Store -> Store ist
also Store−◦→ Store gemeint. Ist (3) überhaupt – in der Variablen loop(be) – lösbar? Die
Lösung wäre auf jeden Fall eine Funktion des Typs (Store −◦→ Store) → (Store −◦→
Store).
256
Allgemein gilt: Für zwei Mengen A und B bilden die partiellen Funktionen von A nach
B einen ω-vollständiger t-Halbverband (semi-lattice), d.h. A −◦→ B besitzt eine
Halbordnung ≤, ein kleinstes Element ⊥ und Suprema ti∈Nai aller aufsteigenden Ketten
a1 ≤ a2 ≤ . . .
Das kleinste Element von A−◦→ B ist die nirgends definierte Funktion. Die Halbordnung
ist wie folgt definiert:
f ≤g
⇐⇒def
∀ a ∈ A : f (a) definiert ⇒ f (a) = g(a).
Eine Funktion h : A → B zwischen zwei aufwärtsvollständigen Verbänden heißt aufwärtsstetig, wenn für alle aufsteigenden Ketten a1 ≤ a2 ≤ . . . von A
h(ti∈Nai) = ti∈Nh(ai)
gilt. Ist A = B, dann ist nach dem Fixpunktsatz von Kleene
lfp(h) =def ti∈N hi(⊥)
der kleinste Fixpunkt von h, m.a.W. die kleinste Lösung der Gleichung x = h(x) in x.
257
Damit erhalten wir die im Abschnitt Ein Haskell-Datentyp für Algebren angekündigte
Definition von loopjavaState, die Gleichung (3) erfüllt:
Für alle be : Store → Bool und c : Store −◦→ Store,
loopjavaState(be)(c) =def ti∈N hibe,c(⊥),
wobei
hbe,c : (Store −◦→ Store) → Store −◦→ Store
für alle f : Store −◦→ Store und st ∈ Store definiert ist durch:
f (c(st)) falls be(st) = True,
hbe,c(f )(st) =
st
sonst.
Damit der Zielinterpreter execute JavaLight-homomorph wird, vereinfachen wir seinen
Definitions- und Wertebereich so, dass er - analog zu evalSem - einer kanonische Abbildung
entspricht, die automatisch homomorph ist. Da die Zielsprache und ihre Semantik nichts
mit JavaLight zu tun haben, werden ihnen zunächst andere Signaturen zugrundegelegt
und sie dann so zu JavaLight-Algebren erweitert, dass JavaLight-homomorph wird:
258
Wir bilden execute aus einer Auswertungs- und einer Entfaltungsfunktion bzgl. einer
Konstruktor- bzw. Destruktorsignatur CΣ = (S, CF, BΣ, BA) bzw. DΣ = (S, DF, BΣ, BA
Die Assemblerprogramme von javaStack werden als CΣ-Gleichungselemente (s.u.) einer ComE genannten DΣ-Algebra formuliert, während die Semantik der Zielsprache als
CΣ-Algebra PF dargestellt wird, die State −◦ → State auf State0 −◦ → State0 mit
State0 = Int ∗ × Store reduziert.
Die Sorten von CΣ und DΣ sind dieselben: S = {com, Int, String}. Int und String sind
Basissorten und werden immer durch Z bzw. eine Menge von Zeichenfolgen interpretiert.
Die uniformen Erweiterungen von ComE und PF zu JavaLight-Algebren nennen wir
javaComE bzw. javaPF . Beide sind - genauso wie javaStack - Instanzen desselben
Typs von JavaLight-Algebren:
type ICI com = Int -> (com,Int)
type javaExt com = JavaAlg (ICI com) (ICI com) (ICI com) (ICI com)
[ICI com] [ICI com] (ICI com)
javaStack :: javaExt [StackCom]
javaComE :: javaExt ComE
javaPF
:: javaExt (State' -> State')
259
Wir müssen javaExt von der oben definierten Abbildung einer Menge (com) auf eine
JavaLight-Algebra erweitern zum Funktor von der Kategorie der Mengen und Abbildungen zur Kategorie der JavaLight-Algebren und -Homomorphismen, d.h. wir brauchen
noch eine Funktion
javaFun :: (a -> b) -> javaExt a -> javaExt b
Die zu finden ist nicht schwierig, genausowenig wie eine Abbildung
mkEqs : [StackCom] → ComE
(s.u.) und der JavaLight-Homomorphismus encode : javaState → javaPF zwischen
Quell- und Zielsprachensemantik. Am Ende sind tatsächlich alle Abbildungen von (1)
JavaLight-homomorph und wir können die Korrektheit des JavaLight-Compilers aus der
Initialität der JavaLight-Termalgebra folgern:
TΣ(JavaLight)
evaljavaState
evaljavaStack-
javaStack
javaF un(mkEqs)-
javaComE
(4)
javaF un(I)
?
javaState
?
-
encode
260
javaPF
execute ist - wie oben bemerkt - die Komposition zweier Abbildungen:
execute
•
-
•
6
ComE
unfold ComE
-
CTCΣ
evalPF-
?
PF
Die Schnittstelle ist CTCΣ, die Algebra der endlichen oder unendlichen (!) CΣ-Bäume.
Hier sind zunächst einmal die Konstruktoren und Destruktoren von CΣ bzw. DΣ:
CF = { sub, pow, gr, inv, jump :: com → com,
push, add, mul :: com Int → com,
load, save :: com String → com,
jumpC :: com com → com,
halt :: com }
Bis auf halt und die Kleinschreibung entsprechen die Namen der Konstruktoren denen von
StackCom. Sie sind aber hier keine Konstanten, die zu Listen zusammengefasst werden,
die dann die Programme bilden. Stattdessen hat - bis auf halt - jeder com-Konstruktor f
mindestens ein Argument der Sorte com, das einer auf das f entsprechende StackComKommando folgenden Befehlsliste entspricht.
261
DF besteht aus genau einem Destruktor, nämlich



({sub, pow, gr, inv, jump} × com) + 






({push, add, mul} × com × Int) +
δ :: com →
({load, save} × com × String) +





 ({jumpC} × com × com) + {halt} 

Mit CF werden Zielprogramme zusammengesetzt, mit δ werden sie zerlegt.
Der Range von δ verwendet - abweichend von unserer obigen Definition einer Destruktorsignatur - die Typkonstruktoren + und ×. Im Abschnitt über Coalgebren wurde bereits
auf die Notwendigkeit der Verwendung von + hingewiesen.
Für eine endliche Menge C stehen auch die Copotenzen (copowers) wie z.B. C × s × s0
für Summen, z.B. C × s × s0 für die Summen von |C| vielen Kopien von s × s0. Die
verbleibenden (inneren) Produkte wie s × s0 werden als eigene Sorten betrachtet. Ihre
Destruktoren sind die jeweiligen Projektionen, z.B. π1 : s × s0 → s und π2 : s × s0 → s0.
262
Die CΣ-Algebra PF
PF com = State0 −◦→ State0
subPF , powPF , grPF , inv PF , , jumpPF :: PF com → PF com
subPF (f ) = f ◦ g where g(a : b : stack, store) = ((b − a) : stack, store)
powPF (f ) = f ◦ g where g(a : b : stack, store) =(ba : stack, store)
(1 : stack, store) falls b > a
grPF (f ) = f ◦ g where g(a : b : stack, store) =
(0 : stack, store) sonst
g(0 : stack, store) = (1 : stack, store)
inv PF (f ) = f ◦ g where
g(1 : stack, store) = (0 : stack, store)
jumpPF (f ) = f
pushPF , addPF , mulPF : PF com × Int → PF com
pushPF (f, a) = f ◦ g where g(stack, store) = (a : stack, store)
addPF (f, n) = f ◦ g where g(a1 : ... : an : stack, store) = ((an + ... + a1) : stack, store)
mulPF (f, n) = f ◦ g where g(a1 : ... : an : stack, store) = ((an ∗ ... ∗ a1) : stack, store)
loadPF , savePF : PF com × String → PF com
loadPF (f, x) = f ◦ g where g(stack, store) = (store(x) : stack, store)
savePF (f, x) = f ◦ g where g(a : stack, store) = (stack, update(store)(x)(a))
263
In allen nicht aufgeführten Fällen ist g und damit auch f ◦ g undefiniert! f ist, intuitiv
gesagt, die Semantik des Zielprogramms, das auf den sub-, pow-, gr- bzw. inv-Befehl
folgt.
jumpC PF : PF com × PF com → PF com
jumpC PF (f, g)(1 : stack, store) = f
jumpC PF (f, g)(0 : stack, store) = g
Die DΣ-Algebra ComE
Sei Σ eine Konstruktorsignatur mit Sortenmenge S, Basissortenmenge BS und Basisalgebra BA. Für alle n > 0 sei Xn eine n-elementige Menge (S \ BS)-sortierter Variablen,
s ∈ S \ BS und x ∈ Xn,s.
Eine (S \ BS)-sortierte Funktion E : Xn → (TΣ(Xn) \ Xn) heißt ideales Σ-Gleichungssystem und das Paar (x, E) Σ-Flussdiagramm der Sorte s. Die (S \ BS)-sortierte
Menge der Σ-Flussdiagramm bezeichnen wir mit F lowΣ.
Jedes Element (x, E) von F lowΣ beschreibt einen Graphen, dessen Knotenmarkierungen
Konstruktoren oder Elemente von BA sind. Er setzt sich zusammen aus den Bildern von
E - die Terme, also endliche Bäume sind -, wobei jede dort auftretende Variable z ∈ Xn
mit der Wurzel von E(z) gleichgesetzt wird. x stelle man sich als “Eingangsknoten” vor.
264
ComEcom = TCΣ(F lowCΣ)com


({sub, pow, gr, inv, jump × ComEcom}) ∪



({push, add, mul} × ComEcom × Int) ∪
δ ComE : ComEcom →
({load, save} × ComEcom × String) ∪



 ({jumpC} × ComE × ComE ) ∪ {halt}
com
com
∀ (x, E) ∈ F lowCΣ,com : E(x) = f t ⇒ δ ComE (x, E) = (f, t)
∀ f :: w → com ∈ CF, t ∈ TCΣ(F lowCΣ)w : δ ComE (f t) = (f, t)
Demnach muss die oben verlangte Funktion mkEqs : [StackCom] → ComE ein Assemblerprogramm in sein Flussdiagramm übersetzen. Sie könnte also in Haskell wie folgt
definiert werden:
type Eqs = Int -> (Label,[Int])
data Label = Sub_ | Pow_ | Gr_ | Inv_ | Jump_ | Push_ | Add_ | Mul_ |
Load_ | Save_ | JumpC | Halt
mkEqs :: [StackCom] -> Eqs
mkEqs coms = listsToFun (Halt,[]) [0..length coms-1] $ map f coms
where lcoms = zip labs coms
labs = [0..length coms]
265
targets = [k | (_,Jump(k)) <- coms]++
concat[[k,i+1] | (i,JumpT(k)) <- coms]++
concat[[k,i+1] | (i,JumpF(k)) <- coms]
********
266
Definition von CTΣ für eine beliebige Konstruktorsignatur Σ = (S, F, BΣ, BA)
Σ-Bäume sind partielle Funktionen von N∗ nach F : Für alle s ∈ S ist CTΣ,s die Menge
aller t : N∗ −◦→ F mit folgenden Eigenschaften:
t = ⊥s ∨ ran(t()) = s,
∀ w ∈ N∗, i ∈ N : ran(t(wi)) = si ⇒
∃ n > i, s0, . . . , si−1, si+1, . . . , sn−1 ∈ S :
dom(t(w)) = s0 . . . si . . . sn−1.
Die Funktionssymbole von Σ werden in CTΣ wie folgt interpretiert:
Sei f :: s0 . . . sn−1 → s ∈ F und für alle 0 ≤ i < n sei ti ∈ CTΣ,si .
f CTΣ (t0, . . . , tn−1)() = f,
∀ i ∈ N, w ∈ N∗ : f CTΣ (t0, . . . , tn−1)(iw) =
267
ti(w)
falls i < n,
undefiniert sonst.
Da die Trägermengen von CTΣ aus partiellen Funktionen bestehen, bilden sie einen ωvollständigen t-Halbverband, dessen Halbordnung, kleinstes Element und Suprema wie
oben für eine beliebige Menge A −◦→ B definiert sind.
Mehr noch: die Interpretationen der Funktionssymbole von Σ sind aufwärtsstetig. Damit gehört CTΣ einer Unterkategorie der Klasse aller Σ-Algebren an, deren Objekte wir
aufwärtsstetige Σ-Algebren nennen.
Im Abschnitt Initiale Algebren haben wir die Termalgebra TΣ als initiale Σ-Algebra charakterisiert, d.h. für jede Σ-Algebra A gibt es genau einen Σ-Homomorphismus von TΣ
nach A. CTΣ hat die gleiche Eigenschaft, sofern wir vor die Substantive Algebra und
Homomorphismus das Adjektiv aufwärtsstetig setzen. Man bezeichnet CTΣ deshalb als
initiale aufwärtsstetige CΣ-Algebra.
268
` Deklarationen und ihre Adressierung*
Erweiterung von JavaLiight
konkrete Syntax
exp
→ Int
| Bool
| String
| fun String
| String(actList)
| exp∧
| exp[exp]
| exp. String
actList → exp∗
abstrakte
Ci:
Cb:
Id:
Fid:
Apply:
Deref
Af:
Rf:
Actuals:
269
Syntax
Int → exp
Bool → exp
String → exp
String → exp
String actList → exp
exp → exp
exp exp → exp
exp String → exp
exp∗ → actList
konkrete Syntax
abstrakte Syntax
MkProg:
stat → prog
prog
→ stat
Skip:
stat
stat
→ Assign:
exp exp → stat
| exp := exp
Read:
String → stat
| read String
Write:
exp → stat
| write exp
Var:
String type → stat
| var String: type
New:
String → stat
| new String
Main:
stat → stat
| begin stat end
String parList type stat → stat
| fun String(parList):type is stat end Fun:
For:
String type → par
par
→ String:type
Funfor:
String type → par
| fun String:type
∗
Formals:
par∗ → parList
parList → par
INT:
type
type
→ Int
BOOL:
type
| Bool
POINTER: type → type
| ^type
ARRAY:
Int Int type → type
| array [Int..Int] type
RECORD: stat → type
| record stat
Name:
String → type
| String
270
Attributbereiche
type
type
type
type
type
data
Symtab = Id -> (TypeDesc,Int,Int)
transient
Depth = Int
vererbt
Label = Int
vererbt
Code = [Command]
abgeleitet
Offset = Int
abgeleitet
TypeDesc = INT | BOOL | Pointer TypeDesc | Func Int TypeDesc |
Formalfunc TypeDesc | Array Int Int TypeDesc |
Record (String -> (TypeDesc,Int,Int)) | Name String
abgeleitet
Symtab liefert die jeweils aktuelle Symboltabelle, Depth die Schachtelungstiefe des aktuellen Scopes, also
des innersten Blocks, in dem der gerade übersetzte Ausdruck auftritt, Label die nächste freie Befehlsnummer, Code den Zielcode und Offset den Platzbedarf für eine Liste aktueller Parameter.
271
data ImpAlg exp acts prog stat par parList type =
ImpAlg {ci :: Int -> exp, cb :: Bool -> exp,
id :: String -> exp, fid :: String -> exp,
apply :: String -> actList -> exp,
deref :: exp -> exp,
af :: exp -> exp -> exp, rf :: exp -> String -> exp,
actuals :: [exp] -> acts,
mkProg: stat -> prog,
skip: stat, assign: exp -> exp -> stat,
read: String -> stat, write: exp -> stat,
var: String -> type -> stat,
new: String -> stat,
main: stat -> stat,
fun: String -> parList -> type -> stat -> stat,
for, funfor: String -> type -> par,
formals: [par] -> parList,
int, bool: type, pointer: type -> type,
array: Int -> Int -> type,
record: stat -> type,
name: String -> type}
272
assemblyAlg :: ImpAlg (Symtab -> Depth -> Label -> (Code,TypeDesc)) Trägermenge für
(Symtab -> Depth -> Label -> (Code,Offset)
Trägermenge für
Code
Trägermenge für
(Symtab -> Depth -> Reladr
-> Label -> (Code,Symtab,Reladr))
Trägermenge für
(Symtab -> Depth -> Reladr
-> (Symtab,Reladr))
Trägermenge für
(Symtab -> Depth -> Reladr -> Symtab)
Trägermenge
für parList
TypeDesc
Trägermenge für
exp
acts
prog
stat
par
type
assemblyAlg = ImpAlg ci cb id fid apply deref af rf not_ leq add sub mul actuals
mkProg skip assign read write seq cond loop typedef var new
main fun for funfor int bool pointer array record name
where mkProg stat = code
where (code,_,_) = stat (const (INT,0,0)) 0 0 0
... siehe mein Übersetzerbau-Skript, Kapitel 5
273
Literatur
[1] R. Giegerich, C. Meyer, Algebraic Dynamic Programming, Proc. AMAST 2002,
Springer LNCS 2422 (2002) 249-364
[2] J.A. Goguen, J.W. Thatcher, E.G. Wagner, J.B. Wright, Initial Algebra Semantics
and Continuous Algebras, J. ACM 24 (1977) 68-95
[3] D. Knuth, Semantics of Context-Free Languages, Mathematical Systems Theory 2
(1968) 127-145; Corrections: Math. Systems Theory 5 (1971) 95-96
[4] E. Moggi, Notions of Computation and Monads, Information and Computation 93
(1991) 55-92
[5] E.G. Manes, M.A. Arbib, Algebraic Approaches to Program Semantics, Springer
(1986)
[6] R.N. Moll, M.A. Arbib, A.J. Kfoury, Introduction to Formal Language Theory,
Springer (1988)
[7] P. Padawitz, Formale Methoden des Systementwurfs,
http://fldit-www.cs.uni-dortmund.de/∼peter/TdP96.pdf
[8] W.C. Rounds, Mappings and Grammars on Trees, Mathematical Systems Theory 4
(1970) 256-287
274
[9] J.J.M.M. Rutten, Automata and coinduction (an exercise in coalgebra), Proc. CONCUR ’98, Springer LNCS 1466 (1998) 194–218
[10] K. Sen, G. Rosu, Generating Optimal Monitors for Extended Regular Expressions,
Proc. Runtime Verification ’03, Elsevier ENTCS 89 (2003)
[11] J.W. Thatcher, E.G. Wagner, J.B. Wright, More on Advice on Structuring Compilers
and Proving Them Right, Theoretical Computer Science 15 (1981) 223-249
[12] 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
275
Index
BΣ, 10
TΣ(X), 12
BA, 10
TΣ, 12, 16
Σ(FPF), 29
Σ(JavaLight), 30
Σ(G), 27
λ-Abstraktion, 36
λ-Applikation, 36
ω-vollständiger t-Halbverband, 257
∼ParseE , 68
state(ϕ), 99
abgeleitetes Attribut, 174
Abl(G), 41
Ableitungsalgebra, 41
abstrakte Syntax, 27
Abstraktion, 75
Accept, 58
Aktionstabelle, 101
Algebra, 13, 32
Alignments, 238
Allrelation, 61
aufwärtsstetige Funktion, 257
Basisalgebra, 10
Basissignatur, 10
Baumautomat, 14
Beh, 53
Beobachter, 53
bewachte CFG, 48
Bildautomat, 57
bottom-up-Parser, 93
CFG, 24
Coalgebra, 54
Coinduktion, 61
compile, 85
compileE, 156
276
Compiler, 6
Compiler-Korrektheit, 19, 35
compStep, 85
Datentypen, 132, 159
Destruktor, 10
dom, 10
Domain, 10
Fixpunktsatz von Knaster und Tarski, 43
flacher Verband, 246
Flow, 264
Flussdiagramm, 264
follow, 95
FPF, 25
fst, 95
Funktionssymbol, 10
ECFG, 83
eindeutig, 33
erweiterte kontextfreie Grammatik, 83
eval, 16
evalRel, 172
execute, 188
executeE, 156
Expr, 132
Gleichungssystem, 18
Gleichungssystem einer CFG, 43
GLS(G), 43
goto-Tabelle, 103
Grammatikregel, 24
Grundterm, 12
Faktor, 18
Felder, 241
finale Algebra, 54
Fixpunktsatz für CFGs, 49
Fixpunktsatz von Kleene, 44, 257
ideales Gleichungssystem, 264
Induktion, 61
initiale Algebra, 16
initiale aufwärtsstetige Algebra, 268
Interpreter, 6
Homomorphismus, 15, 32
277
isomorph, 15
Korrektheit monadischer Parser, 35
JavaAlg, 163
javaDeri, 168
JavaLight, 26
JavaLight-Compiler, 186
JavaLight-Parser, 229
javaList, 184
javaStack, 189
javaState, 171
javaTerm, 164
javaWord, 167
Lösung eines Gleichungssystems, 18
Lang, 22
Linksrekursion, 80
Listen, 121
Listenfaltungen, 127
Listenkomprehension, 131
LL-Compiler, 85, 226
LL-Parser, 82
LR(k)-Grammatik, 96
LR-Automat für G, 99
LR-Compiler, 105
LR-Parser, 93
Kellermaschine, 154, 186
Kern, 71
kleinste Unteralgebra, 33
Konfluenzkriterien, 69
Kongruenz, 61
Konstante, 10
Konstruktor, 10
kontextfreie Grammatik, 24
Korrektheit des JavaLight-Compilers, 255
Monaden, 191
monomorph, 120
NF, 69
Nichtterminal, 24
Normalform, 69
Normalformalgebra, 69
278
Obs, 53
Parse, 66
Parse(N), 77
ParseE, 67
ParseE(G), 77
ParseE-Äquivalenz, 68
Parser, 5, 38
Plusmonade, 35
polymorph, 120
ran, 10
Range, 10
recursive descent, 88
Redukt, 14
Reg, 21
Reg(N), 77
Reguläre Ausdrücke, 21
reguläres Gleichungssystem, 44
Restriktion, 75
SAB, 28
Scanner, 5
Signatur, 10
sortierte Funktion, 15
sortierte Menge, 11
Sprache einer CFG, 33
spracherkennender Automat, 58
StackCom, 154
State, 186
StateE, 154
Store, 134
Syntaxbaum, 27, 33
Term, 12
Termalgebra, 16
Termauswertung, 17
Termersetzung, 66
Termidentität, 61
Terminal, 24
Testumgebung für Alignments, 243
Testumgebung für JavaLight, 235
Testumgebung für Expr, 223
top-down-Parser, 82
279
Trägermenge, 13
transientes Attribut, 174
Typklassen, 140
unfold, 54
Unteralgebra, 33
Variablenbelegung, 17
vererbtes Attribut, 174
Verhaltensgleichheit, 61
Wächter, 37
Word(G), 32
Wortalgebra, 32
Wortmengenalgebra, 22
Zustandssorte, 52
280
Herunterladen