Kapitel 11 - Grundlagen der Programmiersprachen - A. Schwill - 1997

Kapitel 11 - Grundlagen der Programmiersprachen - A. Schwill - 1997
11-1
11 Grundlagen der Programmiersprachen
In diesem Abschnitt verfolgen wir das Ziel, Programmiersprachen exakt zu beschreiben.
Die beiden wichtigsten Aspekte, die eine Programmiersprache kennzeichnen, sind Syntax und Semantik.
Die Syntax legt fest, welche Aneinanderreihungen von Zeichen korrekte Sätze der Sprache sind. Die Semantik bestimmt die inhaltliche Bedeutung aller Sätze der Sprache.
Syntax und Semantik bilden also ein untrennbares Paar, wenn es um die Beschreibung
von Sprachen, und speziell von Programmiersprachen geht. Folglich definiert man eine
Programmiersprache P als ein Paar P=(L,F), wobei L⊆A* eine formale Sprache über
dem Alphabet A={a,...,z,A,...Z,0,...,9,:,;,-,...} ist und die Syntax von P bestimmt und F eine
Abbildung, die jedem Wort (Programm) w∈L seine Bedeutung (Semantik) zuordnet.
Für die exakte Beschreibung von Syntax und Semantik von Programmiersprachen gibt
es eine Reihe von Methoden, von denen wir die wichtigsten im folgenden vorstellen.
11.1 Syntax
In Kapitel 2 hatten wir bereits gesehen, daß Programmiersprachen stets eine feste, präzise Syntax besitzen müssen. In dieser Kurseinheit definieren wir zunächst den Begriff
der Sprache und führen anschließend einige Methoden ein, mit denen man die Syntax
von Sprachen exakt festlegen kann.
11.1.1 Alphabet und Sprache
(Natürliche) Sprachen (Deutsch, Englisch, Französisch, Japanisch, Kisuaheli usw.) werden von Menschen zum Informationsaustausch und zu allen Zwecken der Kommunikation verwendet. Die Beherrschung einer Sprache und ihrer Ausdrucksmöglichkeiten
beeinflußt stark die Vorstellungswelt und die Denkweise von Menschen. Zu jeder natürlichen Sprache existiert in der Regel eine Schriftsprache; manche Sprachen (z.B. Hethitisch) liegen nur noch in dieser Form vor.
In der Informatik interessiert man sich für künstliche Sprachen, unter denen die Programmiersprachen die wichtigsten sind. Künstliche Sprachen wurden Ende des 19. Jahrhunderts entwickelt, um Fakten, Denkabläufe, Schlußfolgerungen beschreiben und analysieren zu können. Neben diesen meist in der Mathematik verwendeten logischen
Kalkülen entstanden mit der Entwicklung von Computern seit 1940 Programmiersprachen.
11-2
Schriftsprachen (im Gegensatz zu Bildsprachen, z.B. Chinesisch), zu denen auch die
Programmiersprachen gehören, sind über einem Alphabet, d.h. einer (endlichen) Menge
von unterscheidbaren Zeichen (Buchstaben) aufgebaut. Die Buchstaben sind geordnet,
d.h., es liegt fest, welches der erste, der zweite, der dritte usw. Buchstabe des Alphabets
ist.
Durch Aneinanderreihung endlich vieler Buchstaben eines Alphabets kann man Wörter
(Zeichenfolgen) bilden. Eine Sprache über einem Alphabet ist dann eine Teilmenge der
Menge aller Wörter, die man mit den Buchstaben des Alphabets bilden kann.
Diese sehr unpräzise Erläuterung der Begriffe wollen wir nun formalisieren.
Definition A:
Eine (zweistellige) Relation R auf einer Menge M ist eine Teilmenge R⊆M×M. Sind
a,b∈M und (a,b)∈R, so sagt man "a und b stehen in der Relation R". Gelegentlich
schreibt man statt (a,b)∈R auch aRb. Eine Relation heißt
- reflexiv, wenn für alle a∈M stets (a,a)∈R gilt;
- transitiv, wenn für alle a,b,c∈M aus (a,b)∈R und (b,c)∈R stets (a,c)∈R folgt;
- symmetrisch, wenn für alle a,b∈M aus (a,b)∈R auch (b,a)∈R folgt;
- antisymmetrisch, wenn für alle a,b∈M aus (a,b)∈R und (b,a)∈R stets a=b folgt.
Beispiele:
1) Man betrachte eine Verwandschaftsrelation. M sei die Menge aller Menschen.
R⊆M×M sei die Menge aller Paare von Menschen, die miteinander verwandt sind,
d.h. (a,b)∈R, wenn die Personen a und b miteinander verwandt sind.
R ist nicht reflexiv, denn eine Person ist nicht mit sich selbst verwandt (Man kann das
aber auch anders sehen). R ist symmetrisch, denn wenn von zwei Menschen a,b die
Person a mit b verwandt ist ((a,b)∈R), dann ist auch b mit a verwandt ((b,a)∈R). R ist
nicht antisymmetrisch.
Ist R transitiv? Wenn von drei Personen a,b,c die Person a mit b und b mit c verwandt
ist ((a,b)∈R und (b,c)∈R), dann ist meist auch a mit c verwandt ((a,c)∈R). Leider gilt
dies aber nicht immer; denn wenn z.B. Ehepartner jeweils ein Kind a und c, die nicht
verwandt sind ((a,c)∉R), in eine Ehe einbringen und dann ein gemeinsames Kind b
haben, dann ist b sowohl mit a als auch mit c verwandt (Halbgeschwister), also folgt
aus (a,b)∈R und (b,c)∈R im allgemeinen nicht (a,c)∈R.
2) Man betrachte die Kleiner-Gleich-Relation. M sei gleich der Menge der natürlichen
Zahlen IN. R⊆IN×IN sei die Menge aller Paare natürlicher Zahlen (a,b), für die a≤b
gilt. R ist reflexiv, transitiv und antisymmetrisch.
11-3
Definition B:
a) Eine reflexive, transitive und symmetrische Relation R⊆M×M auf einer Menge M heißt
Äquivalenzrelation.
b) Eine reflexive, transitive und antisymmetrische Relation heißt (partielle) Ordnung
oder Halbordnung.
c) Ist die Ordnung R total definiert, d.h., gilt für alle a,b∈M die Beziehung (a,b)∈R oder
(b,a)∈R, so ist R eine lineare oder totale Ordnung.
Bezeichnung: Ist R eine Ordnung, so schreibt man anstelle von (a,b)∈R meist a≤b. Ist R
antisymmetrisch und transitiv, so schreibt man meist a<b.
Anschaulich gesprochen sind in einer linearen Ordnung alle, in einer partiellen Ordnung
manche Elemente paarweise miteinander vergleichbar.
Beispiele:
1) Man betrachte die Relation "ist gleich oder ist Chef von" innerhalb eines Betriebes. M
ist also die Menge der Mitarbeiter. R⊆M×M ist die Menge aller Paare von Mitarbeitern
(a,b), wobei a=b oder a Chef von b ist, also a≤b gilt. R ist reflexiv, transitiv und
antisymmetrisch, folglich eine (partielle) Ordnung. R ist aber keine lineare Ordnung,
denn für zwei Mitarbeiter a und b aus unterschiedlichen Abteilungen gilt z.B. weder
a≤b, also a ist Chef von b, noch b≤a, also b ist Chef von a.
2) Auf der Menge IN der natürlichen Zahlen ist die Kleiner-Gleich-Relation R (s.o.
Beispiel 2) eine Ordnung. R ist außerdem eine lineare Ordnung, denn für zwei
beliebige Elemente a,b∈IN gilt a≤b oder b≤a.
Nach diesen Vorbereitungen können wir die Begriffe "Alphabet" und "Wort" definieren,
bzw. wiederholen.
Definition C:
Eine nichtleere endliche Menge A={a1,a2,...,an}, n≥1, mit einer linearen Ordnung a1<a2,
a2<a3,..., an-1<an heißt Alphabet.
Zur Erinnerung: Sei A={a1,a2,...,an} ein Alphabet. Ein Wort w der Länge k über A ist eine
Folge von k Elementen von A, d.h.
w=b1b2...bk mit bj∈A für j=1,...,n und n∈IN.
11-4
Die Länge von w bezeichnet man durch |w|=k. Das Wort der Länge 0 heißt leeres
Wort und wird mit ε bezeichnet. Seien v=b1b2...bi und w=c1c2...cj zwei Wörter über A. vw
ist das Wort, das sich durch Konkatenation (Aneinanderfügen) aus v und w ergibt, d.h.
vw=b1b2...bic1c2...cj.
Diese Operation ist assoziativ und besitzt mit dem leeren Wort ein Einselement. Die
Menge aller Wörter über A, das sogenannte freie Monoid über A, wird mit A* bezeichnet
und ist definiert durch:
A*={b1b2...bk | bi∈A für i=1,...,k und k∈IN}.
Beispiele:
1) Sei A={a,b} mit a<b ein Alphabet. w=aabbab ist ein Wort über A mit |w|=6. Sei
v=babbb. Dann ist die Konkatenation von v und w das Wort vw=babbbaabbab. A* ist
die Menge der Wörter
{ε,a,b,aa,ab,ba,bb,aaa,aab,aba,abb,baa,... }
2) Sei A={ | }. Dann ist
A*={ε, |, ||, |||, ||||, |||||,...} ("Bierdeckelnotation").
A* ist isomorph zur Menge IN, wenn man ε der Null und n Striche der Zahl n∈IN
zuordnet. Die Konkatenation zweier Wörter v,w∈A* entspricht dann der Addition,
denn z.B. gilt für v=||| (entspricht der Zahl 3) und w=||||| (entspricht der Zahl 5)
vw=|||||||| (entspricht der Zahl 8).
Definition D:
Sei A ein Alphabet. Jede Teilmenge L⊆A* heißt Sprache über A.
Beispiele:
1) Sei A={0,1,2,...,9} das Alphabet der üblichen Dezimalziffern. Die Menge
L={a1a2...an | ai∈A für i=1,...,n; n∈IN, n≥1; a1≠0 falls n≥2}
ist eine Sprache über A, die Sprache der Dezimaldarstellungen für die natürlichen
Zahlen (ohne führende Nullen).
2) Sei A={ | } ein Alphabet. Die Menge
L={a1a2...an | ai∈A für i=1,...,n; n Primzahl}
ist eine Sprache über A. L ist die Menge aller Strichfolgen, deren Länge eine
Primzahl ist.
Wir haben nun die für die folgenden Untersuchungen zentralen Begriffe "Alphabet",
"Wort" und "Sprache" kennengelernt. Allerdings war besonders die Definition von Sprache noch recht abstrakt und unhandlich. Im nächsten Abschnitt widmen wir uns daher
11-5
der Frage, wie man Sprachen möglichst anschaulich erfassen und beschreiben kann.
Zuvor wollen wir jedoch zur Erleichterung unserer Notation noch einige neue Bezeichnungen einführen.
Bezeichnungen:
1) Die Menge aller Wörter der Länge n über einem Alphabet A bezeichnet man mit An:
An={w | w∈A* und |w|=n}.
Speziell gilt für n=0:
A0={ε}.
2) Entfernt man aus A* das leere Wort, so erhält man die Menge
A+=A*\{ε}.
3) Sei w∈A* und n∈IN. wn bezeichnet die n-fache Aneinanderreihung von w, d.h.
wn=www...ww.
Speziell gilt für n=0 wiederum: w0=ε.
11.1.2
Syntaxdiagramme
Unter der Syntax einer Sprache verstehen wir die Regeln, denen die Wörter, die zur
Sprache gehören, gehorchen müssen. Wir beschäftigen uns in diesem Abschnitt mit
einer anschaulichen und sehr einfachen Methode, um die Syntax von Sprachen zu
beschreiben, den sog. Syntaxdiagrammen.
Definition E:
Seien N und T Alphabete. N ist die Menge der Nichtterminalsymbole (auch Hilfszeichen
oder Variablen genannt) und T die Menge der Terminalsymbole. Syntaxdiagramme
über N und T sind folgendermaßen aufgebaut:
1) Jedes Syntaxdiagramm ist mit einem Nichtterminalsymbol aus N markiert.
2) Ein Syntaxdiagramm besteht aus Kreisen (oder Ellipsen) und Kästchen, die durch
Pfeile miteinander verbunden sind.
3) In jedem Kreis (oder jeder Ellipse) steht ein Wort über T. In jedem Kästchen steht ein
Nichtterminalsymbol, d.h., ein Element aus N.
4) Aus jedem Kreis (jeder Ellipse) und jedem Kästchen führt genau ein Pfeil hinaus und
genau ein Pfeil hinein.
5) Ein Pfeil darf sich in mehrere Pfeile aufspalten (Verzweigungspunkt) und mehrere
Pfeile dürfen zu einem Pfeil zusammengeführt werden (Zusammenfassungspunkt).
6) In jedem Syntaxdiagramm gibt es genau einen ("Eingangs"-) Pfeil, der von keinem
Kreis (Ellipse) oder Kästchen ausgeht (von außen in das Syntaxdiagramm einlaufender Pfeil), und genau einen ("Ausgangs"-) Pfeil, der zu keinem Kreis (Ellipse) oder
11-6
Kästchen führt (nach außen aus dem Syntaxdiagramm auslaufender Pfeil). Alle
übrigen Pfeile verbinden Kästchen, Kreise (Ellipsen), Verzweigungs- und Zusammenführungspunkte.
7) Von dem Eingangspfeil aus kann man jeden Kreis (Ellipse) und jedes Kästchen des
Syntaxdiagramms auf mindestens einem Weg erreichen, und von jedem Kreis
(Ellipse) oder Kästchen kann man auf mindestens einem Weg zum Ausgangspfeil
gelangen. (Diese Bedingung besagt, daß das Syntaxdiagramm nicht in mehrere
Teile zerfallen darf.)
Soweit die Definition von Syntaxdiagrammen.
Beispiel: Sei T={0,1,2,...,9,+,-} das Alphabet der Terminalsymbole, N={Ziffer, Ziffernfolge,
ZifferohneNull, Ganzzahl} die Menge der Nichtterminalsymbole. Syntaxdiagramme über N
und T zeigen Abb. 1.
11-7
ZifferohneNull
1
2
3
4
0
ZifferohneNull
5
6
7
8
9
Ziffer
Ziffernfolge
Ziffer
Ganzzahl
+
0
-
ZifferohneNull
Ziffernfolge
Abb.1: Beispiele für Syntaxdiagramme
Syntaxdiagramme über N und T definieren eindeutig eine Sprache L⊆T*. Wie bestimmt
man aber nun diese Sprache?
Definition F: Auswertung eines Syntaxdiagramms
11-8
Man betritt das Syntaxdiagramm durch den Eingangspfeil und befolgt solange jeweils
eine der folgenden Regeln, bis man das Syntaxdiagramm auf dem Ausgangspfeil verlassen hat:
1) Trifft man auf einen Verzweigungspunkt, so folgt man nach Belieben einem der
weiterführenden Pfeile.
2) Trifft man auf einen Zusammenfassungspunkt, so folgt man dem weiterführenden
Pfeil.
3) Trifft man auf einen Kreis (eine Ellipse), so schreibt man seinen Inhalt, also ein Wort
über T, auf (bzw. hängt es hinten an das schon aufgeschriebene Wort an) und verläßt
den Kreis (die Ellipse) über den ausgehenden Pfeil.
4) Trifft man auf ein Kästchen, in dem ein Nichtterminalsymbol ω steht, so kopiert man
das Syntaxdiagramm mit der Bezeichung ω anstelle des Kästchen ein. Der in das
Kästchen hineinlaufende Pfeil wird mit dem Eingangspfeil des Syntaxdiagramms für
ω und der von dem Kästchen abgehende Pfeil mit dem Ausgangspfeil des Syntaxdiagramms für ω verbunden.
Die Regel 4 gilt für jedes mögliche Nichtterminalsymbol ω. Insbesondere kann ein Syntaxdiagramm auch in sich selbst eingesetzt werden. Dadurch ist ein rekursives Einsetzen möglich!
Nun wird auch klar, wie die Namen "Terminalsymbol" und "Nichtterminalsymbol" zustande kommen: Während Terminalsymbole einen Endzustand (lat.: terminare = beenden)
darstellen, symbolisieren Nichtterminalsymbole einen Zwischenzustand und können
weiter ersetzt werden.
Definition G:
Jedes Wort, das nach irgendeiner Auswertung eines Syntaxdiagramms aufgeschrieben
ist, kann durch das Syntaxdiagramm erzeugt werden. Die durch das Syntaxdiagramm
definierte Sprache ist die Menge aller Wörter, die auf diese Weise erzeugt werden
kann.
Beispiele:
1) Wir betrachten noch einmal die Syntaxdiagramme aus Abb. 1.
Zunächst betrachten wir das Syntaxdiagramm ZifferohneNull. Wir betreten es links
durch den Eingangspfeil, gehen bei den Verzweigungspunkten z.B. zweimal geradeaus und dann nach unten. Dort treffen wir auf den Kreis mit der Inschrift 3. Wir
schreiben 3 auf und verlassen auf den unten stehenden Pfeilen das Syntaxdiagramm. Die Zeichenfolge "3" ist also ein Wort der vom Syntaxdiagramm ZifferohneNull
11-9
definierten Sprache. Man sieht unmittelbar, daß die Sprache, die dieses Syntaxdiagramm definiert, genau folgende endliche Menge ist:
{1,2,3,4,5,6,7,8,9}.
Betritt man das Syntaxdiagramm Ziffer, so kann man entweder das Zeichen 0 hinschreiben, oder man wählt das Kästchen ZifferohneNull. Dieses Kästchen ist nun
durch das Syntaxdiagramm ZifferohneNull zu ersetzen. Wie man dieses auswertet,
haben wir bereits diskutiert. Die von Ziffer definierte Sprache lautet daher:
{0,1,2,3,4,5,6,7,8,9}.
Das Syntaxdiagramm Ziffernfolge kann man, ohne auf einen Kreis oder ein Kästchen
zu treffen, wieder verlassen, d.h., das leere Wort gehört zu der hierdurch definierten
Sprache. Man kann sich aber beim Verzweigungspunkt für die untere Richtung entscheiden und eine Ziffer erzeugen; dies kann man beliebig of t wiederholen. Folglich
definiert das Syntaxdiagramm Ziffernfolge genau die Menge aller Wörter über den
Ziffern, also
{0,1,2,3,4,5,6,7,8,9}*.
Man kann sich davon überzeugen, daß die durch Ganzzahl definierte Sprache die
Menge aller Dezimaldarstellungen der ganzen Zahlen ist (ohne führende Nullen, ggf.
mit Vorzeichen).
2) Im Laufe der Vorlesung haben wir regelmäßig den Begriff des Bezeichners verwendet, ohne jemals genau zu definieren, was wir unter einem Bezeichner verstehen. Mit
Hilfe von Syntaxdiagrammen können wir diese Lücke nun schließen (Abb. 2). Ein
Bezeichner ist nach dieser Definition also eine Folge von Buchstaben und/oder Ziffern, die stets mit einem Buchstaben beginnt.
11-10
Ziffer
0
1
2
3
a
A
b
B
4
5
6
7
z
Z
8
9
Buchstabe
...
Bezeichner
Buchstabe
Buchstabe
Ziffer
Abb. 2: Syntaxdiagramme für Bezeichner
Wir haben Syntaxdiagramme eingeführt, um Sprachen präzise definieren können. Dabei haben wir aber noch nicht überprüft, ob Syntaxdiagramme ein genügend mächtiges
Darstellungsmittel in dem Sinne sind, daß man jede beliebige Sprache L⊆T* über einem
Alphabet T mit ihnen beschreiben kann. Wir wollen hier ein Ergebnis vorwegnehmen,
welches in Vorlesungen über Formale Sprachen erarbeitet wird. Man kann beweisen,
daß mit Syntaxdiagrammen aus theoretischer Sicht zwar nur relativ eingeschränkte,
aber gerade die für die Praxis sehr interessanten Sprachen beschrieben werden
können, die sog. kontextfreien Sprachen. Die Syntax sehr vieler bekannter Programmiersprachen (auch die von uns benutzten Programmiersprachen PRO und ASS) fällt im
wesentlichen (Präzisierung: siehe Vorlesungen über Theoretische Informatik) unter die
11-11
kontextfreien Sprachen. Daß es aber auch sehr einfache Sprachen gibt, die nicht durch
Syntaxdiagramme definiert werden können, zeigt folgendes
Beispiel: T sei die Menge { | }. Man kann zeigen, daß die Sprache L⊆T*, die genau alle
Folgen mit quadratisch vielen Strichen enthält, also
L={ |n | n ist Quadratzahl},
nicht durch ein Syntaxdiagramm definiert werden kann.
11.1.3
Backus-Naur-Form (BNF)
Eine andere nicht-graphische Methode zur Darstellung von Sprachen ist die BackusNaur-Form, die von den beiden Wissenschaftlern J. Backus und P. Naur vor über 30
Jahren vorgeschlagen und von einem Komitee zur Definition der Programmiersprache
ALGOL 60 verwendet wurde.
Dieser Ansatz verläuft, vereinfacht gesprochen, so: Man beginnt mit einer Zeichenfolge
und erzeugt durch eine Folge von Veränderungsschritten potentiell jedes mögliche Wort
der Sprache. Die Regeln, nach denen Veränderungen an Zeichenfolgen vorgenommen
werden dürfen, bezeichnet man als Grammatik. Es ist klar, daß wir zur Notation des
Regelwerks – wie schon bei Syntaxdiagrammen – spezielle Symbole benötigen, die mit
den Zeichen der Sprache, die wir definieren wollen, nicht verwechselt werden können.
Fachmännisch gesprochen: Wir benötigen eine Metasprache, d.h. eine Sprache, mit der
wir eine andere Sprache mit Hilfe einer Grammatik definieren können. Eine solche
Metasprache ist die Backus-Naur-Form.
Wie schon bei Syntaxdiagrammen unterscheidet man bei einer Grammatik in BackusNaur-Form zwei Zeichenmengen: ein Alphabet N von Nichtterminalsymbolen und ein
Alphabet T von Zeichen der zu definierenden Sprache, die Terminalsymbole.
Jedes Nichtterminalsymbol ist in spitze Klammern <> eingeschlossen, die nicht als Terminalsymbole vorkommen dürfen. Dies hat den Zweck, Nichtterminalsymbole kenntlich
zu machen, denn sie sind ja meist mit Hilfe des gleichen Alphabets geschrieben wie die
Sprache selbst (nämlich im Alphabet der deutschen Sprache). In der Menge der Nichtterminalsymbole zeichnet man ein Symbol S besonders aus, das Startsymbol.
Das Startsymbol ist gewissermaßen der allgemeinste Begriff, d.h., die Zeichenfolge, bei
der man mit der Durchführung von Veränderungsschritten beginnt. Die Veränderungsregeln selbst bezeichnet man als Regeln oder Produktionen. Sie werden in der BackusNaur-Form folgendermaßen aufgeschrieben:
X ::= w1 | w2 | ... | wn.
11-12
Dabei ist X∈N stets ein einzelnes (in spitze Klammern gefaßtes) Nichtterminalsymbol.
w1,w2,...,wn∈(N∪T)* sind n beliebige Wörter, die aus Nichtterminal- und Terminalsymbolen gebildet sind (auch das leere Wort ε ist möglich). Die obige Produktion bedeutet:
Wenn in einem Wort an irgendeiner Stelle das Nichtterminalsymbol
X vorkommt, so darf es durch eines der Worte w1,w2, ...,wn ersetzt werden.
Damit dieser Ersetzungsschritt problemlos durchgeführt werden kann, dürfen die Symbole "::=" und "|" nicht als Terminalsymbole vorkommen. Sie gehören zur Metasprache.
Das Viertupel G=(N,T,P,S), bestehend aus der Menge der Nichtterminalsymbole N, der
Menge der Terminalsymbole T, der Menge der Produktionen P und dem Startsymbol S
bezeichnet man als Grammatik in Backus-Naur-Form (BNF-Grammatik).
Beispiel: Die folgende BNF-Grammatik entspricht den Syntaxdiagrammen in Abb. 1. Es
sei
T={0,1,2,...,9,+,-},
N={<Ziffer>,<ZifferohneNull>,<Ziffernfolge>,<Ganzzahl>}.
Das Startsymbol S sei das Symbol <Ganzzahl>. Die Produktionen seien definiert durch:
<ZifferohneNull>::= 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9
<Ziffer>::= 0 | <ZifferohneNull>
<Ziffernfolge>::= ε | <Ziffer><Ziffernfolge>
<Ganzzahl>::= 0 | +0 | -0 | <ZifferohneNull><Ziffernfolge> |
+ <ZifferohneNull><Ziffernfolge> |
- <ZifferohneNull><Ziffernfolge>
Definition H:
Eine Grammatik in Backus-Naur-Form, kurz BNF-Grammatik, ist ein Viertupel
G=(N,T,P,S) mit
a) T ist die Menge der Terminalsymbole, N die Menge der Nichterminalsymbole
und T∩N=∅;
b) S∈N ist das Startsymbol;
c) P ist die Menge der Produktionen. Eine Produktion p∈P ist eine Zeichenfolge der
Form
X ::= w1 | w2 | ... | wn.
wobei X∈N, n≥1 und wi∈(N∪T)* für i=1,2,...,n sind.
Wie kann man nun mit Hilfe einer BNF-Grammatik aus dem Startsymbol Wörter erzeugen, und was ist die von der Grammatik definierte Sprache? Wie bereits kurz angeschnitten, beginnt man mit dem Startsymbol S. Wann immer ein Wort über T∪N, also
11-13
bestehend aus Terminal- und Nichtterminalsymbolen, vorliegt, in dem das Nichtterminalsymbol X vorkommt, darf man X durch eines der Wörter w1,w2,...,wn ersetzen, sofern
X::=w1 | w2 | ... | wn eine Produktion der Grammatik ist. Die Ersetzungsschritte brechen
ab, wenn man ein Wort über T erhalten hat, welches also nur noch aus Terminalsymbolen besteht.
Definition I:
Sei G=(N,T,P,S) eine BNF-Grammatik.
a) Ein Wort v∈(N∪T)* heißt in einem Schritt aus einem Wort u∈(N∪T)* ableitbar,
wenn es Wörter u1,u2∈(N∪T)*, ein X∈N und eine Produktion der Form
X::=w1|w2|...|wn in P gibt, so daß für ein i∈{1,...,n} gilt:
u=u1Xu2 und v=u1wu2.
Man schreibt dann kurz u→v.
b) Ein Wort v∈(N∪T)* heißt ableitbar aus einem Wort u∈(N∪T)*, wenn u=v ist oder
wenn es Wörter u=u0,u1,u2,...,ur=v∈(N∪T)* gibt, so daß
ui-1→ui für i∈{1,..., r}
gilt. Man schreibt dann kurz u→*v.
u0→u1→u2→...→ ur heißt Ableitung der Länge r von u0 nach ur.
c) Die von einer BNF-Grammatik G erzeugte Sprache ist definiert durch
L(G)={w∈T* | S→*w}.
L(G) ist also die Menge aller aus dem Startsymbol ableitbaren Wörter, die nur aus
Terminalsymbolen bestehen.
Beispiele:
1) Man betrachte obiges Beispiel. Frage: Ist das Wort 732 aus dem Startsymbol ableitbar? Ja, denn man kann folgende Ableitung bilden:
<Ganzzahl> → <ZifferohneNull> <Ziffernfolge> →
<ZifferohneNull> <Ziffer> <Ziffernfolge> →
7 <Ziffer><Ziffernfolge> →
7 <Ziffer> <Ziffer> <Ziffernfolge> →
7 <ZifferohneNull> <Ziffer> <Ziffernfolge> →
7 <ZifferohneNull> <Ziffer> → 73 <Ziffer> →
73 <ZifferohneNull> → 732.
Die von der BNF-Grammatik G erzeugte Sprache L(G) ist wie beim zugehörigen
Syntaxdiagramm die Menge der Dezimaldarstellungen der ganzen Zahlen ohne
führende Nullen und ggf. mit Vorzeichen.
2) Sei G=(N,T,P,<start>) eine BNF-Grammatik mit N={<start>}, T={a,b},
11-14
P={ <start>::= ε | a<start> | b<start>}.
Mit G kann man jede beliebige Folge von a und b erzeugen, d.h. L(G)=T*.
3) Sei G=(N,T,P,S) eine BNF-Grammatik mit N={<start>}, T={(,)},
P={<start>::=(<start>)<start> | ε}.
Mit G kann man alle korrekten Klammerungen erzeugen, d.h., alle Klammerfolgen,
bei denen an jeder Stelle der Folge mehr Klammern geöffnet als geschlossen wurden und zusätzlich am Ende der Folge die Zahl der geöffneten Klammern gleich der
Zahl der geschlossenen Klammern ist. Zum Beispiel ist ((()())()) eine korrekte,
((())()))())) eine unkorrekte Klammerung.
Eine Ableitung soll die Wirkungsweise verdeutlichen:
<start> → (<start>) <start> → ((<start>)<start>)<start> →
((<start>)<start>)(<start>)<start> → (() <start>)(<start>)<start> →
(())(<start>)<start> → (())()<start> → (())()
Dabei ist in jedem zwischenzeitlich abgeleiteten Wort das Nichtterminalsymbol
unterstrichen, das im nächsten Schritt ersetzt wird.
4) Die folgende Grammatik G=(N,T,P,<Bezeichner>) mit
N={<Bezeichner>,<Buchstabe>,<Zeichenkette>,<Ziffer>},
T={a,b,...,z,0,1,...,9} und
P={<Bezeichner>::= <Buchstabe> | <Buchstabe><Zeichenkette>,
<Zeichenkette>::= <Buchstabe> | <Ziffer> |
<Buchstabe><Zeichenkette> |
<Ziffer><Zeichenkette>,
<Ziffer>::= 0 | 1 | ... | 9,
<Buchstabe>::= a | b | ... | z}
definiert die Sprache der Wörter über T, die als Bezeichner verwendet werden
dürfen.
BNF-Grammatiken dienen in der Informatik wie Syntaxdiagramme meist dazu, die Syntax von Programmiersprachen zu definieren.
Für bestimmte Produktionen hat man in der Backus-Naur-Form Abkürzungen eingeführt.
1. Abkürzung: Symbole oder Symbolfolgen innerhalb einer Produktion, die auch weggelassen werden können, werden in eckige Klammern eingeschlossen.
Beispiel: Eine Produktion der Form
A ::= B C | B
wobei B und C beliebige Symbolfolgen sind, kann man abkürzen durch
A ::= B [C].
11-15
Die Produktion für <Bezeichner> in obigem Beispiel 4 schreibt man dann entsprechend
als
<Bezeichner> ::= <Buchstabe> [<Zeichenkette>]
2. Abkürzung: Symbole oder Symbolfolgen, die beliebig oft wiederholt oder aber auch
weggelassen werden können, werden in geschweifte Klammern eingeschlossen.
Beispiel: Anstelle der Produktionen für <Bezeichner> und <Zeichenkette> in Beispiel 4 kann
man nun kurz schreiben:
<Bezeichner>::= <Buchstabe> {<Buchstabe> | <Ziffer>}
Welche Mächtigkeit besitzen nun BNF-Grammatiken im Vergleich zu Syntaxdiagrammen? Man kann zeigen, daß man zu jeder BNF-Grammatik ein gleichwertiges Syntaxdiagramm konstruieren kann und umgekehrt. Den Beweis werden wir hier nicht führen;
er ist relativ einfach, und man kann ihn sich an Beispielen leicht klar machen. Man kann
also mit BNF-Grammatiken die gleichen Sprachen definieren wie mit Syntaxdiagrammen. Diese Sprachen nennt man die kontextfreien Sprachen.
Anhand der Backus-Naur-Form von Grammatiken läßt sich gut nachvollziehen, was der
Begriff "kontextfrei" bedeutet. Er bedeutet, daß innerhalb eines Ableitungsprozesses
jedes Nichtterminalzeichen <X> ersetzt werden darf, unabhängig davon, in welchem
Kontext es auftritt, d.h., welche Zeichen rechts oder links neben <X> stehen.
Da alle bekannten Programmiersprachen im wesentlichen zu den kontextfreien Sprachen gehören, haben Syntaxdiagramme wie auch BNF-Grammatiken eine große Bedeutung. Doch Vorsicht! Wir haben gesagt "im wesentlichen" und meinen damit, daß
gewisse, aber wenige syntaktische Regeln einer Programmiersprache nicht mit Hilfe
eines Syntaxdiagramms oder einer BNF-Grammatik definiert werden können. Zwei dieser Regeln sind z.B.
- Jeder Bezeichner, der in einem Programm verwendet wird, muß vorher deklariert
werden.
- Die Anzahl der in der Deklaration einer Prozedur auftretenden formalen Parameter ist
gleich der Zahl der bei der Anwendung der Prozedur aufgeführten aktuellen Parameter.
Man kann zeigen, daß keine dieser beiden Regeln durch ein Syntaxdiagramm oder eine
BNF-Grammatik erzwungen werden kann. Trotz dieses Mangels definiert man Programmiersprachen stets durch Syntaxdiagramme oder BNF-Grammatiken. Regeln der obigen
Form, die man hiermit nicht beschreiben kann, fügt man umgangssprachlich hinzu.
11-16
11.2 Semantik
Wie zu Beginn dieses Kapitels erwähnt ist eine Programmiersprache P ein Paar P=(L,F)
mit der formalen Sprache L⊆A*, die die Syntax von P bestimmt, und der Abbildung F, die
jedem Programm w∈L seine Semantik zuordnet. Mit der Syntax haben wir uns soeben
beschäftigt. Wie kann man nun die Semantik eines Programms präzisieren?
Schon früher (Abschnitt 4.5.1) hatten wir einem Programm mit der Methode der denotationalen Semantik die von w berechnete Funktion
fw: I→O
von der Menge I der Eingabedaten in die Menge O der Ausgabedaten als Bedeutung
zuordnet. F ist nach diesem Ansatz also eine Funktion
F: A*→(I→O) mit
fw, falls w∈L
F[w]=
⊥, sonst.
F nennt man auch semantische Funktion. F liefert also zu jedem syntaktisch korrekten Programm als seine Bedeutung die zugehörige berechnete Funktion. Programme,
die syntaktisch fehlerhaft sind, haben keine Bedeutung, und die semantische Funktion
ist hierfür undefiniert.
Beispiel: Gegeben sei folgende Zeichenfolge w mit
w ≡ funktion f x:int → int ≡
wenn x=1 dann 2 sonst 2*(f(x-1)).
Offenbar ist w ein syntaktisch korrektes Programm. Wie lautet die Semantik von w? Es
gilt:
F[w]=exp2 mit
2x, falls x≥1
exp2(x)=
⊥, sonst.
w berechnet also die Zweierpotenz, wenn man natürliche Zahlen ≥1 eingibt. In den übrigen Fällen terminiert w nicht, und die berechnete Funktion ist undefiniert.
Die Aufgabe besteht nun darin, F für alle Programme w einer Programmiersprache
P=(L,F) zu definieren. Natürlich kann man dies nicht für die unendlich vielen w explizit
durchführen, vielmehr muß man den speziellen syntaktischen Aufbau von w ausnutzen
und F induktiv über diesen Aufbau definieren. Man präzisiert also die Semantik aller
Sprachelemente isoliert voneinander, zunächst der elementaren Sprachelemente, dann
der einzelnen Konstruktoren usw. Schließlich liegt für jedes Sprachelement das partielle
Verhalten der semantischen Funktion vor, und man kann für ein konkretes Programm
11-17
alle diese Teilfunktionen entsprechend des Programmaufbaus zusammensetzen und
erhält so die Semantik des Gesamtprogramms.
Um die prinzipielle Vorgehensweise bei der Definition einer semantischen Funktion F zu
verstehen, beschränken wir uns jedoch der Übersichtlichkeit halber im weiteren Verlauf
auf die Semantikdefinition einer Teilmenge von ML, genannt µML.
11.2.1 Die Syntax von µML
In µML mögen nur folgende Typen und Konstrukte von µML zugelassen sein:
der elementare Datentyp int,
Ausdrücke über int und bool,
Wertdeklarationen der Form val x=E mit einem arithmetischen
Ausdruck E vom Typ int,
Funktionsdefinitionen der Form fun f x =... mit einem Parameter,
eine spezielle Eingabeanweisung read.
Die Syntax von µML lautet in Backus-Naur-Form:
<prog> ::= <decls> ; <call>
<decls> ::= <decl> ; <decls> | ε
<decl> ::= <valuedecl> | <fundecl>
<valuedecl> ::= val <id> = <arithexp>
<fundecl> ::= fun <id> <id> = <exp>
<exp> ::= <arithexp> | <condexp>
<arithexp> ::= (<arithexp> <op> <arithexp>) | <const> |
<id> | <id> (<arithexp>)
<op> ::= + | - | * | /
<condexp> ::= if <boolexp> then <exp> else <exp>
<boolexp> ::= <arithexp>=<arithexp> | <arithexp>≠<arithexp> | true | false
<id> ::= "Bezeichner nach üblichen Konventionen"
<const> ::= "ganzzahlige Konstante"
<call> ::= (read(<id>); <arithexp>).
11.2.2
Umgebungen
Die Semantik etwa eines arithmetischen Ausdrucks ist sein Wert. Um also die Semantik
eines Ausdrucks z.B. der Form
2+x
11-18
zu ermitteln, müssen die Werte aller Objekte des Ausdrucks entsprechend der beteiligten Operationen verknüpft werden. Die Werte von Konstanten sind unmittelbar abzulesen, wie ermittelt man jedoch die Werte von Bezeichnern? Offenbar muß man eine Art
Symboltabelle mitführen, aus der man die aktuelle Bindung jedes Bezeichners an jeder
Programmstelle ablesen kann. Da sich Bindungen im Laufe eines Programms, etwa
durch Neudefinition, ändern können, benötigt man ferner Operationen, um den Inhalt der
Symboltabelle ebenfalls ändern zu können.
Diese Überlegungen führen auf den Begriff der sog. Umgebung.
Umgebungen u sind Abbildungen von der Menge der möglichen Bezeichner in die
Menge der möglichen Werte; in µML sind das einerseits die elementaren Werte und
andererseits Funktionsrümpfe (=Ausdrücke). Wir definieren also:
u: {Bezeichner} → int∪({Bezeichner}×[int→int]×{Umgebungen}).
Zu jedem Bezeichner x liefert u den aktuellen Wert u(x), sofern x gebunden ist,
anderenfalls undefiniert ⊥. Ist x an einen elementaren Wert gebunden, so ist u(x) dieser
Wert, ist x an eine Funktion gebunden, so ist u(x) ein Tripel
(p,r,u').
Hierbei ist p der Bezeichner des formalen Parameters von x, r der Funktionsrumpf von x
(also ein Ausdruck, der eine Funktion in [int→int] beschreibt) und u' die Umgebung, in
der x deklariert wurde. u' wird benötigt, um das Konzept der statischen Bindung von ML
formal korrekt zu formulieren: Wird nämlich x aufgerufen, so gelten für die Objekte im
Rumpf von x die Bindungen u' zum Zeitpunkt der Deklaration von x und nicht die Bindungen u zum Zeitpunkt des Aufrufs von x. Die Umgebung u' muß daher in dem Augenblick
wiederhergestellt werden, wenn x aufgerufen wird.
Für später legen wir folgende Bezeichnungen fest: Zu einem Namen x, der in der
aktuellen Umgebung u eine Funktion bezeichnet, also u(x)=(p,r,u'), liefere π den formalen Parameter p, also π(u(x))=p, ρ den Rumpf der Funktion, also ρ(u(x))=r, und υ die
Umgebung, in der x deklariert wurde, also υ(u(x))=u'.
Beispiel: Definiert man in der Umgebung u eine Funktion
fun f p = if B then E1 else E2,
so erhält man danach eine neue Umgebung u', in der für f gilt:
u'(f)=(p, if B then E1 else E2, u).
Umgebungen stellen wir meist als Menge von Gleichungen
u={x1=w1,...,xn=wn}
dar. Hierbei bedeutet xi=wi, daß der Bezeichner xi in der Umgebung u den Wert wi
besitzt. Für y≠xi, i=1,...,n gilt u(y)=⊥.
11-19
Im Laufe eines Programms können Bezeichner an unterschiedliche Werte gebunden
werden. Um diese Veränderungen von Umgebungen übersichtlich formal zu beschreiben, definieren wir eine Operation + auf Umgebungen wie folgt: Sei
u={x1=w1,...,xn=wn} und
u'={y1=v1,...,ym=vm}.
Dann gilt:
u"=u+u' mit
u(x), falls u'(x)=⊥,
u"(x)=
u'(x), sonst.
In u" werden also die Bindungen der Bezeichner in u durch die Bindungen in u' überschrieben. Man beachte, daß + nicht kommutativ ist.
Beispiel: Sei u={x=2, y=3}, u'={x=(a,if a=0 then 0 else 1,{y=4,z=0}), z=5}. Dann ist
u"=u+u'={x=(a,if a=0 then 0 else 1,{y=4,z=0}), y=3, z=5}.
Nun wollen wir die Semantik von µML definieren. Dabei gehen wir wie erwähnt induktiv
vor, indem wir für die einzelnen Sprachelemente von µML semantische Funktionen
definieren, die im Zusammenspiel geeignet sind, die Semantik von Programmen zu
beschreiben.
Programme.
Wir beginnen bei Programmen und brechen die zugehörige semantische Funktion
schrittweise auf semantische Funktionen für die Bestandteile der Programme herunter.
Die Bedeutung von Programmen beschreibt die semantische Funktion:
B: µML→[int*→int].
B bildet also wie erwartet Programmtexte in µML auf Funktionen ab, die einen Eingabeund einen Ausgabewert vom Typ int besitzen. B ist definiert durch
B[<decls>;(read(<id>);<arithexp>)](α)=
E[<arithexp>]((D[<decls>]∅)+{<id>=α}).
Erläuterung: Die Bedeutung eines Programms der angegebenen Form
<decls>;(read(<id>);<arithexp>)
mit dem Eingabewert α erhält man wie folgt: Man stellt zunächst mittels D die Umgebung
her, die die Deklarationen <decls> auf der leeren Umgebung ∅ (noch kein Bezeichner
gebunden) bewirken. Zu dieser Umgebung fügt man die Bindung des aktuellen Eingabewertes α an den Bezeichner <id> hinzu. In dieser neuen Umgebung wertet man anschließend mittels der semantischen Funktion E den Ausdruck <arithexp> aus und erhält
das Ergebnis. D und E beschreiben wir im folgenden.
11-20
Deklarationen.
Die Bedeutung von Deklarationen beschreibt die semantische Funktion
D: {Deklarationen}→[{Umgebungen}→{Umgebungen}].
Eine Folge von Deklarationen bildet also unter D Umgebungen auf neue Umgebungen
ab. D ist definiert durch:
D[ ]u=u,
D[<decl>;<decls>]=D[<decls>]°D[<decl>].
Erläuterung: Bilde zu einer Umgebung zunächst die neue Umgebung, die durch die
Deklaration <decl> hergestellt wird, und wende darauf die Umgebungstransformation
von <decls> an. Auf diese Weise werden die einzelnen Deklarationen schrittweise
abgearbeitet und die jeweiligen Umgebungen aufgebaut.
Wertdeklarationen:
D[val <id>=<arithexp>]u=u+{<id>=E[<arithexp>]u}.
Erläuterung: Eine Wertdeklaration verändert eine vorliegende Umgebung u durch
Hinzufügen der Bindung von <id> an den mittels der semantischen Funktion E (s.u.) in
der Umgebung u bestimmten Wert des Ausdrucks <arithexp>.
Funktionsdeklarationen:
D[fun <id1> <id2> = <exp>]u=u+{<id1>=(<id2>,<exp>,u)},
Erläuterung: Eine Funktionsdeklaration verändert eine Umgebung u durch Hinzufügen
der Bindung von <id1> an das Tripel bestehend aus dem formalem Parameter <id2>,
dem Funktionsrumpf <exp> und der Umgebung, in der <id1> deklariert wurde. Dies ist u
selbst.
Ausdrücke.
Die Bedeutung von Ausdrücken wird durch die semantische Funktion
E: {Ausdrücke}→[{Umgebungen}→int∪bool]
beschrieben: Sie wertet einen Ausdruck in einer Umgebung aus und liefert einen Wert
aus int oder bool. Man definiert sie induktiv.
Für arithmetische Ausdrücke:
E[(<arithexp1> + <arithexp2>)]u=(E[<arithexp1>]u + E[<arithexp2>]u),
↑
↑
Beachte: Hier handelt es sich um ein Funktionszeichen, hier um die zugehörige Funktion.
Analog lautet die Definition für die Operationen -,*,/.
Für boolesche Ausdrücke:
E[<arithexp1> = <arithexp2>]u=E[<arithexp1>]u = E[<arithexp2>]u,
E[<arithexp1> ≠ <arithexp2>]u=E[<arithexp1>]u ≠ E[<arithexp2>]u,
11-21
E[true]u=wahr,
E[false]u=falsch.
Für bedingte Ausdrücke:
E[if <boolexp> then <exp1> else <exp2>]u=
E[<exp1>]u, falls E[<boolexp>]u=wahr,
E[<exp2>]u, falls E[<boolexp>]u=falsch.
Für Konstanten:
E[<const>]u=<const>.
Für Bezeichner:
E[<id>]u=u(<id>).
Für Funktionsanwendungen:
E[<id>(<arithexp>)]u=
E[ρ(u(<id>))](D[fun <id> (π(u(<id>)))=ρ(u(<id>));
val π(u(<id>))=E[<arithexp>]u]υ(u(<id>))),
Erläuterung: Die Semantik eines Funktionsaufrufs erhält man wie folgt: Man wertet den
Funktionsrumpf ρ(u(<id>)) aus. Das muß in der Umgebung υ(u(<id>)) geschehen, in der
die Funktion <id> deklariert wurde. Diese Umgebung reichert man mittels D einerseits
an um die Funktionsdeklaration fun <id> ... selbst, denn <id> muß ja innerhalb von <id>
bekannt sein (um Rekrusionen zu ermöglichen). Andererseits ergänzt man die Bindung
des aktuellen Parameters <arithexp>, der in der Umgebung u, also vor Aufruf, mittels
E[<arithexp>]u ausgewertet wird, an den formalen Parameter π(u(<id>)), d.h. man
deklariert den formalen Parameter in der Umgebung υ(u(<id>)) neu.
Für boolesche Ausdrücke:
E[<arithexp1> = <arithexp2>]u=E[<arithexp1>]u = E[<arithexp2>]u,
E[<arithexp1> ≠ <arithexp2>]u=E[<arithexp1>]u ≠ E[<arithexp2>]u,
E[true]u=wahr,
E[false]u=falsch.
Die Funktionen D[a], E[a] usw. heißen auch Denotationen von a. Die Funktionensemantik
heißt danach auch denotationale Semantik.
Beispiele:
1) Wir bestimmen die Semantik eines einfachen Programms P. P sei
val x=1;
val y=3;
(read(z); (y*(z+x))).
Für einen beliebigen Eingabewert α:int gilt:
11-22
B[P](α)=E[(y*(z+x))]((D[val x=1; val y=3]∅)+{z=α}).
Hierbei ist
D[val x=1; val y=3]=D[val y=3] ° D[val x=1].
Im einzelnen:
D[val x=1]∅=∅+{x=E[1]∅}={x=1},
D[val y=3]{x=1}={x=1}+{y=3}={x=1,y=3}.
Folglich gilt:
B[P](α)=E[(y*(z+x))] {x=1,y=3,z=α}.
Setzen wir u={x=1,y=3,z=α}, so folgt:
E[(y*(z+x))]u=(E[y]u*E[(z+x)]u)=(E[y]u*(E[z]u+E[x]u))
=(u(y)*(u(z)+u(x)))=(3*(α+1)).
Damit ist bewiesen, daß die Semantik des Programms P, dargestellt durch die
berechnete Funktion, lautet:
B[P](α)=3*(α+1).
2) Wir bestimmen die Semantik von P':
val c=1;
fun f x = if x=0 then c else f(x-1);
(read(x);f(x)).
Für einen beliebigen Eingabewert α:int gilt:
B[P'](α)=E[f(x)]((D[val c=1; fun f x=if x=0 then c else f(x-1)]∅)+{x=α}).
Hierbei ist
D[val c=1; fun f x=if x=0 then c else f(x-1)]=
D[fun f(x)=if x=0 then c else f(x-1)] ° D[val c=1].
Im einzelnen:
D[val c=1]∅=∅+{c=E[1]∅}={c=1},
D[fun f x=if x=0 then c else f(x-1)]{c=1}=
{c=1}+{f=(x, if x=0 then c else f(x-1),{c=1})}=
{c=1,f=(x, if x=0 then c else f(x-1),{c=1})}.
Folglich gilt:
B[P'](α)=E[f(x)] {x=α, c=1, f=(x, if x=0 then c else f(x-1),{c=1})}.
Setzen wir u={x=α, c=1, f=(x, if x=0 then c else f(x-1),{c=1})}, so folgt
E[f(x)]u=E[if x=0 then c else f(x-1)]
(D[fun f x=if x=0 then c else f(x-1); val x=E[x]u]υ(u(f)))=
E[if x=0 then c else f(x-1)]
(D[fun f x=if x=0 then c else f(x-1); val x=E[x]u]{c=1})=
(*)
E[if x=0 then c else f(x-1)]{x=α, c=1, f=(x, if x=0 then c else f(x-1),{c=1})}=
E[c]u=u(c)=1, falls E[x=0]u=wahr, d.h., falls u(x)=0,
11-23
=
E[f(x-1)]u, sonst.
Hierbei ist
(**)
E[f(x-1)]u=E[if x=0 then c else f(x-1)]
(D[fun f x=if x=0 then c else f(x-1); val x=E[x-1]u]υ(u(f)))=
E[f(x-1)]u=E[if x=0 then c else f(x-1)]
(D[fun f x=if x=0 then c else f(x-1); val x=E[x-1]u]{c=1})=
E[if x=0 then c else f(x-1)]{x=α-1, c=1, f=(x, if x=0 then c else f(x1),{c=1})}.
Nun betrachte man die Zeilen (*) und (**), die zu folgender Gleichung führen:
E [ if x=0 then c else f(x-1)]u=
1, falls u(x)=0,
(***)
=
E [ if x=0 then c else f(x-1)] {x=α-1, c=1, f=(x, if x=0 then c else f(x-1),{c=1})}, sonst.
Setzt man nun zur Abkürzung
F:= E[if x=0 then c else f(x-1)],
G:= D[val x=x-1],
u:= {x=α, c=1, f=(x, if x=0 then c else f(x-1),{c=1})}
(wie oben),
so erhält man aus (***) folgende Funktionalgleichung
1, falls u(x)=0,
F(u)=
F(G(u)), sonst.
Gesucht ist in dieser Gleichung die Funktion F, welche die Semantik des Programmstücks
if x=0 then c else f(x-1)
beschreibt; G und u sind bekannt. Die semantische Analyse des Programms P' hat
also nicht – wie in Beispiel 1 – zu einer expliziten Angabe der berechneten Funktion
von P' geführt. Vielmehr haben wir nun eine Funktionsgleichung vorliegen, die erst
aufzulösen ist.
Da es sich bei der obigen Gleichung für F um eine implizite Gleichung handelt, kann
man sich fragen, ob es überhaupt Funktionen F gibt, die diese Gleichung erfüllen.
Dies ist gleichbedeutend mit der Frage, ob man dem analysierten Programm in dem
entwickelten Kalkül der semantischen Funktionen tatsächlich eine Bedeutung zuordnen kann.
11-24
Analysiert man die Gleichung genauer, so stellt sich heraus, daß es sich um eine
sog. Fixpunktgleichung0 der Form
F=τ(F)
handelt, wobei τ ein Funktional ist mit
1, falls u(x)=0,
τ(F)(u)=
F(G(u)), sonst.
Die Lösung F dieser Fixpunktgleichung ist dann die gesuchte Semantik des Programms. Die obige Frage nach der Lösung der Gleichung ist damit auf das Problem
reduziert, unter welchen Bedingungen solche Fixpunkte existieren und ob sie ggf.
eindeutig bestimmt sind. Die Eindeutigkeit ist besonders wichtig, denn die Semantik
des Programmstücks (der Fixpunkt) sollte ja auf keinen Fall mehrdeutig sein. Ferner
sollte der Fixpunkt auch berechenbar sein.
Diesem Problemkreis, dessen Lösung einen relativ hohen technischen Aufwand erfordert, können wir uns in dieser Vorlesung nicht widmen. Wir wollen uns stattdessen mit
einem kurzen Abriß der Vorgehensweise begnügen, wie man den gesuchten Fixpunkt
berechnet, ohne jedoch die zugrundeliegenden mathematischen Strukturen darzustellen oder das Verfahren Fixpunktberechnungsverfahren beweistechnisch herzuleiten.
Dies bleibt Vorlesungen über die Theorie der Programmierung vorbehalten.
11.2.3
Fixpunktberechnung
Wie gesehen ordnet die Funktionensemantik den Programmen einer Programmiersprache Funktionen F als Bedeutung zu, die teilweise – wie in Beispiel 2 aus 11.2.2 –
durch implizite Fixpunktgleichungen der Form F=τ(F) bestimmt sind. Wie geht man vor,
um diese Gleichungen nach F aufzulösen?
Hierzu muß man zunächst überlegen, welcher Art die Quell- und Zielbereiche sind, die
den Funktionen F und τ zugrundeliegen, und welche Funktionen F als Lösungen nur in
Frage kommen können oder sollen. Sodann muß man den Bereichen, auf denen F und τ
operieren, eine geeignete Struktur aufprägen und für τ nur noch gewisse strukturverträgliche Abbildungen zulassen, so daß für solche τ ein eindeutig bestimmter Fixpunkt existiert und ermittelt werden kann.
0 Sei
f: M→M eine Funktion. Jedes x∈M mit f(x)=x heißt Fixpunkt von f.
Beispiel: Sei f: IN→IN definiert durch f(x)=Summe aller Teiler von x ohne x selbst. f besitzt Fixpunkte, z.B.
f(6)=1+2+3=6,
f(28)=1+2+4+7+14=28.
11-25
Die grundlegenden Arbeiten zu diesem Problemkreis stammen von Dana S. Scott:
Outline of a mathematical theory of computation, 1970. Er schlägt als grundlegende
Struktur der Bereiche den vollständigen Verband vor. Später hat sich gezeigt, daß
bereits die Struktur vollständiger partieller ordnungen (cpo) ausreicht.
Welche Intuition steckt hinter diesen partiellen Ordnungen? Die typischen Bereiche in
der Funktionensemantik sind Datentypen wie int, bool usw. τ operiert auf Funktionenklassen über diesen Typen. Da die Funktionen auch partiell definiert sein können, empfiehlt
es sich, die Datentypen zu vervollständigen, also um das "bottom"-Element ⊥ (Bedeutung: "undefiniert") zu erweitern, wie wir es bereits mehrfach getan haben. Partielle
Funktionen lassen sich dann bezgl. ihres Grades an Definiertheit ordnen. Kleinstes
Element bezgl. dieser Ordnung (hier mit ≤ bezeiechnet) ist die total undefinierte Funktion ⊥ mit
⊥(x)=⊥ für alle x.
Zwei Funktionen f und g sind vergleichbar nach folgender Vorschrift:
f≤g :<=> für alle x: (f(x)≠⊥ => f(x)=g(x)),
d.h. g ist an mindestens sovielen Stellen definiert wie f und stimmt an den Stellen, an
denen f definiert ist, mit f überein. Offenbar gilt dann:
⊥≤f für alle f.
≤ ist eine partielle Ordnung. Fordert man nun für die partielle Ordnung eine gewisse
Abschlußeigenschaft, nämlich daß jede Kette x1≤x2≤x1≤... ein Supremum (eine kleinste
obere Schranke) besitzt, und für die Funktionale τ die Monotonie
f≤g => τ(f)≤τ(g)
sowie weitere Eigenschaften, darunter die Stetigkeit, so kann man schließlich zeigen,
daß ein Fixpunkt der Gleichung
F=τ(F)
existiert. Im allgemeinen gibt es jedoch mehrere Fixpunkte, darunter aber nur genau
einen bezgl. der Ordnung ≤ kleinsten Fixpunkt F0. F0 ist also der Fixpunkt, der für die
wenigsten Argumente definiert ist. Dieses F0 definiert man als die gesuchte Semantik
des Programms. F0 läßt sich relativ einfach als Grenzwert
F0=limn→∞τn(⊥)
berechnen. Grundlage dieses Ergebnisses ist der Fixpunktsatz von Kleene, den wir im
folgenden ohne Beweis angeben.
Satz: (Fixpunktsatz von Kleene)
Sei (M,≤) eine vollständige partielle Ordnung und τ: M→M eine stetige Funktion. Dann
besitzt τ einen bezgl. der Ordnung ≤ kleinsten Fixpunkt F0, und es gilt
11-26
F0=limn→∞ τn(⊥).
Der Fixpunktsatz von Kleene liefert folgendes konstruktives Verfahren, um den Fixpunkt
zu berechnen:
1. Schritt:
Setze x0:=⊥.
2. Schritt:
Wiederhole für n=0,1,2,...
xn+1←τ(xn).
Gilt dann irgendwann xr=xr+1, so ist xr=F0 der gesuchte kleinste Fixpunkt. Tritt dieser Fall
nicht ein, so terminiert die Berechnung nicht, jedoch approximiert der Zwischenwert xn
den gesuchten kleinsten Fixpunkt beliebig genau. Häufig gelingt es dann, den Fixpunkt
F0=limn→∞ xn
durch Grenzwertbetrachtungen zu gewinnen.
Nun können wir auch die Fixpunktgleichung aus Abschnitt 11.2.2 lösen, wenn wir uns
vergewissert haben, daß der Fixpunktsatz von Kleene in dieser Situation anwendbar ist.
Tatsächlich gilt – und dies müssen wir hier ohne Beweis hinnehmen –, daß, wann immer
die Bestimmung der semantischen Funktion eines Programms aus µML auf eine Fixpunktgleichung führt, die beteiligten Objekte die Voraussetzungen des Satzes von
Kleene erfüllen.
Folglich ist die implizite Funktionalgleichung der Form (s. letztes Beispiel 11.2.2)
E [ if x=0 then c else f(x-1)]u
1, falls u(x)=0,
=
E [ if x=0 then c else f(x-1)] {x=α-1, c=1, f=(x, if x=0 then c else f(x-1),{c=1})}, sonst,
die Ausgangspunkt aller unserer Überlegungen zur Semantik war, sinnvoll, und sie
besitzt nach dem Satz von Kleene einen eindeutig bestimmten kleinsten Fixpunkt, der
die Bedeutung von
E[if x=0 then c else f(x-1)]
und damit des gesamten Programms definiert. Diesen Fixpunkt wollen wir im folgenden
berechnen.
Beispiele:
1) Zur Abkürzung setzen wir
F:= E[if x=0 then c else f(x-1)],
G:= D[val x=x-1],
u:= {x=α, c=1, f=(x, if x=0 then c else f(x-1),{c=1})}.
Dann ist folgende Gleichung zu lösen:
11-27
1, falls u(x)=0
F(u)=
F(G(u)), sonst.
Setzt man
1, falls u(x)=0
τ(F)(u)=
F(G(u)), sonst,
so kann man den Fixpunktsatz von Kleene verwenden und rechnen:
1, falls u(x)=0
1, falls α=0
τ(⊥)(u)=
=
⊥(G(u)), sonst
⊥, sonst.
1, falls α=0
1, falls u(x)=0
τ2(⊥)(u)=
=
τ(⊥)(G(u)), sonst
1, falls G(u)(x)=0
1, falls α=0
=
⊥(G(G(u))), sonst
1, falls α-1=0
⊥, sonst.
Hierbei ist
G(u)=D[val x=x-1]u=u+{x=E[x-1]u}=u+{x=E[x-1]u-1}=u+{x=α-1}
={x=α-1, c=1, f=(x, if x=0 then c else f(x-1),{c=1})}
und folglich
G(u)(x)=α-1.
Also gilt:
τ2(⊥)(u)=
1, falls α=0
1, falls α=1
⊥, sonst.
1, falls α=0
1, falls u(x)=0
τ3(⊥)(u)=
=
τ2(⊥)(G(u)), sonst
G2(u)(x)=α-2.
Hierbei gilt – wie oben –
1, falls α=0
τ3(⊥)(u)=
1, falls α=1
1, falls α=2
⊥, sonst.
1, falls G(u)(x)=0
=
1, falls α-1=0
1, falls G2(u)(x)=0
1, falls α-2=0
⊥, sonst
⊥, sonst.
Also insgesamt
Dann gilt offenbar allgemein:
1, falls 0≤α≤n-1,
τn(⊥)(u)=
1, falls α=0
11-28
⊥, sonst.
Für den Grenzwert gilt dann
1, falls α≥0,
limn→∞ τn(⊥)(u)=
=: F0(α).
⊥, sonst.
F0 ist der kleinste Fixpunkt des Funktionals τ und die Semantik des Programms. Man
überzeuge sich von der Fixpunkteigenschaft durch Einsetzen von F0 in die Gleichung τ(F)=F. Das Programm berechnet also die Funktion
1, falls α≥0,
F0(α)=
⊥, sonst.
2) Wir bestimmen die Semantik des Programms P"
fun f x=if x=0 then 0 else f x;
(read(y); f(y)).
Für einen beliebigen Eingabewert α:int gilt:
B[P"](α)=E[f(y)]((D[fun f x=if x=0 then 0 else f x]∅)+{y=α})
=E[f(y)](∅+{f=(x,if x=0 then 0 else f x,∅)}+{y=α})
=E[f(y)]{y=α,f=(x,if x=0 then 0 else f x,∅)}.
Setzen wir zur Abkürzung
u={y=α,f=(x,if x=0 then 0 else f x,∅)}.
Dann gilt:
E[f(y)]u=E[if x=0 then 0 else f x]
(D[fun f x=if x=0 then 0 else f x; val x=E[y]u]υ(u(f)))=
E[if x=0 then 0 else f x] (D[fun f x=if x=0 then 0 else f x; val x=E[y]u]∅)=
E[if x=0 then 0 else f x]{x=α, f=(x, if x=0 then 0 else f x,∅)}.
Mit u'={x=α, f=(x, if x=0 then 0 else f x,∅)} gilt dann:
(*)
E[if x=0 then 0 else f x]u'
E[0]u'=0, falls E[x=0]u'=wahr, d.h., falls u'(x)=α=0,
=
E[f x]u', sonst.
Hierbei ist
(**)
E[f x]u'=E[if x=0 then 0 else f x]
(D[fun f x=if x=0 then 0 else f x; val x=E[x]u']υ(u'(f)))=
E[f x]u'=E[if x=0 then 0 else f x]
(D[fun f x=if x=0 then 0 else f x; val x=E[x]u']∅)=
E[if x=0 then 0 else f x]u'.
Nun betrachte man die Zeilen (*) und (**), die zu folgender Gleichung führen:
11-29
0, falls u'(x)=0,
(***)
E[if x=0 then 0 else f x]u'=
E[if x=0 then 0 else f x]u', sonst.
Setzt man nun zur Abkürzung
F:= E[if x=0 then 0 else f x],
so erhält man aus (***) folgende Funktionalgleichung
0, falls u'(x)=0,
F(u')=
F(u'), sonst.
Wir wenden den Satz von Kleene an und berechnen den Fixpunkt, indem wir setzen
0, falls u'(x)=0
τ(F)(u')=
F(u'), sonst,
so kann man den Fixpunktsatz von Kleene verwenden und rechnen:
0, falls u'(x)=0
0, falls α=0
τ(⊥)(u)=
=
⊥(u'), sonst
⊥, sonst.
0, falls u'(x)=0
τ2(⊥)(u)=
=
τ(⊥)(u'), sonst
0, falls α=0
0, falls u'(x)=α=0 =
⊥(u'), sonst
0, falls α=0
⊥, sonst.
Also gilt:
0, falls α=0
τ2(⊥)(u)=
⊥, sonst.
Wegen τ2(⊥)(u)=τ(⊥)(u) ist F0:=τ(⊥) der kleinste Fixpunkt und die Semantik des Programms P". Man überzeuge sich von der Fixpunkteigenschaft durch Einsetzen von
F0 in die Gleichung τ(F)=F. Das Programm berechnet also die Funktion
0, falls α=0,
F0(α)=
⊥, sonst.
Was this manual useful for you? yes no
Thank you for your participation!

* Your assessment is very important for improving the work of artificial intelligence, which forms the content of this project

Download PDF

advertisement