Martin Sulzmann
Im folgenden betrachten wir Beispiele von Programmen. Dazu gibt es Veständnissfragen und kleinere Programmieraufgaben.
Was ist der Sinn und Zweck folgendes Programmtextes. Könnte ein Fehler darin versteckt sein?
Die Intention ist die Suche nach dem Ende eines String, siehe
a[i] != '\0'
.
Da es in C keine “array out of bounds check” gibt, wird versucht
via i >=0 && i < MAX
unerlaubte Zugriffe
abzufangen.
Passt alles?
Nicht ganz. Die Reihenfolgen der einzelnen Bedingungen sind falsch.
Aus der Mathematik kennt man dass &&
ein
kommutativer Operator ist. Dies gilt hier nicht mehr.
Z.B. die Anweisung a[i] != '\0'
kann zu einer
Speicherverletzung führen weil der Index i sich ausserhalb der erlaubten
Grenzen befindet. Die Bedingungen
i >=0 && i < MAX
liefern daher FALSCH. Aber
das Programm steigt von vorher aus, wegen der Speicherverletzung.
Deshalb sollte es richtig lauten.
Zuerst die Überprüfung, ob Indexgrenzen gültig sind.
Dann der Zugriff auf die Position.
Das Prinzip dahinter heisst “Kurzschlussauswertung”:
Im Falle von “UND” verknüpften Bedingungen werden diese in der textuellen Reihenfolge von links nach rechts ausgewertet.
Falls eine (Teil)Bedingung FALSCH liefert, liefert der ganze Ausdruck FALSCH.
Wichtig hier ist, dass die Anweisung a[i] != '\0'
gar nicht ausgewertet wird.
Analog dazu ist die Auswertung von “ODER” verknüpften Bedingungen. Wiederum in textueller Reihenfolge von links nach rechts. Sobald eine (Teil)Bedingung WAHR liefert, liefert der ganze Ausdruck WAHR. Danach folgende Bedingungen werden nicht mehr ausgewertet.
Was bedeutet dies für die Verhersagbarkeit der zeitlichen Ausführung eines Programmes? Da auf Grund der Kurzschlussauswertung nicht alle (Teil)Bedingungen ausgewertet werden müssen, kann die Ausführungszeit sehr unterschiedlich sein.
Betrachte folgendes (künstliches) Beispiel.
Angenommen x ist kleiner als 5. Dann wird der Ausdruck
factorial(y) > x
nicht ausgewertet.
Angenommen x ist größer als 5. Der Ausdruck
factorial(y) > x
muss ausgewertet werden und kann je
nach Paramter y einige Zeit in Anspruch nehmen (die Berechnungsformel
für die Fakultät einer Zahl ist zwar einfach, benötigt aber einen
gewissen Berechnungsaufwand).
Zusammengefasst. Je nach Wert des Parameters x ist die zeitliche Ausführung des Programms unterschiedlich.
Aber ist es nicht von Vorteil, einen Ausdruck wie
factorial(y) > x
erst gar nicht zu berechnen (wenn der
linke Teil schon FALSCH liefert)? Jein. In gewissen Bereichen (man denke
an eingebettete Systeme) möchte man, dass die zeitliche Ausführungen
eines Programmes unabhängig ist von irgendwelchen Laufzeitparametern
(“best case = worst case”). Das Prinzip der Kurschlussauswertung macht
dies aber schwierig.
Wie könnte man die Kurschlussauswertung umgehen? Der oft angewendete Trick ist die Booleschen Operatoren durch ihre Bitweisen Pendants zu ersetzen.
Z.B. im folgenden Programmtext werden immer beide Bedingungen ausgewertet
Die Regel lautet. Verwende &
anstatt
&&
und |
anstatt ||
.
Dadurch wird die Kurzschlussauswertung umgangen.
Ist dies aber nicht gefährlich, siehe obiges Beispiel zum Arrayzugriff? Richtig. Falls aber gewünscht ist die Kurzschlussauswertung zu umgehen, muss das Programm umgeschrieben werden. Z.B. im Falle des obigen Beispiels
bool stop = false;
while(!stop) {
if(i >=0 & i < MAX) {
if(a[i] != '\0') {
i++;
} else {
stop = true;
}
} else {
stop = true; // out-of bounds
}
}
Diese Lösung ist “aufwendiger” aber in gewisser Hinsicht ist der Programmablauf viel durchschaubarer. Solch ein Programmierstil wird vor allem in Bereichen angewandt in denen die Robustheit von Programmen (zeitliches Verhalten, Korrektheit, …) eine grosse Rolle spiel. Man denke an eingebettete Systeme im Flugzeug, Auto, Ampelsteuerung etc.
Vergleich von while, do-while und for Schleifen.
Geben Sie für jede Art von Schleife ein Beispiel.
Kann jede Schleifenart in die andere übersetzt werden?
Welche Schleife finden Sie am sinnvollsten?
Braucht man Schleifen überhaupt?
Schleifen sind in der Tat nicht notwendig, sobald ihre Programmiersprache rekursive Funktionsaufrufe unterstützt. Ein (praktisches) Problem dabei ist, dass die Schachtelungstiefe von Funktionsaufrufen in der Regel begrenzt ist
Betrachten Sie folgende Ausdrücke. Annahme dabei ist
Was machen die einzelnen Ausdrücke?
Betrachten Sie folgenden Programmtext.
Schreiben Sie obiges Programm um. Dabei sollen Sie anstatt einer while Schleife eine for Schleife verwenden und anstatt des Arrayzugriffs nur Zeigerzugriffe verwenden.
C Programmierkonzepte: Bitoperationen, Schleifen und bedingte Anweisungen
Wir betrachten eine Menge von Elementen, z.B.
{A, B, C, D}
. Aus der Kombinatorik wissen wir, dass es
genau 2^4 = 16
verschiedene Untermenge für eine Menge mit 4
Elementen gibt.
Für unser obiges Beispiel, gibt es folgende Untermengen.
0 : {}
1 : {A }
2 : {B }
3 : {A B }
4 : {C }
5 : {A C }
6 : {B C }
7 : {A B C }
8 : {D }
9 : {A D }
10 : {B D }
11 : {A B D }
12 : {C D }
13 : {A C D }
14 : {B C D }
15 : {A B C D }
Ihre Aufgabe ist es ein C Programm zu schreiben, welches die Menge aller Untermengen einer 4-elementigen Menge auf der Konsole ausgibt.
#include <stdio.h>
/*
Fuer Menge mit n Element gibt es 2^n (2 hoch n) Untermengen.
Mit Hilfe der Bitshiftoperation koennen wir 2^n einfach berechnen.
Wir schieben die 1, n Stellen nach links: 1 << n.
Jede Untermenge ist dargestellt als Zahl von 0 .. (1 << n) - 1.
Fuer jede Untermenge/Zahl testen wir welche Elemente vorkommen.
Die Elemente sind an Bitpositionen gebunden.
Fuer das i-te Element muessen wir testen, ob dieses Bit gesetzt ist.
Falls x die aktuelle Untermenge/Zahl, dann
(x & (1<<i))
(1<<i) ist eine Bitmaske und wird Bitweise verundet mit x.
Dadurch testen wir, ob in x das i-te Bit gesetzt wird.
Falls ja, fuegen wir i zur aktuellen Untermenge hinzu.
Wir nehmen an, dass i=0 A ist, i=1 B ist, usw.
Um das i-te Element auszugeben, berechnen wir dessen Ascii-code.
Der ist 65+i wobei 65 der Ascii-code von A ist.
*/
int main() {
int n = 4;
int x,i;
for(x=0; x < (1 << n); x++) {
printf("\n { ");
for(i=0; i < n; i++) {
if(x & (1<<i)) {
printf(" %c", 65+i);
}
}
printf(" } \n");
}
}
Noch ein Vorschlag. Verwendet Funktionen (so haben wir weiteres Beispiel mit Funktionen). Der Vorschlag ist aber weniger allgemein. Behandelt nur den Fall n=4.
Falls Bit Position gesetzt liefere die Position als Wert zurück.
Ansonsten gebe -1
zurück.
Ausgabe von Element an bestimmer Position.
void printElem(unsigned short int x, int pos) {
switch(pos) {
case 0 : printf("A ");
break;
case 1 : printf("B ");
break;
case 2 : printf("C ");
break;
case 3 : printf("D ");
break;
}
}
Für alle möglichen Bitkombinationen (gleich alle Zahlen zwischen
0
und 15
), teste für jede Position ob Element
vorhanden.
int main() {
unsigned short int x;
int pos;
for(x=0; x < 16; x++) {
printf("\n %d : {", x);
for(pos=0; pos<4; pos++) {
printElem(x, test(x,pos));
}
printf("}");
}
printf("\n");
return 0;
}
Erweiteren Sie die Aufgabe:
Geben Sie die Menge aller Untermenge einer
n
-elementigen Menge aus, wobei n
ein frei
wählbarer Parameter ist.
Als Namensbezeichnung für Elemente benutzen Sie einfach 1, 2, …
# include <stdio.h>
int test(unsigned short int x, int pos) {
if (x & (1<<pos)) return pos;
return -1;
}
void printElem(unsigned short int x, int pos) {
switch(pos) {
case 0 : printf("A ");
break;
case 1 : printf("B ");
break;
case 2 : printf("C ");
break;
case 3 : printf("D ");
break;
}
}
int main() {
unsigned short int x;
int pos;
for(x=0; x < 16; x++) {
printf("\n %d : {", x);
for(pos=0; pos<4; pos++) {
printElem(x, test(x,pos));
}
printf("}");
}
printf("\n");
return 0;
}
Was sticht ins Auge?
In C/C++ ist es möglich “freistehende” Funktionen zu definieren welche nicht an eine Klasse gebunden sind.
Funktionsdefinitionen dürfen nicht geschachtelt sein.
Funktionen müssen erst deklariert werden bevor man die Funktion verwenden kann. Dies gilt auch für Variablen, Klassen etc.
Jede Funktionsdefinition impliziert eine Funktionsdeklaration. Obige Reihenfolge ist wichtig. Aufgabe. Vertausche “main” mit “test” und kompiliere neu.
Reihenfolgen kann vertauscht werden, solange “Vorwärtsreferenzen” gegeben sind. Betrachte folgende Variante.
# include <stdio.h>
// forward references
int test(unsigned short int x, int pos);
void printElem(unsigned short int x, int pos);
int main() {
unsigned short int x;
int pos;
for(x=0; x < 16; x++) {
printf("\n %d : {", x);
for(pos=0; pos<4; pos++) {
printElem(x, test(x,pos));
}
printf("}");
}
printf("\n");
return 0;
}
int test(unsigned short int x, int pos) {
if (x & (1<<pos)) return pos;
return -1;
}
void printElem(unsigned short int x, int pos) {
switch(pos) {
case 0 : printf("A ");
break;
case 1 : printf("B ");
break;
case 2 : printf("C ");
break;
case 3 : printf("D ");
break;
}
}
Wir betrachten Funktionen und komplexe Datentypen an einem Beispiel. Ziel ist die Darstellung von rationalen Zahlen und Operationen auf rationalen Zahlen. In diesem Beispiel betrachten wir auch verschiedene Testansätze (User-Tests, Unit-Tests, Invarianten). Eingestreut finden sich eine Reihe von Aufgaben und Fragen. Es ist wichtig den kompletten Text zu lesen!
Eine rationale Zahl beschreibt das Verhältnis zweier ganzen Zahlen.
Die eine wird als Zähler, die andere als Nenner bezeichnet. Z.B.
5 / 3
ist eine rationale Zahl mit Zähler 5 und Nenner 3.
Wer es nochmal genauer nachlesen will siehe hier.
Hier ist eine direkte Darstellung in C als Struktur.
Wir verwenden die folgende Abkürzung.
Im folgenden definieren wir eine Reihe von Funktionen welche rationale Zahlen manipulieren.
void printRZ(RZ x); // Ausgabe
RZ scanRZ(); // Eingabe
RZ multRZ(RZ x, RZ y); // Multiplikation
RZ addRZ(RZ x, RZ y); // Addition
RZ skaliereRZ(RZ x, int k); // Skalierung
Obige Funktionsprototypen spezifizieren wie die einzelnen Funktionen
verwendet werden. Z.B. RZ multRZ(RZ x, RZ y);
spezifiert
die Funktion multRZ
als Funktion mit zwei Parametern vom
Typ RZ
und Rückgabe vom Typ RZ
. Alternative
Name für Funktionsprototyp sind Funktionsdeklaration oder auch
Funktionssignatur.
Wir betrachten die Eingabe und Ausgabe welche jeweils Komponentenweise geschieht.
void printRZ(RZ x) {
printf("%d/%d", x.Zaehler, x.Nenner);
}
RZ scanRZ() {
RZ x;
scanf("%d", &x.Zaehler);
printf(" / ");
scanf("%d", &x.Nenner);
return x;
}
Der Einfachheithalber überprüfen wir nicht, ob der Nenner 0 ist.
Betrachte scanf("%d", &x.Zaehler);
. Der Ausdruck
&x.Zaehler
übergibt den Zähler als Referenz zur
scanf
Funktion. Der eingelesene String wird in eine Zahl
umgewandelt und kann dadurch x.Zaehler
zugewiesen
werden.
Beachte. Der Ausdruck &x.Zaehler
wird interpretiert
als &(x.Zaehler)
. Sprich .
hat eine höhere
Präferenz als als &
.
Es folgt skaliereRZ
und addRZ
. Die
Definition von multRZ
findet sich unten.
RZ addRZ(RZ x, RZ y) {
RZ z, xTemp, yTemp;
xTemp = skaliereRZ(x,y.Nenner);
yTemp = skaliereRZ(y, x.Nenner);
z.Zaehler = xTemp.Zaehler + yTemp.Zaehler;
z.Nenner = xTemp.Nenner;
return z;
}
RZ skaliereRZ(RZ x, int k) {
RZ z;
z.Zaehler = x.Zaehler*k;
z.Nenner = x.Nenner*k;
return z;
}
Beachte. Die Definition von addRZ
verwendet
skaliereRZ
welches aber erst danach definiert wird. Dies
ist kein Problem da wir oben schon alle Funktionen mit Hilfe von
Funktionsprototypen spezifziert haben.
Betrachte addRZ
. Mit Hilfe von skaliereRZ
bringen wir x
und y
auf einen gemeinsamen
Nenner. Von den daraus folgenden rationalen Zahlen, müssen wir dann nur
noch die Zähler addieren.
Wie können wir sicherstellen, dass sich kein Bug in unserer Implementierung befindet. Durch Tests. Falls ein Test fehlschlägt, haben wir möglicherweise einen Bug entdeckt (Der Test selber könnte einen Bug haben!). Falls die Tests erfolgreich sind, heisst es nicht, dass es nicht doch noch einen Bug gibt. Zumindest aber, im Falle einer ausreichenden Anzahl von Tests, haben wir eine gewisse Sicherheit. Wir betrachten verschiedene Arten von Tests.
Der User liefert die Eingaben. Verschiedene Operationen werden ausgeführt. Anhand der Ausgabe überprüft der User ob es stimmt.
void test1() {
RZ x, y, z;
printf("Test 1 \n");
printf("Eingabe1: \n");
x = scanRZ();
printf("\n Eingabe2: \n");
y = scanRZ();
printRZ(x); printRZ(y);
printf("\n Add Ausgabe: ");
printRZ(addRZ(x,y));
printf("\n Mult Ausgabe: ");
printRZ(multRZ(x,y));
}
Wir automatisieren User-Tests. Dies liefert dann Unit-Tests. Im Programm legen wir die Eingaben fest. Führen die Operation (oder eine Sequenz von Operationen) aus. Und vergleichen dann das berechnete mit dem erwarteten Ergebnis. Diese Test Methode ist unter dem Namen Unit-Tests bekannt. In vielen IDES sind Unit-Tests integriert (sogenannte boilerplate code wird von der IDE generiert).
Hier ein paar Unit-Tests für unser Beispiel.
void unitTest1() {
RZ x, y, z;
x.Zaehler = 5; x. Nenner = 3;
y.Zaehler = 3; y.Nenner = 5;
z = multRZ(x,y);
if(z.Zaehler != z.Nenner)
printf("\n Fehler");
x.Zaehler = 2; x. Nenner = 3;
y.Zaehler = 1; y.Nenner = 3;
z = addRZ(x,y);
if(z.Zaehler != z.Nenner)
printf("\n Fehler");
}
Unit-Tests sind wichtig. Ein Nachteil ist, dass für jeden Testfall (Eingabe) und Operationssequenz das erwartete Ergebnis bekannt sein muss.
Assertions (auf Deutsch Zusicherung) sind Aussagen die an bestimmte Programmzustände gekoppelt sind. Invarianten sind Aussagen die immer gelten. Zumindest für mehrere Programmabfolgen wie z.B. Schleifen. Hier ist ein einfaches Beispiel.
int i;
i = 1;
i++;
// Assertion die prueft, dass i inkrementiert wurde und den Wert 2 hat
if(i != 2)
printf("\n Fehler");
for(; i<10; i++) {
// Invariante
if(i <= 0)
printf("\n Fehler");
}
i != 2
und i <= 0
sind beides Aussagen
(also Assertions). Die Aussage i != 2
ist an einem
bestimmte Programmzustand (Programmpunkt) gekoppelt. Die Aussage
i <= 0
negiert soll für die Schleife gelten. Im
letzteren Fall spricht man deshalb von einer Invariante.
Welche Invarianten gelten für unseren Fall?
Betrachte die Multiplikation rationaler Zahlen. Folgendes gilt:
n/m * m/n = (n*m) / (n*m)
Für Zahlen m
und n
, und Operation
n/m * m/n
gilt die Invariante das im Ergebnis Nenner und
Zähler gleich sein müssen.
Angewandt auf unsere Implementierung.
int m,n;
RZ x,y,z;
for(n = 1; n <6; n++) {
for(m = 1; m < 6; m++) {
x.Zaehler = n; x.Nenner = m;
y.Zaehler = m; y.Nenner = n;
z = multRZ(x,y);
if(z.Zaehler != z.Nenner)
printf("\n Fehler");
}
}
Finden sie weitere Invarianten (z.B. für addRZ).
Betrachte folgende Variante von skaliereRZ
. Die zu
skalierende rationale Zahl wird als Referenz übergeben.
void skaliereRZ2(RZ *x, int k) {
RZ z;
(*x).Zaehler = (*x).Zaehler*k;
(*x).Nenner = (*x).Nenner*k;
}
Zur Erinnerung. Deklaration RZ *x
spezifiert
x
als Referenz auf einen Wert vom Typ RZ
. Um
auf den eigentlich Wert (rationale Zahl) zuzugreifen, schreiben wir
*x
. Der Wert ist eine Struktur. Der Zugriff auf das Zaehler
Element ist wie folgt: (*x).Zaehler
.
Wir betrachten eine Variante von addRZ
welche
skaliereRZ2
verwendet.
RZ addRZ2(RZ x, RZ y) {
RZ z;
skaliereRZ2(&x,y.Nenner);
skaliereRZ2(&y, x.Nenner);
z.Zaehler = x.Zaehler + y.Zaehler;
z.Nenner = x.Nenner;
return z;
}
Beachte: Bei Aufruf einer Funktion mit Referenzübergabe muss die
Referenz und nicht der Wert übergeben werden. Deshalb finden wir z.B.
skaliereRZ2(&x,y.Nenner)
. Mit &y
üergeben wir die Referenz auf y
, mit y.Nenner
übergeben wir den Wert gespeichert im Nenner von y
.
In der Implementierung von addRZ2
hat sich ein Fehler
eingeschlichen. Die Addition zweier rationaler Zahlen wird nicht immer
korrekt ausgeführt. Finden sie den Fehler mit Hilfe von Tests (z.B.
Unit-Tests oder mit Hilfe von Invarianten). Beheben sie den Fehler.
Implementierung sie eine Funktion welche die rationale Zahl kürzt.
Folgende Kürzungsregel gilt: (k * m) / (k * n) = m / n
.
Enthält Lösungsvorschläge für einen Teil der Aufgaben.
#include <stdio.h>
// Rationale Zahlen: Zaehler/Nenner.
// Repraesentiert als struct.
// Oft schreiben wir x1/x2 als Kurzform fuer x.Zahler/x.Nenner.
struct RZ_ {
int Zaehler;
int Nenner;
};
// Abkuerzende Schreibweise.
typedef struct RZ_ RZ;
// Geht auch direkt. Typedef kombiniert mit Strukturdeklaration.
/*
typedef struct RZ_ {
int Zaehler;
int Nenner;
} RZ;
*/
// Vorwaertsfunktions Deklarationen
void printRZ(RZ x);
RZ scanRZ();
RZ multRZ(RZ x, RZ y);
RZ addRZ(RZ x, RZ y);
RZ skaliereRZ(RZ x, int k);
// Ausgabe.
void printRZ(RZ x) {
printf("%d/%d", x.Zaehler, x.Nenner);
}
// Eingabe.
RZ scanRZ() {
RZ x;
scanf("%d", &x.Zaehler);
printf(" / ");
scanf("%d", &x.Nenner);
return x;
}
// Multiplikation ist Komponenteweise.
// x1/x2 * y1/y2
// = (x1*y1) / (x2*y2)
RZ multRZ(RZ x, RZ y) {
RZ z;
z.Zaehler = x.Zaehler * y.Zaehler;
z.Nenner = x.Nenner * y.Nenner;
return z;
}
// Addition. Auf gemeinsamen Nenner bringen.
// x1/x2 + y1/y2
// = (x1*y2) / (x2*y2) + (y1*x2) / (x2*y2)
// = (x1*y2 + y1*x2) / (x2*y2).
RZ addRZ(RZ x, RZ y) {
RZ z, xTemp, yTemp;
xTemp = skaliereRZ(x,y.Nenner);
yTemp = skaliereRZ(y, x.Nenner);
z.Zaehler = xTemp.Zaehler + yTemp.Zaehler;
z.Nenner = xTemp.Nenner;
return z;
}
// Skaliere.
RZ skaliereRZ(RZ x, int k) {
RZ z;
z.Zaehler = x.Zaehler*k;
z.Nenner = x.Nenner*k;
return z;
}
// Variante mit Referenzuebergabe.
void skaliereRZ2(RZ *x, int k) {
RZ z;
(*x).Zaehler = (*x).Zaehler*k;
(*x).Nenner = (*x).Nenner*k;
}
// Add Variante welche skaliereRZ2 verwendet
RZ addRZ2(RZ x, RZ y) {
RZ z;
skaliereRZ2(&x,y.Nenner);
skaliereRZ2(&y, x.Nenner);
z.Zaehler = x.Zaehler + y.Zaehler;
z.Nenner = x.Nenner;
return z;
}
// Beachte:
// In C muessen all Variablen am Anfang der Funktion deklariert werden.
// In C++ duerfen Variablen auch innerhalb einer Funktion definiert werden.
void test1() {
RZ x, y, z;
printf("Test 1 \n");
printf("Eingabe1: \n");
x = scanRZ();
printf("\n Eingabe2: \n");
y = scanRZ();
printRZ(x); printRZ(y);
printf("\n Add Ausgabe: ");
printRZ(addRZ(x,y));
printf("\n Mult Ausgabe: ");
printRZ(multRZ(x,y));
}
void unitTest1() {
RZ x, y, z;
x.Zaehler = 5; x. Nenner = 3;
y.Zaehler = 3; y.Nenner = 5;
z = multRZ(x,y);
if(z.Zaehler != z.Nenner)
printf("\n Fehler");
x.Zaehler = 2; x. Nenner = 3;
y.Zaehler = 1; y.Nenner = 3;
z = addRZ(x,y);
if(z.Zaehler != z.Nenner)
printf("\n Fehler");
}
RZ inverseRZ(RZ x) {
int tmp;
tmp = x.Zaehler;
x.Zaehler = x.Nenner;
x.Nenner = tmp;
return x;
}
void invarianten() {
int i,n,m;
RZ rz[4] = { {1,2}, {3,4}, {5,2}, {4,7} }; // Fest definierte Auswahl von Tests
RZ x,y,z;
n = 4;
// mult: n/m * m/n liefert Zaehler == Nenner
for(i=0; i<n; i++) {
x = multRZ(rz[i], inverseRZ(rz[i]));
if(x.Zaehler != x.Nenner)
printf("\n Fehler");
}
for(n = 1; n <6; n++) {
for(m = 1; m < 6; m++) {
x.Zaehler = n; x.Nenner = m;
y.Zaehler = m; y.Nenner = n;
z = multRZ(x,y);
if(z.Zaehler != z.Nenner)
printf("\n Fehler");
}
}
// add: 1/m + (m-1)/m liefert Zaehler == Nenner
m = 10;
for(i=2; i<m; i++) {
x.Zaehler = 1; x.Nenner = i;
y.Zaehler = i-1; y.Nenner = i;
z = addRZ(x,y);
if(z.Zaehler != z.Nenner)
printf("\n Fehler %d %d %d \n", i, z.Zaehler, z.Nenner);
}
// add2 Variante
for(i=2; i<m; i++) {
x.Zaehler = 1; x.Nenner = i;
y.Zaehler = i-1; y.Nenner = i;
z = addRZ2(x,y);
if(z.Zaehler != z.Nenner)
printf("\n Fehler %d %d %d \n", i, z.Zaehler, z.Nenner);
}
}
int main() {
// test1();
unitTest1();
invarianten();
}
Wir betrachten Strings und Manipulationen auf Strings. Hintergrund zu Strings. Siehe C - Teil 3.
Ein Wort ist eine Sequenz von Zeichen in der das Leerzeichen (white-space) nicht vorkommt. Aufgabe ist es eine Funktion zu schreiben, die die Anzahl der Wörter in einem String zählt.
Verwenden Sie folgenden Funktionsprototypen.
Wir entwickeln eine Musterlösung für diese Aufgabe. Wichtig dabei ist der Nachweis der Korrektheit.
Um ein erstes Verständnis zu bekommen betrachten wir mögliche Eingaben und die erwarteten Ausgaben.
Bevor man mit der Implementierung anfängt ist es elementar wichtig genau die Aufgabenstellung zu analysieren. WAS genau ist gefordert? WAS sind typische Eingaben und die erwarteten Ergebnisse?
Das WIE implementiere ich die Aufgabenstellung ist hier erst einmal zweitrangig.
Deshalb entwerfen wir als erstes einen Testrahmen, mit dessen Hilfe wir unsere Implementierung testen können. Von der “Java” Vorlesung sind Sie mit Unit-Tests vertraut. Solch ein Unit-Test Framework steht uns hier nicht zur Verfügung (gibt es natürlich auch für C/C++). Den notwendigen Testrahmen haben wir uns aber schnell selber gebaut.
Zuerst beschreiben wir die möglichen Testergebnisse.
Ein Testfall besteht aus einem String und dem erwarteten Ergebnis. Wir schreiben eine kleine Hilfsfunktion, die einen Testfall ausführt und auf das erwartete Ergebnis testet.
Test testCount(char* input, int expected) {
Test t;
if(expected == count(input)) {
t = OK;
}
else {
t = FAIL;
}
return t;
}
Wir werden sicherlich eine Reihe von Testfällen benötigen, deshalb fassen wir einen Testfall in einer Struktur zusammen.
Folgende Hilfsfunktion erlaubt uns die automatische Ausführung einer ganzen Reihe von Testfällen.
void runTests(int no, TestCase test[]) {
Test t;
int i;
for(i=0; i < no; i++) {
printf("Test %d: ", i);
t = testCount(test[i].input, test[i].expected);
if(OK == t)
printf("OK \n");
if(FAIL == t)
printf("FAIL \n");
}
}
Eingabe no
beschreibt die Anzahl der Testfälle. Die
eigentlichen Testfälle werden als Array test
bestehend aus
Strukturen des Typs TestCase
übergeben. Auf jeden Testfall
wenden wir die Hilfsfunktion testCount
an.
In unseren Fall protokollieren wir die Testergebnisse durch Aufgabe auf der Konsole. In Industrieprojekten werden die Testergebnisse normalerweise auf Platte geschrieben und mit der Versionsnummer der getesteten Software versehen.
Hier ist eine Auswahl signifikanter Testfälle.
int main() {
const int testNo = 9;
TestCase tests[9] = {
{"", 0},
{"Hallo", 1},
{" Hallo", 1},
{"Hallo", 1},
{" Hallo ", 1},
{"Hal lo", 2},
{" Hal lo", 2},
{"Hal lo ", 2},
{" Hal lo ", 2}
};
runTests(testNo,tests);
}
Den Testrahmen haben wir also, wir haben aber immer noch keine
einzige Zeile der Funktion count
implementiert! Keine Angst
kommt gleich. Die wichtige Nachricht an dieser Stelle ist: Ein Grossteil
der Entwicklungszeit wird nicht auf das WIE verwendet, der
meiste Entwicklungsaufwand beschäftigt sich mit WAS:
Nun endlich zur eigentlichen Implementierung. Betrachten wir noch einmal die Beispieleingaben und -ausgaben.
Im Falle des leeren Strings geben wir 0
zurück.
Ansonsten suchen wir den Anfang eines Wortes? WIE machen wir das?
Entweder (a) wir befinden uns schon am Anfang eines Wortes, oder (b) das aktuelle Zeichen ist ein Leerzeichen.
Im Fall (b) überspringen wir alle Leerzeichen. Aha, wir haben die Aufgabe zerlegt in eine Teilaufgabe. Hier ist eine Funktion, die die Teilaufgabe (a) umsetzt.
Wir “iterieren” durch den String solange bis wir auf ein Zeichen treffen, welches nicht eine Leerzeichen ist. Rückgabe ist ein Zeiger auf das getroffene Nicht-Leerzeichen.
Betrachten wir Fall (a). Wir befinden uns am Anfang eines Wortes. Ein Wort besteht aus einer Sequenz von Nicht-Leerzeichen. Also iterieren wir durch den String bis wir auf ein Leerzeichen treffen. Das stimmt fast! Zu beachten ist hier der Spezialfall, dass wir das Ende des Strings erreichen. Fall (a) lässt sich wiederum sehr einfach mit Hilfe einer Funktion beschreiben.
Wir haben jetzt die einzelnen Teilaufgaben. Damit lässt sich nun die
count
Aufgabe einfach lösen.
Zuerst in Pseudo-Code:
Solange das Ende nicht erreicht ist:
Überspringe alle Leerzeichen
Fall Zeichen gefunden
Hier ist die (literale) Umsetzung.
int count(char* input) {
int cnt = 0;
while(*input != '\0') {
input = leerzeichen(input);
if(*input != '\0') {
cnt++;
input = zeichen(input);
}
}
return cnt;
}
Was kann man an der Lösung verbessern?
Performance. Z.B. leerzeichen und zeichen als inline deklararieren.
Die Namensgebung ist auch verbesserungswürdig. Anstatt
leerzeichen
ein treffender Name ist
leerzeichenUeberspringen
oder skipBlanks
auf
Englisch.
#include "stdio.h"
// Ueberspringe alle Leerzeichen
// Rueckgabe ist Zeiger auf das erste Nichtleerzeichen
char* leerzeichen(char* input) {
while(*input == ' ')
input++;
return input;
}
// Scanne durch string solange bis wir auf ein
// Leerzeichen oder das Ende des Strings treffen.
// Effektiv ueberspringen wir ein Wort.
// Rueckgabe: Zeiger auf Ende oder Leerzeichen.
char* zeichen(char* input) {
while(*input != '\0' && *input != ' ')
input++;
return input;
}
int count(char* input) {
int cnt = 0;
// Solange das Ende nicht erreicht ist:
// 1. Ueberspringe alle Leerzeichen
// 2. Falls Zeichen gefunden
// (a) setze Zaehler hoch
// (b) Gehe zu Wortende
while(*input != '\0') {
input = leerzeichen(input);
if(*input != '\0') {
cnt++;
input = zeichen(input);
}
}
return cnt;
}
typedef enum {
OK,
FAIL
} Test;
Test testCount(char* input, int expected) {
Test t;
if(expected == count(input)) {
t = OK;
}
else {
t = FAIL;
}
return t;
}
typedef struct {
char* input;
int expected;
} TestCase;
void runTests(int no, TestCase test[]) {
Test t;
int i;
for(i=0; i < no; i++) {
printf("Test %d: ", i);
t = testCount(test[i].input, test[i].expected);
if(OK == t)
printf("OK \n");
if(FAIL == t)
printf("FAIL \n");
}
}
int main() {
const int testNo = 9;
TestCase tests[9] = {
{"", 0},
{"Hallo", 1},
{" Hallo", 1},
{"Hallo", 1},
{" Hallo ", 1},
{"Hal lo", 2},
{" Hal lo", 2},
{"Hal lo ", 2},
{" Hal lo ", 2}
};
runTests(testNo,tests);
}
#include "stdio.h"
/*
1. Woerter in einem String zaehlen.
Was ist ein Wort?
Wir definieren ein Wort als eine Sequenz von
Zeichen welche nicht getrennt ist durch Leerzeichen.
2. Zustands-basierte Modellierung.
Wir definieren unser Alphabet als die Menge
Sigma = { CH, EOS, WS }
wobei
EOS = Nullterminator
WS = Leerzeichen ('white space')
CH = Zeichen ('character')
Die Menge von Zustaenden und Transitionen ist wie folgt.
1 -- WS --> 1
1 -- EOS --> 2
1 -- CH --> 3
3 -- WS --> 1
3 -- CH --> 3
3 -- EOS --> 2
Menge von Zustaenden ist {1,2,3}
1 ist der Startzustand
3 ist der Finalzustand
Obiger Automat ist ein Beipsiel eines endlichen Automaten.
3. Zaehlen von Woertern.
Um die Woerten zu zaehlen, benoetigen wir neben einer Eingabe (Zeichen WS, CH, EOS)
auch eine Ausgabe (Anzahl der gefunden Woerter).
Dazu koennen wir einen Mealy/Moore Automaten verwenden.
Z.B. im Fall eines Mealy Automaten koennen wir auf die Transition eine Ausgabe legen.
Notation.
Wir schreiben
state1 -- In / Out --> state2
Fuer einen Uebergang von Zustand state1 nach Zustand state2,
falls das aktuelle Eingabesymbol In ist.
Als Ausgabe finden wir Out.
Ein Mealy Automate liefert einen Trace von Ausgabesymbolen.
Wir benutzen zur Vereinfachung eine Zustandsvariable.
-- / cnt=0 --> 1
1 -- WS --> 1
1 -- EOS --> 2
1 -- CH / cnt++ --> 3
3 -- WS --> 1
3 -- CH --> 3
3 -- EOS --> 2
*/
// Implementierung 1
char* scanWS(char* s) {
while(*s == ' ') {
s++;
}
return s;
}
int isCH(char c) {
return (c != ' ' && c != '\0');
}
char* scanCH(char* s) {
while(isCH(*s)) {
s++;
}
return s;
}
int countWords(char* s) {
int cnt = 0;
s = scanWS(s);
while(isCH(*s)) {
cnt++;
s = scanCH(s);
s = scanWS(s);
}
return cnt;
}
// Implementierung 2
// Direkte Umsetzung des Modells.
int countWordsModell(char* s) {
int state = 1;
int cnt = 0;
while(1) {
switch(state) {
case 1:
if(*s == ' ')
state = 1;
if(*s == '\0')
state = 2;
if(isCH(*s)) {
cnt++;
state = 3;
}
break;
case 2:
return cnt;
case 3:
if(*s == ' ')
state = 1;
if(*s == '\0')
state = 2;
if(isCH(*s))
state = 3;
break;
} // switch
s++; // next symbol
} // while
}
int main() {
// Write your own tests and use cases :)
return 1;
}
Speicherveraltung (malloc/free) am Beispiel von strings.
printf
ist Teil der stdio
Bibliothek.
malloc
und free
sind Teil der
stdlib
Bibliothek.
char* initString(int n) {
char* p = malloc((n+1) * sizeof(char));
char* q = p;
int i;
for(i = 0; i < n; i++, q++) {
*q = ' ';
}
*q = '\0';
return p;
}
Die initString
Funktion liefert einen String mit n
Leerzeichen.
Dazu werden n+1 Zeichen (char) mit Hilfe von `malloc’ allokiert.
Die ersten n Stellen im String werden mit Leerzeichen beschrieben.
Wichtig ist die Nullterminierung (zur Kennzeichnung des Ende des Strings) mittels des Ascii-codes 0.
Wir berechnen die Länge eines Strings. Eine solche Funktion ist natürlich in der C Standard Bibliothek schon vorhanden. Zur Übung schreiben wir unsere eigene `len’ Funktion.
char* duplicateChars(char* s) {
int i = len(s);
char* t = initString(2*i);
char* p = t;
char* r = s;
while(*s != '\0') {
*p = *s;
p++;
*p = *s;
p++;
s++;
}
free(r);
return t;
}
Die duplicateChars
Funktion dupliziert jedes Zeichen in
einem String.
Dazu wird ein String entsprechender Größe angelegt.
Wir iterieren über den bestehenden String und kopieren jedes Zeichen zweimal.
Beachte. Der bestehende String wird gelöscht. D.h. der von dem String
belegte Speicher wird freigegeben. Siehe den Aufruf
free(r)
.
Es folgt ein Anwendungsbeispiel.
int main() {
char* s;
char* t;
s = initString(3);
s[0] = '1';
s[1] = '2';
s[2] = '3';
printf("\n %s", s);
t = duplicateChars(s);
printf("\n %s", t);
return 1;
}
Der String “123” wird anglegt. Dann werden die Zeichen verdoppelt.
Die zwei printf
Ausgaben liefern.
123
112233
Nach dem Aufruf duplicateChars(s)
kann nicht mehr auf
den String “123” referenziert via dem Zeiger s
zugegriffen
werden, da dieser Speicher freigegeben wurde (innerhalb von
duplicateChars
).
Der Speicher des String “112233” referenziert via dem Zeiger
t
wird nach Abschluss der main
Routine nicht
freigegeben (es fehlt free(t)
). Wir haben dadurch ein
Speicherleck.
#include <stdio.h>
#include <stdlib.h>
char* initString(int n) {
char* p = malloc((n+1) * sizeof(char));
char* q = p;
int i;
for(i = 0; i < n; i++, q++) {
*q = ' ';
}
*q = '\0';
return p;
}
int len(char* s) {
int i = 0;
while(*s != '\0') {
s++;
i++;
}
return i;
}
char* duplicateChars(char* s) {
int i = len(s);
char* t = initString(2*i);
char* p = t;
char* r = s;
while(*s != '\0') {
*p = *s;
p++;
*p = *s;
p++;
s++;
}
free(r);
return t;
}
int main() {
char* s;
char* t;
s = initString(3);
s[0] = '1';
s[1] = '2';
s[2] = '3';
printf("\n %s", s);
t = duplicateChars(s);
printf("\n %s", t);
return 1;
}
Explizite Speicherverwaltung und Pointer sind ``trickreiche’’ C Themen Anhand eines erweiterten Beispiels sollen diesen Themen geübt werden. Als Beispiel betrachten wir Fibonacci.
Der komplette erarbeitete Source Code befindet sich im Anhang. Fragen und erweiterte Aufgaben befinden sich im Haupttext.
In C kann die Fibonacci wie folgt ausgedrückt werden.
Was ergibt der Aufruf fib(3)
?
Die Funktionsaufrufabfolge kann durch einen Stack simuliert werden. Das Laufzeitsystem benutzt einen Run-Time Stack zur Verwaltung von Funktionsaufrufen Dieser Run-Time Stack kann mittels Debugger beobachtet werden
Aufgabe: Setzen Sie geignete break points
und verfolgen
Sie die Berechung von fib(3)
.
Der Speicherbereich bei der Ausführung des Programmes ist wie folgt aufgeteilt.
CODE
STATIC DATA
STACK
...
HEAP
Folgende Variante der Fibonacci Implementierung, erlaubt es uns zu beobachten an welchem Ende des Speichers sich der Stack befindet.
int fib2(int n) {
int f1, f2;
int* nPtr;
printf("fib2(%d)\n",n);
nPtr = &n;
printf("&n = %ld \n",(long int)nPtr);
if (n<=1) return 1;
f1=fib2(n-1);
f2=fib2(n-2);
return f1+f2;
}
&n
liefert Adresse der Variable n
int
Wert.long int
Wert
gecastet fib(4) -> fib(3) -> fib(2) -> fib(1)
-> fib(0)
-> fib(1)
-> fib(2) -> fib(1)
-> fib(0)
fib(2)
Funktion fib
ist ein Beispiel einer reinen
Funktion (auch Referenz transparent genannt, auf English
pure/referentially transparent function). Eine reine Funktion
liefert für jeden Aufruf bei gleicher Eingabe das gleiche Ergebnis. Im
allgemeinen gilt dies für Funktionen in C nicht. Betrachte eine
Funktion, deren Berechnung von einer globalen oder statischen Variablen
abhängt.
enum Status {
Full=0,
Empty=1
};
struct Fib {
int fib;
enum Status status;
};
struct Fib fibArr[100];
Das Fibonacci Array dient zur Speicherung der ersten 100 Werte von Fibonacci.
Der Zugriff darauf geschieht via zweier Operationen:
Es folgt eine Implementierungsskizze beider Operationnen.
fib(n) = x
:fib(n)
:Aufgabe: Was fehlt in der Implementierungsskizze? (Hinweis: Die Länge des Fibonacci Arrays ist begrenzt)
Wir wiederholen die Definition des Fibonacci Arrays und fügen eine Typdefinition hinzu.
enum Status {
Full=0,
Empty=1
};
struct Fib {
int fib;
enum Status status;
};
struct Fib fibArr[100];
typedef struct Fib* FibPtr;
Der Typ FibPtr
beschreibt einen Zeiger auf die Struktur
Fib
. Beachte:
fibArr
auf das erste Element in dem Array
von 100 Element vom Typ der Struktur Fib
.fibArr
ein Pointer vom Typ
FibPtr
.Als nächstes gehen wir die Optimierung von Fibonacci mit Hilfe des Fibonacci Arrays an.
Zuerst definieren wir eine Hilfsfunktion, die es uns erlaubt die einzelnen Elemente des Fibonacci Arrays auszugeben. Der erste Parameter ist ein Zeiger auf das erste Element und der zweite Parameter beschreibt die Länge des Arrays. Aktuell gehen wir von 100 Elementen aus, das kann sich ja aber auch ändern.
void printFibArray(FibPtr f, int len) {
int i;
for(i=0; i<len; i++) {
printf("fib[%d]=",i);
if(f[i].status == Empty) {
printf("Empty\n");
}
else {
printf("%d\n",f[i].fib);
}
}
}
Aufgabe: Durch falsche Anwendung ist der Parameter f
möglicherweise gleich dem NULL Zeiger. Füge Sie eine Abfrage ein, die
einen Zugriff auf NULL ausschliesst.
Wir allokieren ein Fibonacci Array von variabler Länge.
FibPtr allocFibArray(int len) {
FibPtr f, f2;
int i;
f = malloc(sizeof(struct Fib) * len);
f2 = f;
for(i = 0, f2 = f; i < len; i++,f2++) {
f2->status = Empty;
}
return f;
};
// Anstatt return, Uebergebe pointer
void allocFibArray2(int len, FibPtr* f) {
*f = allocFibArray(len);
}
In der ersten Variante wir das allokierte Fibonacci Array als Rückgabewert geliefert. Genau genau erhalten wir als Rückgabewert einen Zeiger auf das erste Element.
Alternativ kann als Argument ein Speicherbereich für den Rückgabewert bereitgestellt werden.
In der zweiten Variante, erhalten wir als Argument einen Zeiger
auf FibPtr
. Der Wert auf den der Zeiger zeigt wird gleich
dem Zeiger auf das erste Element des Fibonacci Arrays gesetzt.
Beide Varianten sind gleichwertig. Es ist eine Frage des Geschmacks/Stils, ob man Rückgabewerte verwendet oder diese via Zeiger als Argument übergibt.
Aufgaben:
In obiger Implementierung gehen wir davon aus, dass immer
genügend Speicher vorhanden ist. Sprich der malloc
Aufruf
liefert uns den angeforderten Speicher.
allocFibArray
die
Speichermangel abfängt.Wir gehen davon aus, dass ein Fiboncacci Array fester Größe gegeben ist. Zur Strukturierung verwenden wir eine Struktur, die die Arraylänge und den Zeiger auf das erste Arrayelement enthält.
Es folgt der Source Code von Fibonacci mit dynamischer Programmierung. Erklärungen finden sich im Anschluss.
int fib3(struct FibArray fa, int n){
int f1, f2, fib;
if (n <= 0) return 1;
// Lookup
if(n < fa.len) {
if(fa.f[n].status == Full) {
return fa.f[n].fib;
}
}
// Berechne fib
if (n<=1) {
fib = 1;
}
else {
f1=fib3(fa,n-1);
f2=fib3(fa,n-2);
fib = f1 + f2;
}
// Speichere fib Wert
if(n < fa.len) {
if(fa.f[n].status == Empty) { // Redundant?
fa.f[n].status = Full;
fa.f[n].fib = fib;
}
}
return fib;
}
Lookup falls schon berechet und Index innerhalb der gegebenen Arraygrenze lieft.
Berechne fib, falls
Speichere Wert, falls Index innerhalb Grenzen liegt.
Aufgabe: Erklären Sie wieso die Redundant
markiert
Stelle überflüssig ist. (Erklärung findet sich im Anhang)
Aufgabe: Implementieren Sie eine Variante, die eine dynamische Anpassung des Fibonacci Arrays erlaubt.
fa
wird nun als Pointer übergeben, so können wir von
aussen Längenanpassungen sehen.
Eine Implementierung von fib4
findet sich im Anhang.
Wir haben den Umgang mit Arrays, Pointer und manueller Speicherverwaltung am Beispiel von Fibonacci und dynamischer Programmierung geübt.
#include "stdio.h"
#include "stdlib.h"
// Standard Fibonacci
int fib(int n){
int f1, f2;
printf("fib(%d)\n",n);
if (n<=1) return 1;
f1=fib(n-1);
f2=fib(n-2);
return f1+f2;
}
// Standard Fibonacci
// and print n's memory location
int fib2(int n) {
int f1, f2;
int* nPtr;
printf("fib2(%d)\n",n);
nPtr = &n;
printf("&n = %ld \n",(long int)nPtr);
if (n<=1) return 1;
f1=fib2(n-1);
f2=fib2(n-2);
return f1+f2;
}
enum Status {
Full=0,
Empty=1
};
struct Fib {
int fib;
enum Status status;
};
struct Fib fibArr[100];
typedef struct Fib* FibPtr;
void printFibArray(FibPtr f, int len) {
int i;
for(i=0; i<len; i++) {
printf("fib[%d]=",i);
if(f[i].status == Empty) {
printf("Empty\n");
}
else {
printf("%d\n",f[i].fib);
}
}
}
FibPtr allocFibArray(int len) {
FibPtr f, f2;
int i;
f = malloc(sizeof(struct Fib) * len);
f2 = f;
for(i = 0, f2 = f; i < len; i++,f2++) {
f2->status = Empty;
}
return f;
};
// Variante: Pointer auf zu allokierenden Speicherbereich
// wird als Argument uebergeben. Deshalb Uebergabe als
// 'Referenz' => pointer of pointer
void allocFibArray2(int len, FibPtr* f) {
*f = allocFibArray(len);
}
struct FibArray {
FibPtr f;
int len;
};
// Fibonacci und dynamische Programmierung
int fib3(struct FibArray fa, int n){
int f1, f2, fib;
if (n <= 0) return 1;
// Lookup falls schon berechnet
// und Index innerhalb Grenzen ist
if(n < fa.len) {
if(fa.f[n].status == Full) {
return fa.f[n].fib;
}
}
// Berechne fib, weil
// (a) erste Berechnung, oder
// (b) Index ausserhalb Grenzen ist
if (n<=1) {
fib = 1;
}
else {
f1=fib3(fa,n-1);
f2=fib3(fa,n-2);
fib = f1 + f2;
}
// Set fib Wert, falls Index innerhalb Grenzen ist
if(n < fa.len) {
if(fa.f[n].status == Empty) { // Redundant?
fa.f[n].status = Full;
fa.f[n].fib = fib;
}
}
return fib;
}
// Redundant, weil falls nicht Empty => Full,
// aber dann haetten wir ja schon einen lookup
// durchgefuehrt und hatten den gespeicherten fib Wert
// zurueckgegeben. Also, muss der status hier immer Empty sein.
// Fibonacci und dynamische Programmierung
// mit dynamischer Anpassung von fa.
// Pointer auf FibArray, so dass wir Laengenaenderung
// von 'aussen' beobachten koennen.
int fib4(struct FibArray* fa, int n){
int f1, f2, fib;
if (n <= 0) return 1;
if(n < fa->len) {
if(fa->f[n].status == Full) {
return fa->f[n].fib;
}
}
// Dynamische Anpassung durch Verdopplung
// (1) Allokieren neuen Speicherbereich
// (2) Kopiere 'alt' nach 'neu'
// (3) Gebe 'alt' frei
else {
FibPtr fPtr;
int i;
fPtr = allocFibArray(fa->len * 2); // (1)
for(i=0; i < fa->len; i++) // (2)
fPtr[i] = fa->f[i];
free(fa->f); // (3)
fa->f = fPtr;
fa->len = fa->len * 2;
}
if (n<=1) {
fib = 1;
}
else {
f1=fib4(fa,n-1);
f2=fib4(fa,n-2);
fib = f1 + f2;
}
if(n < fa->len) {
if(fa->f[n].status == Empty) { // Immer noch redundant?
fa->f[n].status = Full;
fa->f[n].fib = fib;
}
}
return fib;
}
// if(fa->f[n].status == Empty) ist redundant
// aber NICHT if(n < fa->len),
// wir haben keine Garantie dass der erweiterte Bereich
// gross genug ist!
// Mittels defines Auswahl der jeweiligen Version
// #define STANDARD_FIB
// #define FIB_MEM_LOC
// #define FIB_DYN
#define FIB_DYN_DYN
int main() {
#ifdef STANDARD_FIB
printf("fib(%d) = %d \n", 4,fib(4));
#endif
#ifdef FIB_MEM_LOC
printf("fib2(%d) = %d \n", 4,fib2(4));
#endif
#ifdef FIB_DYN
FibPtr f;
struct FibArray fa;
int len;
len = 9;
allocFibArray2(len,&f);
printFibArray(f,len);
fa.len = len;
fa.f = f;
fib3(fa,len+1);
printFibArray(fa.f,fa.len);
#endif
#ifdef FIB_DYN_DYN
FibPtr f;
struct FibArray fa;
int len;
len = 9;
allocFibArray2(len,&f);
printFibArray(f,len);
fa.len = len;
fa.f = f;
fib4(&fa,len*3);
printFibArray(fa.f,fa.len);
#endif
return 1;
}
Wir betrachten eine Verallgemeinerung unseres Testrahmens.
Zuerst kapseln den allgemeinen Testfall in einer abstrakten Klasse.
Die Testausführung kann dann generisch beschrieben werden.
void runTests(int no, TestCase* test[]) {
TestResult t;
int i;
for(i=0; i < no; i++) {
cout << "Test " << i << " ";
t = test[i]->execute();
if(OK == t)
cout << "OK\n";
if(FAIL == t)
cout << "FAIL\n";
}
}
Beachte: Virtuelle Methoden verlangen einen dynamischen Zeigertyp.
Wir können uns dann eine spezielle Testinstanz als Erweiterung der abstrakten Testklasse bauen.
class TestCount : public TestCase {
char* input;
int expected;
public:
TestCount(char* input_, int expected_) {
input = input_; expected = expected_; }
TestResult execute() {
if(expected == count(input))
return OK;
return FAIL;
}
};
Beachte: Wir bekommen die Warnung
warning: deprecated conversion from string constant to ‘char*’
Korrekterweise sollten wir entweder string
benützen oder
casten. Beides wurde hier der Einfachheithalber nicht gemacht.
#include <iostream>
using namespace std;
// Generischer Testrahmen
typedef enum {
OK,
FAIL
} TestResult;
class TestCase {
public:
virtual TestResult execute() = 0;
};
void runTests(int no, TestCase* test[]) {
TestResult t;
int i;
for(i=0; i < no; i++) {
cout << "Test " << i << " ";
t = test[i]->execute();
if(OK == t)
cout << "OK\n";
if(FAIL == t)
cout << "FAIL\n";
}
}
// Count Implementierung
char* leerzeichen(char* input) {
while(*input == ' ')
input++;
return input;
}
char* zeichen(char* input) {
while(*input != '\0' && *input != ' ')
input++;
return input;
}
int count(char* input) {
int cnt = 0;
while(*input != '\0') {
input = leerzeichen(input);
if(*input != '\0') {
cnt++;
input = zeichen(input);
}
}
return cnt;
}
// Count Test Erweiterung
class TestCount : public TestCase {
char* input;
int expected;
public:
TestCount(char* input_, int expected_) {
input = input_; expected = expected_; }
TestResult execute() {
if(expected == count(input))
return OK;
return FAIL;
}
};
int main() {
// Count Testfaelle
const int testNo = 9;
TestCase* tests[9] = {
new TestCount("",0),
new TestCount("Hallo", 1),
new TestCount(" Hallo", 1),
new TestCount("Hallo", 1),
new TestCount(" Hallo ", 1),
new TestCount("Hal lo", 2),
new TestCount(" Hal lo", 2),
new TestCount("Hal lo ", 2),
new TestCount(" Hal lo ", 2)
};
runTests(testNo,tests);
}
Diese Aufgabe besteht aus folgenden Teilen:
Vereinfachung regulärer Ausdrücke
Transformation Regulärer Ausdrücke in endliche Automaten
Ausführung des endlichen Automaten
Teile der Aufgabe sind schon vorgegeben. Siehe Files
RE.h
und FSA.h
. Ihre Aufgabe ist es, die
fehlenden Teile zu vervollständigen. Die Grundgerüste der einzelnen
Teilaufgaben finden Sie im ilias.
In der theoretischen Informatik haben sie schon reguläre Ausdrücke
kennengelernt. Mittels regulärer Ausdrücke lassen sich reguläre Sprache
beschreiben, wobei eine Sprache aus einer Menge von Wörtern besteht.
Jedes Wort besteht aus einer endlichen Anzahl von Zeichen. In unserem
Fall stellen wir Zeichen als char
dar. Deshalb sind Wörter
nichts anderes als Strings.
Wir betrachten folgende syntaktischen Konstrukte:
eps
– “Epsilon” der leere Stringphi
– “Phi” die leere Sprachec
– das Zeichen c
r1 + r2
– Alternative zwischen r1
und
r2
r1 | r2
r1 r2
– Verkettung/Konkatenation von r1
mit r2
r*
– Kleenesche Hülle, entweder leerer String oder
beliebige Verkettung von r
Die Sprache L(r)
beschrieben durch den regulären
Ausdruck r
ist definiert wie folgt:
L(phi) = {}
L(eps) = { eps }
L(c) = { c }
L(r1 + r2) = { w | w in L(r1) oder w in L(r2) }
L(r1 r2) = { w1 w2 | w1 in L(r1) und w2 in L(r2) }
L(r*) = { w | w = eps oder w=w1 ... wn jedes wi in L(r) }
Im ersten Schritt bilden wir die Syntax von regulären Ausdrücken nach C++ ab. Dazu verwenden wir Vererbung.
class RE {};
class Phi : public RE {};
class Eps : public RE {};
class Ch : public RE {
private:
char c;
public:
Ch (char _c) { c = _c; }
};
class Alt : public RE {
private:
RE* r1;
RE* r2;
public:
Alt (RE* _r1, RE* _r2) { r1 = _r1; r2 = _r2; }
};
class Conc : public RE {
private:
RE* r1;
RE* r2;
public:
Conc (RE* _r1, RE* _r2) { r1 = _r1; r2 = _r2; }
};
class Star : public RE {
private:
RE* r;
public:
Star (RE* _r) { r = _r; }
};
Z.B. die abgeleitete Klasse Alt
repräsentiert die
Alternativen zwischen zwei regulären Ausdrücken. Die Alternativen werden
als private Instanzvariablen r1
und r2
gespeichert. Beides sind Zeiger weil, wie wir noch sehen werden, wir
virtuelle Methoden auf der Basisklassen und den Ableitungen definieren
werden. Der Alt
Konstruktor bekommt als Argument zwei
reguläre Ausdrücke in C++ Repräsentation und initialisiert die
Instanzvariablen.
class RE {
public:
virtual REType ofType()=0;
virtual string pretty()=0;
virtual bool containsEps()=0;
};
ofType
liefert einen enum Wert, der die Art des
regulären Ausdrucks kennzeichnet. pretty
ist eine “pretty
print” Methode, die den regulären Ausdruck in einen String übersetzt.
containsEps
liefert true
falls in der Sprache,
die vom dem reguläre Ausdruck beschrieben wird, sich der leere String
befindet.
Obige virtuelle Methoden sind schon für die abgeleiteten Klassen
definiert. Zur Lösung der Aufgaben dürfen Sie weitere (virtuelle)
Methoden zur Klasse RE
hinzufügen.
Als Beispiel der Definition der virtuellen Methoden, betrachten wir
die abgeleitete Klasse Alt
.
enum REType {
PhiType, EpsType, ChType,
AltType, ConcType, StarType };
class Alt : public RE {
private:
RE* r1;
RE* r2;
public:
Alt (RE* _r1, RE* _r2) { r1 = _r1; r2 = _r2; }
REType ofType() { return AltType; }
string pretty() {
string s("(");
s.append(r1->pretty());
s.append("+");
s.append(r2->pretty());
s.append(")");
return s;
}
bool containsEps() {
return (r1->containsEps() || r2->containsEps());
}
};
Die Syntax von regulären Ausdrücken ist fest vorgegeben. Sprich für
die Klasse RE
gibt es keine weiteren Ableitungen, ausser
den oben beschriebenen. Deshalb benutzen wir einen enum, wobei die Tags
des enums jede Ableitung der Klasse RE
beschreiben. Z.B.
AltType
beschreibt Alt
, ConcType
beschreibt Conc
usw. Deshalb liefert ofType
für die Ableitung Alt
den Wert AltType
.
Im Falle von pretty
wird ersichtlich, wieso wir
virtuelle Methoden verwenden. pretty
wird für jede
Alternative aufgerufen. Zur Laufzeit wir dann basierend auf der
konkreten Instanz der Alternativen, die geeignete pretty
Definition ausgewählt.
Die resultierenden Strings werden dann mit “+” verbunden. Beachte: Mittels Klammern “(” und “)” wird eine eindeutige Darstellunge garantiert. Z.B.
liefert (c(a+b))
. Ohne die Klammern erhalten wir
ca+b
was nicht eindeutig ist. Natürlich könnten wir eine
Präferenz zwischen den Alternativ-, Verkettungs- und Staroperator
definieren, um die Anzahl der Klammern zu reduzieren (freiwillige
Aufgabe).
Im Falle einer Alternative, ist der leere String enthalten, falls
eine der beiden Alternativen den leeren String enthält. Die Methode
containsEps
setzt diese Vorgabe literal um.
Betrachte folgenden regulären Ausdruck.
eps ((a*)* (phi + b))
Obiger Ausdruck kann vereinfacht werden in die Form
(a*) b
Beide Ausdrücke sind äquivalent. Der letztere Ausdruck ist aber in einfacher und verständlicherer Form.
Ziel ist einen regulärern Ausdruck soweit wie möglich zu vereinfachen. Dazu verwenden wir Vereinfachungsregeln formuliert in der Form
linkeSeite ==> rechteSeite
eps r ==> r
r1 r2 ==> phi
falls L(r1)={}
oder
L(r2)={}
r* ==> eps
falls L(r)={}
(r*)* ==> r*
r + r ==> r
r1 + r2 ==> r2
falls
L(r1)={}
r1 + r2 ==> r1
falls
L(r2)={}
Für jede der obigen Regeln gilt
L(linkeSeite) = L(rechteSeite)
.
Angewandt auf unser Beispiel.
eps ((a*)* (phi + b))
==>1 (a*)* (phi + b)
==>4 a* (phi + b)
==>6 a* b
Wir erweitern Klasse RE
wie folgt.
Methode isPhi
liefert true
falls der
Ausdruck die leere Sprache beschreibt und ist schon vordefiniert.
Methode simp
führt die Vereinfachungsregeln aus. Per
Default liefert simp
den ürsprünglichen Ausdruck. Ihre
Aufgabe ist es simp
in den abgeleiteten Klassen geeignet zu
erweitern.
Als Beispiel betrachte man Alt
und die Umsetzung der
Regeln 6 und 7.
RE* simp() {
// First, simplify subparts
r1 = r1->simp();
r2 = r2->simp();
// Then, check if any of the simplification rules are applicable
// 6. `r1 + r2 ==> r2` falls `L(r1)={}`
if(r1->isPhi()) return r2;
// 7. `r1 + r2 ==> r1` falls `L(r2)={}`
if(r2->isPhi()) return r1;
return this;
}
Etwas schwierger sind Regeln 3 und 4 im Falle von
Star
.
RE* simp() {
// Simplify subparts
r = r->simp();
// Then, check if any of the simplification rules are applicable
// 3. `r* ==> eps` falls `L(r)={}`
if(r->isPhi()) {
return new Eps();
}
// 4. `(r*)* ==> r*`
if(r->ofType() == StarType) {
return this->r;
}
return this;
}
Beachte: In obiger Beispielimplementierung ignorieren wir das Problem
der Speicherverwaltung. Z.B. im Falle von Regel (6)
r1 + r2 ==> r2
falls L(r1)={}
sollte jemand
de von r1
belegten Speicherbereich freigeben. In dieser
Aufgabe dürfen Sie das Aufräumen der von regulären Ausdrücken
Speicherplätze ignorieren. Als freiwillige Zusatzaufgabe überlegen Sie
sich ein geeignetes Verwaltungsschema welches garantiert, dass der
belegte Speicherplatz freigeben wird. Die Herausforderung hierbei ist zu
organisieren wer und wann den Speicherplatz freigeben soll.
Ihre eigentliche Aufgabe ist die Implementierung der noch fehlenden
obigen Vereinfachungsregeln. Für Regel (5) r + r ==> r
benötigen Sie eine Methode zum Test auf Gleichheit zwischen regulären
Ausdrücken.
Die einfachste Implementierung wendet die pretty
Funktion an und vergleicht dann die beiden daraus resultierenden
Strings.
Alternativ könnten wir rekursiv über den Strukturaufbau der regulären Ausdrücke laufen und die einzelnen Komponenten vergleichen.
bool equals(RE* r1, RE* r2) {
bool b;
if(r1->ofType() != r2->ofType())
return false;
switch(r1->ofType()) {
case PhiType: b = true;
break;
case EpsType: b = true;
break;
case ChType: {
Ch* c1 = (Ch*)r1;
Ch* c2 = (Ch*) r2;
b = c1->getChar() == c2->getChar();
break;
}
case StarType: {
Star* r3 = (Star*) r1;
Star* r4 = (Star*) r2;
b = equals(r3->getRE(),r4->getRE());
break;
}
case AltType: {
Alt* r3 = (Alt*) r1;
Alt* r4 = (Alt*) r2;
b = equals(r3->getLeft(),r4->getLeft()) &&
equals(r3->getRight(), r4->getRight());
break;
}
case ConcType: {
Conc* r3 = (Conc*) r1;
Conc* r4 = (Conc*) r2;
b = equals(r3->getLeft(),r4->getLeft()) &&
equals(r3->getRight(), r4->getRight());
break;
}
}// switch
return b;
} // equals
Zusammengefasst. Erweiteren Sie RE.h
mit den noch
fehlenden Vereinfachungsregeln. Als eine mögliche Testroutine können Sie
folgendes Gerüst verwenden.
// Main fuer Teilaufgabe 1
// Bitte weitere Testfaelle hinzufuegen.
#include "RE.h"
#include <iostream>
using namespace std;
int main() {
// phi + c
RE* r3 = new Alt (new Phi(), new Ch('c'));
// c + phi
RE* r4 = new Alt (new Ch('c'), new Phi());
cout << r3->pretty() << endl;
cout << r3->simp()->pretty() << endl;
// c**
RE* r5 = new Star(new Star (new Ch('c')));
cout << r5->pretty() << endl;
cout << r5->simp()->pretty() << endl;
// phi*
RE* r6 = new Star(new Phi());
cout << r6->pretty() << endl;
cout << r6->simp()->pretty() << endl;
// (phi + c) + c**
RE* r7 = new Alt(r3,r5);
cout << r7->pretty() << endl;
// exhaustively apply simplifications
cout << simpFix(r7)->pretty() << endl;
}
Die nächste Aufgabe ist die Transformation von regulären Ausdrücken in Automaten. Ihre Aufgabe ist die Implementierung eines Transformationsalgorithmus der einen regulären Ausdruck in einen Automaten umwandelt.
Zuerst ein kurze Wiederhohlung der grundlegenden Konzepte endlicher Automaten.
Ziel ist die Überprüfung, ob eine Eingabewort Teil der vom Automaten akzeptierten Sprache ist. Dazu werden nacheinander die einzelnen Zeichen des Eingabewortes betrachtet. Je nach aktuellem Zustand und aktuellem Zeichen, ändert sich der Zustand des Automaten (Beachte: Wir betrachten hier auch spontante Zustandsübergange, auch epsilon Transitionen genannt). Falls sämtliche Zeichen des Eingabewortes betrachtet, sprich konsumiert wurden, und der Automat befindet sich in einem Finalzustand, dann ist das Eingabewort Teil der vom Automaten akzeptierten Sprache.
Formal ist ein endlicher Automat beschrieben duch
Beispiel:
Sigma = {a,b,c}
Q = {1,2,3,4,5}
Initialzustand = 1
Finalzustände = {4,5}
Transitionen:
1 -----> 2
1 --a--> 3
1 --a--> 5
2 --a--> 2
2 --b--> 2
2 --a--> 3
3 --c--> 4
5 --b--> 4
Für Eingabewort “ab” betrachten wir die Ausführung des Automaten.
Wir starten im Zustand 1. Unser Automat ist nichtdeterministisch, deshalb kann der Automat zu jeden Zeitpunkt in einer Menge von “aktiven” Zuständen befinden.
Bei der Abarbeitung des Eingabewortes gibt es zwei prinzipielle Aktionen.
Verfolge alle spontanen Übergange, d.h. wir bilden die Hülle (englisch closure) aller spontanten Übergänge.
Konsumiere aktuelles Zeichen des Eingabeworts
1. current = { 1 }
Als erstes betrachten wir alle spontanen (epsilon) Übergange
die von 1 ausgehen.
Daraus folgt current = {1, 2}
wegen 1 -----> 2
Aktuelles Eingabezeichen ist a, die erste Position in "ab".
Daraus folgt current = {2,3,5} wegen
1 --a--> 3
1 --a--> 5
2 --a--> 3
2 --a--> 2
2. current = {2,3,5}
Hülle der spontanen Übergänge ist hier gleich current
Aktuelles Eingabezeichen ist b, die zweite Position in "ab".
Daraus folgt current = {2,4} wegen
2 --b--> 2
5 --b--> 4
Da 4 ein Finalzustand ist, gehört "ab" zur der vom
dem Automaten akzeptierten Sprache.
Endliche Automaten haben eine sehr einfache operationelle Beschreibung als eine Sequenz von Transitionsübergängen. In vielen Situationen sind jedoch reguläre Ausdrücke der geeignetere Formalismus. Beide Formalismen sind gleichwertig, d.h. ihre Ausdrücksstärke ist gleichwertig. Sprich jeder reguläre Ausdruck kann in einen endlichen Automaten umgewandelt werden und umgekehrt. Hier betrachten wir nur die Richtung von regulären Ausdrücken nach endlichen Automaten. Im folgenden betrachten wir einen speziellen Algorithmus der einen regulären Ausdruck in einen Automaten umwandelt (transformiert).
Ein möglicher Algorithmus ist der Thompson NFA Algorithmus beschrieben hier:
http://en.wikipedia.org/wiki/Thompson’s_construction_algorithm
Gegeben sein ein regulärer Ausdruck. Der Algorithmus von Ken Thompson zeigt wie man aus dem regulärern Ausdruck einen Automaten konstruiert. Der Automat ist nichtdeterministischen weil mehrere Folgezustände nichtdeterministisch erreicht werden können. Deshalb NFA, was auf Englisch heisst “non-deterministic finite automata”.
Sie können natürlich sonst einen Algorithmus verwenden. Wichtig ist, dass der konstruierte Automat die gleiche Sprache akzeptiert, wie der reguläre Ausdruck. Zum Testen, ob Ihre Transformation diese Kriterium erfüllt wird Ihnen eine Testrahmen zur Verfügung gestellt (siehe unten).
Folgende Klassen sind gegeben. Bauen Sie Ihre Lösung auf Grundlage dieser Klassen auf.
Transition
beschreibt eine Transition innerhalb eines
Automaten. NFA
beschreibt eine nicht-deterministischen
Automaten, der aus einer Menge von Transitionen, einem Startzustand und
einer Menge von Finalzuständen besteht. FSA
ermöglicht die
Ausführung eines NFA und wird erst später relevant.
Zuerst betrachen wir die Klasse Transition
.
class Transition {
private:
int from;
char c;
int to;
bool epsilon;
public:
Transition(int _from, int _to) {
from = _from; to = _to;
epsilon = true;
}
Transition(int _from, char _c, int _to) {
from = _from; c = _c; to = _to;
epsilon = false;
}
bool isEpsilonTransition() { return epsilon; }
int toState() { return to; }
bool trigger(int from, char c) {
return (!epsilon && from == this->from && c == this->c);
}
bool trigger(int from) {
return (epsilon && from == this->from);
}
};
Zustände werden als Integer Werte repräsentiert. Eine Transition kann zwei Formen haben.
from --- c ---> to
from ---------> to
Der Übergang von from
nach to
findet statt,
falls die aktuelle Eingabe mit dem Zeichen c
übereinstimmt,
oder wir können spontan von from
nach to
übergehen. Letzer Fall ist eine sogeannte “epsilon” Transition. Die
überladenen Konstruktoren von Transition
erlauben die
Konstruktion von Transitionen beider Arten.
Sind epsilon Transitionen notwendig? Nein. Jeder Automat mit epsilon Transitionen kann in einen Automaten ohne epsilon Transitionen. Jedoch sind epsilon Transitionen sehr hilfreich um nicht-deterministische Vorgänge einfach zu beschreiben. Siehe Aufgabe Übersetzung von regulären Ausdrücken nach Automaten.
Methode isEpsilonTransition
unterscheidet zwischen
beiden Arten. Überladene Methode trigger
testet ob ein
Übergang möglich ist. Methode toState
liefert den
Folgezustand.
Nun zu der NFA
Klasse.
class NFA {
private:
vector<Transition> ts;
int init;
vector<int> final;
public:
NFA(vector<Transition> _ts, int _init, vector<int> _final) {
ts = _ts;
init = _init;
final = _final;
}
NFA(vector<Transition> _ts, int _init, int _final) {
ts = _ts;
init = _init;
final.push_back(_final);
}
vector<Transition> getTransitions() { return ts; }
int getInitial() { return init; }
vector<int> getFinals() { return final; }
friend class FSA;
};
Ein NFA
besteht aus einer Menge von Transitionen, einem
Startzustand und einer Menge von Finalzuständen. Wir benutzen STL
vector
zur Speicherung von Transitionen und Finalzuständen.
Der zweite Konstruktor erleichert die Erstellung eines NFAs mit genau
einem Start und Endzustand.
Ihre Aufgabe ist die Implementierung einer Transformationsfunktion von regulären Automaten (RE) nach nicht-deterministischen Automaten (NFA).
Es gibt mindestens zwei mögliche Ansätze.
RE
RE.h
muss dann FSA.h
importierenWeitere Beispiele für beide Ansätze finden Sie unter Laboraufgaben (siehe Evaluator für arithmetische Ausdrücke).
Unten finden Sie ein Gerüst fuer den Ansatz “Eigenständige Funktion”.
Die beiden Fälle Phi
und Alt
sind schon
implementiert.
#include "FSA.h"
#include "RE.h"
int nameSupply;
void init() {
nameSupply = 0;
}
int fresh() {
return nameSupply++;
}
// Macht die eigentliche Arbeit
NFA transformWorker(RE *r);
// Schnittstelle fuer Benutzer
// Ruecksetzen des "name supplies" zur Generierung von eindeutigen Zustaenden
// Aufruf von transform2
NFA transform(RE *r) {
init();
return transformWorker(r);
}
// Wir liefern einen NFA zurueck mit genau einem Start und
// genau einem Stop(end)zustand.
NFA transformWorker(RE *r) {
vector<Transition> ts;
int start, stop;
switch(r->ofType()) {
case EpsType: // TODO
case ChType: // TODO
case StarType: // TODO
case ConcType: // TODO
// Phi: Akzeptiert kein Wort
// NFA besteht aus einem Start und Stopzustand und *keiner* Transition
case PhiType:
start = fresh();
stop = fresh();
return NFA(ts, start, stop);
case AltType: {
Alt* r2 = (Alt*) r;
// 1. Baue NFAs der linken und rechten Alternative
NFA n1 = transformWorker(r2->getLeft());
NFA n2 = transformWorker(r2->getRight());
// 2. Generieren neuen Start-/Stopzustand.
// Sammle Start-/Stopzustaende von n1 und n2
// N.B. Annahme: finals besteht aus genau einem Zustand
start = fresh();
stop = fresh();
int n1_start = n1.getInitial();
int n1_stop = n1.getFinals()[0];
int n2_start = n2.getInitial();
int n2_stop = n2.getFinals()[0];
// 3. Sammle Transitionen auf von n1 und n2.
// Verbinde neuen Start-/Endzustand mit alten Start-/Endzustaenden.
vector<Transition> t1 = n1.getTransitions();
vector<Transition> t2 = n2.getTransitions();
ts.insert(ts.end(),t1.begin(),t1.end());
ts.insert(ts.end(),t2.begin(),t2.end());
ts.push_back(Transition(start, n1_start));
ts.push_back(Transition(start, n2_start));
ts.push_back(Transition(n1_stop, stop));
ts.push_back(Transition(n2_stop, stop));
return NFA(ts,start,stop);
}
} // switch
} // transformWorker
Eine weitere Aufgabe ist die Ausführung des aus dem regulären
Ausdrucks konstrukierten NFA. Der Rahmen zur Ausführung wird durch die
Klasse FSA
definiert.
class FSA : public NFA {
private:
vector<int> current;
void closure();
public:
FSA(NFA fsa) : NFA(fsa.ts,fsa.init,fsa.final) {
current.push_back(init);
closure();
}
void reset();
void step(char c);
bool isFinal();
bool run(string s);
};
Ein FSA nimmt einen NFA. Instanzvariable current
beschreibt die Menge der “aktiven” Zustände. Beachte wir gehen von einem
nicht-deterministischen Automaten aus. Im Falle eines deterministischen
Automaten, würde die Menge aus genau einem Zustand bestehen.
Am Anfang wird current
gleich dem Startzustand des NFA
gesetzt. Übergänge finden nur statt (sprich Transitionen werden
getriggert) bei Lesen eines Zeichens. Da der NFA “epsilon” Transitionen
enthält, müssen wir alle von current
aus erreichbaren
Zustände bilden die via einer oder mehrerer epsilon Transitionenen
erreichbar sind. Dies geschieht durch Aufruf der Methode
closure
.
closure
Ihre Aufgabe ist es Methode closure
zu implementieren.
Zuerst betrachten wir ein Beispiel mit folgenden drei Transitionen
(Zustandsübergängen).
s1 -----> s2
s2 --a--> s3
s2 -----> s4
Von Zustand s1
gibt es eine “epsilon” Transition nach
s2
. Von Zustand s2
ist ein Übergang in den
Zustand s3
möglich, falls die aktuelle Eingabe
a
ist. Der letzte Zustandsübergang ist wieder ein “epsilon”
Transition.
Annahme: current = {s1}
current = {s1, s2}
wegen folgender “epsilon”
Transition s1 -----> s2
.Aufgrund des neu hinzugefügten Zustandes s2
können
weitere Zustände via “epsilon” erreicht werden.
current = {s1, s2, s4}
wegen “epsilon”
Transition s2 -----> s4
.Keine weiteren Zustände können via “epsilon” erreicht werden. Die
closure
von {s1}
ist
{s1,s2,s4}
.
Beachte, Zustand s3
kann nur erreicht werden falls die
aktuelle Eingabe a
ist. Deshalb ist s3
nicht
in der closure
von {s1}
.
Hinweise zur Implementierung von closure
:
Für jeden Zustand s1
in current
und jede
“epsilon” Transition s2 -----> s3
, fügen wir
s3
zu current
hinzu falls folgende Bedingungen
gelten:
s3
ist nicht schon enthalten in
current
Dieser Vorgang wird solange ausgeführt, bis keine Zustände mehr zu
current
hinzugefügt werden können.
Im Detail:
isEpsilonTransition
der Klasse
Transition
können Sie alle epsilon Transitionen
herausfiltern.trigger
und toState
der Klasse Transition
können Sie Testen, ob ein Zustand
erreicht wird. Angenommen t
ist eine Transition und
q
ein Zustand (Integer), dann liefert der Ausdruck
t.isEpsilonTransition() && t.trigger(q)
wahr, falls
t
eine epsilon Transition ist und Transition
t als Startpunkt den Zustand
qhat. Via
t.toState(q)`
können Sie den Endpunkt der (epsilon) Transition berechnen.find
Method. Angenommen v
ist vom Typ
vector<int>
und q
ist ein Zustand, dann
liefert der Ausdruck find(v.begin(),v.end(),q) == v.end()
wahr, falls q
nicht in dem Vektor v
enthalten
ist.Methoden reset
, isFinal
, step
und run
sind schon gegeben.
Ein reset
bedeutet, wir brechen die Ausführung ab, und
starten neu:
Methoden isFinal
überprüft, ob wir einen Finalzustand
erreicht haben:
bool FSA::isFinal() {
for(int i=0; i < final.size(); i++) {
if(find(current.begin(),current.end(),final[i]) != current.end())
return true;
}
return false;
}
Methode step
nimmt als Eingabe das aktuelle Zeichen
c
. Basierend auf der Menge der “aktiven” Zustände in
current
, werden alle Folgezustände berechnet die mit der
Eingabe c
erreicht werden können. Diese Menge wird gleich
current
gesetzt. Es folgt das bilden der transitiven Hülle
der “epsilon” Transitionen mittels closure
.
void FSA::step(char c) {
vector<int> next;
for(int i=0; i < ts.size(); i++) {
for (int j=0; j < current.size(); j++) {
if(ts[i].trigger(current[j],c))
next.push_back(ts[i].toState());
}
}
current = next;
closure();
}
Methode run
überprüft ob der Eingabestring von dem
Automaten akzeptiert wird.
bool FSA::run(string s) {
reset();
for(int i=0; i < s.length(); i++) {
step(s[i]);
}
return isFinal();
}
Zum Testen der Teilaufgaben benutzen Sie bitte folgenden Testrahmen.
Annahme: Sie haben FSA.h
mit einer Implementierung der
closure
Funktion erweitert und Teilaufgbe 1 as
eingenständige Funktion folgender Form realisiert (falls als Methode,
müssen Sie einfach den transform Aufruf geeignet umschreiben).
Zur Überprüfung, ob ihre Implemenierung korrekt ist benötigen wir eine unabhängige Implementierung eines Automaten.
Die untenstehende Implementierung beruht auf Brzozowski’s Derivaten. Die Details sind unwichtig. Falls Sie’s interessiert, weitere Informationen finden Sie http://lambda-the-ultimate.org/node/2293.
RE* deriv(RE* r, char l) {
switch(r->ofType()) {
case PhiType:
return r;
case EpsType:
return new Phi();
case ChType:
if (((Ch*)r)->getChar() == l) {
return new Eps();
}
else {
return new Phi();
}
case StarType: {
RE* r1 = ((Star*) r)->getRE();
return new Conc(deriv(r1,l),r);
}
case AltType: {
RE* r1 = ((Alt*) r)->getLeft();
RE* r2 = ((Alt*) r)->getRight();
return new Alt(deriv(r1,l), deriv(r2,l));
}
case ConcType: {
RE* r1 = ((Conc*)r)->getLeft();
RE* r2 = ((Conc*)r)->getRight();
if(r1->containsEps()) {
return new Alt(new Conc(deriv(r1,l),r2), deriv(r2,l));
}
else {
return new Conc(deriv(r1,l),r2);
}
}
}// switch
}
bool match(RE* r, string s) {
for(int i=0; i < s.length(); i++) {
r = deriv(r, s[i]);
}
return r->containsEps();
}
Ein Testfall besteht aus einem regulären Ausdruck und einem Eingabestring. Testausführung ist wie folgt.
bool testExec(RE* r, string s) {
NFA nfa = transform(r);
FSA fsa(nfa);
bool b1 = fsa.run(s);
bool b2 = match(r,s);
return b1 == b2;
}
Unten finden Sie ein paar einfache Testfälle. Sie sollten natürlich Ihre Implementierung gegen weitere Fälle testen.
FSA.h
// File: FSA.h
// Endliche Automaten
#ifndef __FSA__
#define __FSA__
#include <vector>
#include <string>
#include <algorithm>
using namespace std;
class Transition;
class NFA;
class FSA;
class Transition {
private:
int from;
char c;
int to;
bool epsilon;
public:
Transition(int _from, int _to) {
from = _from; to = _to;
epsilon = true;
}
Transition(int _from, char _c, int _to) {
from = _from; c = _c; to = _to;
epsilon = false;
}
bool isEpsilonTransition() { return epsilon; }
int toState() { return to; }
bool trigger(int from, char c) {
return (!epsilon && from == this->from && c == this->c);
}
bool trigger(int from) {
return (epsilon && from == this->from);
}
};
class NFA {
private:
vector<Transition> ts;
int init;
vector<int> final;
public:
NFA(vector<Transition> _ts, int _init, vector<int> _final) {
ts = _ts;
init = _init;
final = _final;
}
NFA(vector<Transition> _ts, int _init, int _final) {
ts = _ts;
init = _init;
final.push_back(_final);
}
vector<Transition> getTransitions() { return ts; }
int getInitial() { return init; }
vector<int> getFinals() { return final; }
friend class FSA;
};
class FSA : public NFA {
private:
vector<int> current;
void closure();
public:
FSA(NFA fsa) : NFA(fsa.ts,fsa.init,fsa.final) {
current.push_back(init);
closure();
}
void reset();
void step(char c);
bool isFinal();
bool run(string s);
};
void FSA::reset() {
current.clear();
current.push_back(init);
closure();
}
bool FSA::isFinal() {
for(int i=0; i < final.size(); i++) {
if(find(current.begin(),current.end(),final[i]) != current.end())
return true;
}
return false;
}
void FSA::closure() {
// Ihre Aufgabe
}
void FSA::step(char c) {
vector<int> next;
for(int i=0; i < ts.size(); i++) {
for (int j=0; j < current.size(); j++) {
if(ts[i].trigger(current[j],c))
next.push_back(ts[i].toState());
}
}
current = next;
closure();
}
bool FSA::run(string s) {
reset();
for(int i=0; i < s.length(); i++) {
step(s[i]);
}
return isFinal();
}
#endif // __FSA__
RE.h
// Reguläre Ausdrücke
#ifndef __RE__
#define __RE__
#include <string>
#include <sstream>
using namespace std;
// Vorwaertzreferenz
class RE;
// Prototypen von Hilfsfunktionen
bool equals(RE* r1, RE* r2);
enum REType {
PhiType,
EpsType,
ChType,
AltType,
ConcType,
StarType };
// Basisklasse
class RE {
public:
virtual REType ofType()=0;
virtual string pretty()=0;
virtual bool containsEps()=0;
virtual bool isPhi()=0;
virtual RE* simp() { return this; }
};
// Abgeleitete Klassen
class Phi : public RE {
public:
REType ofType() { return PhiType; }
string pretty() { return "phi"; }
bool containsEps() { return false; }
bool isPhi() { return true; }
};
class Eps : public RE {
public:
REType ofType() { return EpsType; }
string pretty() { return "eps"; }
bool containsEps() { return true; }
bool isPhi() { return false; }
};
class Ch : public RE {
private:
char c;
public:
Ch (char _c) { c = _c; }
char getChar() { return c; }
REType ofType() { return ChType; }
string pretty() {
stringstream ss;
ss << c;
return ss.str();
}
bool containsEps() { return false; }
bool isPhi() { return false; }
};
class Alt : public RE {
private:
RE* r1;
RE* r2;
public:
Alt (RE* _r1, RE* _r2) { r1 = _r1; r2 = _r2; }
RE* getLeft() { return r1; }
RE* getRight() { return r2; }
REType ofType() { return AltType; }
string pretty() {
string s("(");
s.append(r1->pretty());
s.append("+");
s.append(r2->pretty());
s.append(")");
return s;
}
bool containsEps() {
return (r1->containsEps() || r2->containsEps());
}
bool isPhi() {
return (r1->isPhi() && r2->isPhi());
}
RE* simp() {
// First, simplify subparts
r1 = r1->simp();
r2 = r2->simp();
// Then, check if any of the simplification rules are applicable
// 6. `r1 + r2 ==> r2` falls `L(r1)={}`
if(r1->isPhi()) return r2;
// 7. `r1 + r2 ==> r1` falls `L(r2)={}`
if(r2->isPhi()) return r1;
return this;
// N.B. We're a bit relaxed when it comes to garbage collection.
// For example, in case of rule (6) we should clean up the
// memory space occupied by r1 which we ignore here.
}
};
class Conc : public RE {
private:
RE* r1;
RE* r2;
public:
Conc (RE* _r1, RE* _r2) { r1 = _r1; r2 = _r2; }
RE* getLeft() { return r1; }
RE* getRight() { return r2; }
REType ofType() { return ConcType; }
string pretty() {
string s("(");
s.append(r1->pretty());
s.append(r2->pretty());
s.append(")");
return s;
}
bool containsEps() {
return (r1->containsEps() && r2->containsEps());
}
bool isPhi() {
return (r1->isPhi() || r2->isPhi());
}
};
class Star : public RE {
private:
RE* r;
public:
Star (RE* _r) { r = _r; }
RE* getRE() { return r; }
REType ofType() { return StarType; }
string pretty() {
string s;
s.append(r->pretty());
s.append("*");
return s;
}
bool containsEps() {
return true;
}
bool isPhi() {
return false;
}
RE* simp() {
// Simplify subparts
r = r->simp();
// Then, check if any of the simplification rules are applicable
// 3. `r* ==> eps` falls `L(r)={}`
if(r->isPhi()) {
return new Eps();
}
// 4. `(r*)* ==> r*`
if(r->ofType() == StarType) {
return this->r;
}
return this;
}
};
// Structural comparison among regular expressions
bool equals(RE* r1, RE* r2) {
bool b;
if(r1->ofType() != r2->ofType())
return false;
switch(r1->ofType()) {
case PhiType: b = true;
break;
case EpsType: b = true;
break;
case ChType: {
Ch* c1 = (Ch*)r1;
Ch* c2 = (Ch*) r2;
b = c1->getChar() == c2->getChar();
break;
}
case StarType: {
Star* r3 = (Star*) r1;
Star* r4 = (Star*) r2;
b = equals(r3->getRE(),r4->getRE());
break;
}
case AltType: {
Alt* r3 = (Alt*) r1;
Alt* r4 = (Alt*) r2;
b = equals(r3->getLeft(),r4->getLeft()) &&
equals(r3->getRight(), r4->getRight());
break;
}
case ConcType: {
Conc* r3 = (Conc*) r1;
Conc* r4 = (Conc*) r2;
b = equals(r3->getLeft(),r4->getLeft()) &&
equals(r3->getRight(), r4->getRight());
break;
}
}// switch
return b;
} // equals
// Repeated application of simp until we reach a fixpoint
RE* simpFix(RE* r1) {
RE* r2 = r1->simp();
if(equals(r1,r2))
return r1;
return simpFix(r2);
}
#endif // __RE__
Transform.h
// Ein moeglicher Rahmen fuer Aufgabe 4, zweite Teilaufgabe,
// uebersetze regulaeren Ausdruck in einen NFA.
// Der Einfachheit in ein .h File gepackt.
#include <iostream>
using namespace std;
#include "FSA.h"
#include "RE.h"
int nameSupply;
void init() {
nameSupply = 0;
}
int fresh() {
return nameSupply++;
}
// Macht die eigentliche Arbeit
NFA transformWorker(RE *r);
// Schnittstelle fuer Benutzer
// Ruecksetzen des "name supplies" zur Generierung von eindeutigen Zustaenden
// Aufruf von transform2
NFA transform(RE *r) {
init();
return transformWorker(r);
}
// Wir liefern einen NFA zurueck mit genau einem Start und
// genau einem Stop(end)zustand.
NFA transformWorker(RE *r) {
vector<Transition> ts;
int start, stop;
switch(r->ofType()) {
case EpsType: // TODO
case ChType: // TODO
case StarType: // TODO
case ConcType: // TODO
// Phi: Akzeptiert kein Wort
// NFA besteht aus einem Start und Stopzustand und *keiner* Transition
case PhiType:
start = fresh();
stop = fresh();
return NFA(ts, start, stop);
case AltType: {
Alt* r2 = (Alt*) r;
// 1. Baue NFAs der linken und rechten Alternative
NFA n1 = transformWorker(r2->getLeft());
NFA n2 = transformWorker(r2->getRight());
// 2. Generieren neuen Start-/Stopzustand.
// Sammle Start-/Stopzustaende von n1 und n2
// N.B. Annahme: finals besteht aus genau einem Zustand
start = fresh();
stop = fresh();
int n1_start = n1.getInitial();
int n1_stop = n1.getFinals()[0];
int n2_start = n2.getInitial();
int n2_stop = n2.getFinals()[0];
// 3. Sammle Transitionen auf von n1 und n2.
// Verbinde neuen Start-/Endzustand mit alten Start-/Endzustaenden.
vector<Transition> t1 = n1.getTransitions();
vector<Transition> t2 = n2.getTransitions();
ts.insert(ts.end(),t1.begin(),t1.end());
ts.insert(ts.end(),t2.begin(),t2.end());
ts.push_back(Transition(start, n1_start));
ts.push_back(Transition(start, n2_start));
ts.push_back(Transition(n1_stop, stop));
ts.push_back(Transition(n2_stop, stop));
return NFA(ts,start,stop);
}
} // switch
} // transformWorker
TestOrakel.h
//////////////////////////////////
// Testorakel
// der Einfachheit alles in ein .h File gepackt
#include "RE.h"
// Testorakel
RE* deriv(RE* r, char l) {
switch(r->ofType()) {
case PhiType:
return r;
case EpsType:
return new Phi();
case ChType:
if (((Ch*)r)->getChar() == l) {
return new Eps();
}
else {
return new Phi();
}
case StarType: {
RE* r1 = ((Star*) r)->getRE();
return new Conc(deriv(r1,l),r);
}
case AltType: {
RE* r1 = ((Alt*) r)->getLeft();
RE* r2 = ((Alt*) r)->getRight();
return new Alt(deriv(r1,l), deriv(r2,l));
}
case ConcType: {
RE* r1 = ((Conc*)r)->getLeft();
RE* r2 = ((Conc*)r)->getRight();
if(r1->containsEps()) {
return new Alt(new Conc(deriv(r1,l),r2), deriv(r2,l));
}
else {
return new Conc(deriv(r1,l),r2);
}
}
}// switch
}
bool match(RE* r, string s) {
for(int i=0; i < s.length(); i++) {
r = deriv(r, s[i]);
}
return r->containsEps();
}
testTeil1.cpp
// Testrahmen fuer 1. Teilaufgabe
#include "RE.h"
#include "FSA.h"
#include "TestOrakel.h"
#include <iostream>
#include <string>
using namespace std;
// Systematischer Test
// Ein Testfall besteht aus einem regulaeren Ausdruck (RE)
// und einem Eingabestring.
//
// Testorakel match ueberprueft ob string s enthalten in regex r.
//
// Simplifizierer korrekt fuer Testfall, falls Testorakel gleiches
// Ergebnis liefert fuer Original regex r und simplifizierten regex r->simp()
bool testSimp(RE* r, string s) {
bool b = (match(r,s) == match(r->simp(),s));
cout << "Test case: " << r->pretty() << " " << s << "\n";
cout << "Test result: " << b << endl;
return b;
}
int main() {
cout << "Ein paar Testfaelle. Ueberpruefung per Auge" << endl;
RE* r3 = new Alt (new Phi(), new Ch('c'));
RE* r4 = new Alt (new Ch('c'), new Phi());
cout << r3->pretty() << endl;
cout << r3->simp()->pretty() << endl;
RE* r5 = new Star(new Star (new Ch('c')));
cout << r5->pretty() << endl;
cout << r5->simp()->pretty() << endl;
RE* r6 = new Star(new Phi());
cout << r6->pretty() << endl;
cout << r6->simp()->pretty() << endl;
cout << "Verwende testSimp" << endl;
testSimp(r5, "ab");
// TODO: mehr Tests
}
testTeil2und3.cpp
// Testrahmen fuer 2. und 3. Teilaufgabe
#include <iostream>
using namespace std;
#include "FSA.h"
#include "RE.h"
#include "Transform.h"
#include "TestOrakel.h"
#include <iostream>
#include <string>
using namespace std;
// Systematischer Test
// Ein Testfall besteht aus einem regulaeren Ausdruck (RE)
// und einem Eingabestring.
//
// Testorakel match ueberprueft ob string s enthalten in regex r.
//
// closure und Tranformater RE -> NFA korrekt, falls Testorakel gleiches
// Ergebnis liefert wie Ausfuehrung des resultierenden NFA.
bool testClosureTransform(RE* r, string s) {
NFA nfa = transform(r);
FSA fsa(nfa);
bool b1 = fsa.run(s);
bool b2 = match(r,s);
bool b = b1 == b2;
cout << "Test case: " << r->pretty() << " " << s << "\n";
cout << "Test result: " << b << endl;
return b;
}
int main() {
RE* r3 = new Alt (new Phi(), new Ch('c'));
testClosureTransform(r3, "ab");
// TODO: mehr Tests
}
Standard qsort is not type-safe.
How to make it type-safe?
We make use of templates and lambdas (as found in C++14).
// Usage: g++ -std=c++14
#include <stdlib.h>
#include <stdio.h>
/* Recall the prototype of qsort.
void qsort(void *base,
size_t num_elements ,
size_t element_size,
int (*compare)(void const *,
void const *));
*/
// qsort Beispiel
// generisch:
// (1) Beliebige Datentypen
// (2) Beliebige compare Funktion (Ordung)
//
// Problem: Nicht typsicher
//
template<typename T>
void printArray(T* x, int len, void pr(T)) {
for(int i=0; i<len; i++) {
pr(x[i]);
}
}
int cmpInt(const void* x, const void* y) { return *((int*)x) >= *((int*)y); }
int cmpFloat(const void* x, const void* y) { return *((float*)x) >= *((float*)y); }
void testQsort() {
int x[3] = {3,2,1};
printArray<int>(x, 3, [] (int x) { printf("%d", x); });
printf("\n");
qsort(x, 3, sizeof(int), &cmpInt);
printArray<int>(x, 3, [] (int x) { printf("%d", x); });
float y[2] = {1.0, 2.0};
qsort(y, 2, sizeof(float), &cmpInt); // Sollte sein &compFloat !!!
}
template<typename T>
int f(T x, T y) { return 1; }
// Typ-safe version of qsort.
// Requires.
// Variable templates C++14
// Needs to be initialized with an value, to be overwritten later.
// The rhs value needs to be generic.
template<typename T>
int (*cmpFunc)(T,T) = &f;
template<typename T>
int cmpHelper(const void* x, const void* y) {
return (*cmpFunc<T>)(*((T*)x), *((T*)y));
}
template<typename T>
void mysort(T* base, size_t len, int cmp(T,T)) {
// qsort expects a function pointer.
// We assign the type-specific cmp function to a global variable.
// The helper cmp function will then access this cmp function
// via the global variable.
cmpFunc<T> = cmp;
qsort(base, len, sizeof(T), &cmpHelper<T>);
}
void testQsortSafe() {
int x[3] = {3,2,1};
printArray<int>(x, 3, [] (int x) { printf("%d", x); });
printf("\n");
mysort<int>(x, 3, [](int x, int y) -> int { return x > y; });
printArray<int>(x, 3, [] (int x) { printf("%d", x); });
printf("\n");
float y[3] = { 1.0, 2.0, 3.0 };
printArray<float>(y, 3, [] (float x) { printf("%f", x); });
mysort<float>(y, 3, [](float x, float y) -> int { return x < y; });
printf("\n");
printArray<float>(y, 3, [] (float x) { printf("%f", x); });
}
int main() {
testQsort();
testQsortSafe();
}
Highlights:
Playing with higher-order functions.
Example of an embedded (internal) domain-specific language (DSL).
DSL seperated from host language via types.
GPL = general-purpose language (C, Java, Kotlin, Haskell, Go, Python, …)
DSL = domain-specific language (SQL, Latex, bash, perl, …)
DSLs are usually external languages. Come with their own syntax, interpreter, compiler, … (“eco-system”).
Internal DSLs (a.k.a. embedded DSLs aka APIs aka Frameworks). Make use of the host language (GPL) to provide similar functionality like a external DSL. Often easier to evolve and adapt. Can reuse existing eco-system. Might to be not as optimized as external DSL. Other issues, domain-specific error handling etc.
There are lots of different parsing approaches.
For example, see LL and LR parsing. Check out the additional notes on syntax analysis.
Another popular approach are https://en.wikipedia.org/wiki/Parser_combinator.
Main idea:
Use combinators to build parsers.
Combinators are fancy API functions which hide much of the plumbing necessary to carry out parsing.
Often, a EBNF specification can be directly translated into some sequence of combinator calls.
There exists lots of parser combinator libraries for most programming languages.
Compared to parsing tools such as yacc and ANTLR, the parser is embedded into the host language. So, parser combinators form an internal domain-specific language (DSL) (aka embedded DSL) whereas yacc and ANTLR are external DSLs.
In the following, we give a (rather naive) encoding of a parser combinator library in C++11.
A parser is a function which takes a string and yields a result and the remaining string.
template<typename T>
struct Result {
T value;
string remainder;
bool status;
};
template<typename T>
Result<T> mkResult(T x, string s, bool b) {
return {x, s, b};
}
template<typename T>
using Parser = std::function<Result<T>(string)>;
where type parameter T
represents the parsing result
(e.g. some abstract syntax tree) and the returning string
represents the remaining input (remainder). As parsing may fail, we also
return a Boolean value to represent either success or failure.
Application of a parser on some input.
The epsilon combinator which parses the empty string. We leave the input string untouched and report as a result the empty string.
template<typename T>
Parser<T> eps(T x) {
return [x](string s) -> Result<T> { return {x, s, true}; };
}
A parser to accept a specific character (item).
template<typename T>
Parser<T> item(char x, T f(char)) {
return [x,f](string s) -> Result<T> {
if (s.length() == 0) {
return mkResult<T>(f('\0'), s, false);
} else if (s[0] == x) {
return mkResult<T>(f(s[0]), s.substr(1), true);
} else {
return mkResult<T>(f('\0'), s, false);
}
};
}
Build a new parser by composition of an exisisting parser.
template<typename T>
Parser<T> alt(Parser<T> p1, Parser<T> p2) {
return [p1,p2](string s) -> Result<T> {
auto r1 = parse(p1, s);
if (!r1.status) {
return parse(p2, s);
} else {
return r1;
}
};
}
template<typename T, typename S>
Parser<S> seq(Parser<T> p, Parser<S> f(T)) {
return [p,f] (string s) -> Result<S> {
auto r = parse(p, s);
if (!r.status) {
return r;
} else {
return parse(f(r.value), r.remainder);
}
};
}
alt
is left-biased, if the first argument (parser)
fails, we try the alternative.
seq
runs two parsers in sequence. Recall that as a side
product of parsing we usually expect to obtain a parse tree. With parser
combinators the result obtained can be arbitrary. Check the type of
seq
!
As we largely ignore here the result produced by parsing, we write a matcher function which checks if our parser matches some input string.
template<typename T>
bool match(Parser<T> p, string s) {
auto r = parse<T>(p, s);
return r.status == true && r.remainder.length() == 0;
}
Some helper check for a specific character.
A specific variant of seq
.
Parser<S> conc(Parser<T> p, Parser<S> q) {
return [p,q] (string s) -> Result<S> {
auto r = parse(p, s);
if (!r.status) {
return r;
} else {
return parse(q, r.remainder);
}
};
}
The following examples
cout << match(acceptABorC(), "ab");
cout << match(acceptABorC(), "c");
cout << match(acceptABorC(), "ac");
yield the output
110
Most parser combinator libraries provide further combinators so we can even deal with left-recursive, ambiguous and even beyond context-free grammars. For details see here https://en.wikipedia.org/wiki/Parser_combinator.
In fact, some of these more expressive combinators can be directly encoded in terms of the host language. In the following, we show how to encode Kleene star.
Consider the regular expression
a* c
which can also be defined in terms of the following CFG productions
A -> a A | c
Observation:
Our host language C++ supports recursive function.
Encode the Kleene star via a recursive function yielding a parser for the above grammar.
Parser<int> acceptAsC() {
return alt<int>(acceptChar('c'),
seq<int,int>(acceptChar('a'),
[] (int i) -> Parser<int> { return acceptAsC(); }));
}
Some points to note.
Symbol c
tells us to stop, hence, is tried first.
Recall that alt
is left-biased.
We can NOT write (the more direct) combinator program
Parser<int> acceptAsC2() {
return alt<int>(acceptChar('c'),
conc<int,int>(acceptChar('a'), acceptAsC2()));
}
C++ is a strictly evaluated language and the above would immediately lead to a stack overflow. Hence, we need to ‘hide’ the recursive call within a (lambda) function.
Aside. In the lazy language Haskell, the more direct encoding is possible because program parts are lazily evaluated (i.e. only when needed).
Parser combinators are domain-specific language embedded into some host language (here C++).
Our host language C++ is strongly type, so we can statically guarantee that our parsers are “well-formed” (a parser is a composition of existing parsers).
We make use of recursion available in the host language to extend the expressiveness of our parser combinator language (see encoding of Kleene star).
DSL (external):
M ::= 'c' | 'a' M
EDSL:
Parser<int> acceptAsC() {
return alt<int>(acceptChar('c'),
seq<int,int>(acceptChar('a'),
[] (int i) -> Parser<int> { return acceptAsC(); }));
}
// Usage: g++ -std=c++11
// Parser combinators https://en.wikipedia.org/wiki/Parser_combinator
#include <string>
#include <iostream>
using namespace std;
template<typename T>
struct Result {
T value;
string remainder;
bool status;
};
template<typename T>
Result<T> mkResult(T x, string s, bool b) {
return {x, s, b};
}
/*
// This variant won't work.
// Lambdas with captures can't be converted (easily) into function pointers.
template<typename T>
using Parser = Result<T> (*) (string);
*/
template<typename T>
using Parser = std::function<Result<T>(string)>;
template<typename T>
Result<T> parse(Parser<T> p, string s) {
return p(s);
}
template<typename T>
Parser<T> eps(T x) {
return [x](string s) -> Result<T> { return {x, s, true}; };
}
template<typename T>
Parser<T> item(char x, T f(char)) {
return [x,f](string s) -> Result<T> {
if (s.length() == 0) {
return mkResult<T>(f('\0'), s, false);
} else if (s[0] == x) {
return mkResult<T>(f(s[0]), s.substr(1), true);
} else {
return mkResult<T>(f('\0'), s, false);
}
};
}
template<typename T>
Parser<T> alt(Parser<T> p1, Parser<T> p2) {
return [p1,p2](string s) -> Result<T> {
auto r1 = parse(p1, s);
if (!r1.status) {
return parse(p2, s);
} else {
return r1;
}
};
}
template<typename T, typename S>
Parser<S> seq(Parser<T> p, Parser<S> f(T)) {
return [p,f] (string s) -> Result<S> {
auto r = parse(p, s);
if (!r.status) {
return r;
} else {
return parse(f(r.value), r.remainder);
}
};
}
/*
// This variant doesn't seem to work.
// Not exactly clear why. Compiler complains can't convert
// lambda to std::function.
template<typename T, typename S>
Parser<S> conc2(Parser<T> p, Parser<S> q) {
return seq<T,S>(p, [q] (T x) -> Parser<S> { return q; });
}
*/
template<typename T, typename S>
Parser<S> conc(Parser<T> p, Parser<S> q) {
return [p,q] (string s) -> Result<S> {
auto r = parse(p, s);
if (!r.status) {
return r;
} else {
return parse(q, r.remainder);
}
};
}
template<typename T>
bool match(Parser<T> p, string s) {
auto r = parse<T>(p, s);
return r.status == true && r.remainder.length() == 0;
}
Parser<int> acceptChar(char c) {
return item<int>(c, [] (char c) { return 1; });
}
Parser<int> acceptABorC() {
return alt(conc<int,int>(acceptChar('a'), acceptChar('b')),
acceptChar('c'));
}
Parser<int> acceptAsC() {
return alt<int>(acceptChar('c'),
seq<int,int>(acceptChar('a'),
[] (int i) -> Parser<int> { return acceptAsC(); }));
}
Parser<int> acceptAsC2() {
return alt<int>(acceptChar('c'),
conc<int,int>(acceptChar('a'), acceptAsC2()));
}
int main() {
cout << match(acceptABorC(), "ab");
cout << match(acceptABorC(), "c");
cout << match(acceptABorC(), "ac");
cout << match(acceptAsC(), "aaaac");
// cout << match(acceptAsC2(), "aaaac");
}