Vertiefung und Spassaufgaben in der Vorlesung/Labor

Martin Sulzmann

Inhalt

  1. Fingerübungen
  2. Berechnung aller Untermengen (C Bitoperationen)
  3. Funktionen (Rationale Zahlen, Referenzübergabe, Tests)
  4. Count - Zählen von Wörtern inklusive Testrahmen (C Zeiger und Strings)
  5. Strings - Speicherverwaltung
  6. Speicherverwaltung und Pointer am Beispiel von Fibonacci (C)
  7. Erweiterbarer Testrahmen (C++ virtuelle Methoden)
  8. Reguläre Ausdrücke und Automaten
  9. Playing with Templates and Lambdas (C++14)
  10. Parser combinators

Fingerübungen

Im folgenden betrachten wir Beispiele von Programmen. Dazu gibt es Veständnissfragen und kleinere Programmieraufgaben.

Boolesche Bedingungen

Was ist der Sinn und Zweck folgendes Programmtextes. Könnte ein Fehler darin versteckt sein?

while(a[i] != '\0' && i >=0 && i < MAX) {
  i++;
}

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.

while(i >=0 && i < MAX && a[i] != '\0') {
  i++;
}
  1. Zuerst die Überprüfung, ob Indexgrenzen gültig sind.

  2. Dann der Zugriff auf die Position.

Das Prinzip dahinter heisst “Kurzschlussauswertung”:

  1. Im Falle von “UND” verknüpften Bedingungen werden diese in der textuellen Reihenfolge von links nach rechts ausgewertet.

  2. Falls eine (Teil)Bedingung FALSCH liefert, liefert der ganze Ausdruck FALSCH.

  3. 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.

if(x > 5 && factorial(y) > x)  {
}

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

if(x > 5 &  factorial(y) > x)  {
}

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.

Kontrollstrukturen (Schleifen)

Vergleich von while, do-while und for Schleifen.

  1. Geben Sie für jede Art von Schleife ein Beispiel.

  2. Kann jede Schleifenart in die andere übersetzt werden?

  3. Welche Schleife finden Sie am sinnvollsten?

  4. 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

Zeiger

Betrachten Sie folgende Ausdrücke. Annahme dabei ist

int a[20];
int i,j;
a[i]
a[i] + j
*(a + i)
*(a + i) + j

Was machen die einzelnen Ausdrücke?

Betrachten Sie folgenden Programmtext.

int a[10];
int i;

i = 0;
while(i < 10) {
  a[i] = i;
  i++;
}

Schreiben Sie obiges Programm um. Dabei sollen Sie anstatt einer while Schleife eine for Schleife verwenden und anstatt des Arrayzugriffs nur Zeigerzugriffe verwenden.

Berechnung aller Untermengen (Bitoperationen)

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.

Lösungsidee

Lösungsvorschlag 1


#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");

  }

}

Lösungsvorschlag 2

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.

int test(unsigned short int x, int pos) {
  if (x & (1<<pos)) return pos;
  return -1;
}

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:

Lösung 2 komplett

# 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?

# 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;
  }

}

Funktionen (Rationale Zahlen, Tests, Referenzübergabe)

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!

Rationale Zahlen

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.

struct RZ_ {
    int Zaehler;
    int Nenner;
};

Wir verwenden die folgende Abkürzung.

typedef struct RZ_ RZ;

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.

Tests

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.

User-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));

}

Unit-Tests

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.

Invarianten

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).

Referenzübergabe

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.

Kürzen (Erweiterte Funktionalität)

RZ kuerzenRZ(RZ x);

Implementierung sie eine Funktion welche die rationale Zahl kürzt. Folgende Kürzungsregel gilt: (k * m) / (k * n) = m / n.

Komplettes Programm

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();
}

Count - Zählen von Wörtern inklusive Testrahmen (Zeiger und Strings)

Wir betrachten Strings und Manipulationen auf Strings. Hintergrund zu Strings. Siehe C - Teil 3.

Zählen von Wörtern

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.

int count(char* input);

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.

count("") ==> 0
count("Hallo") ==> 1
count("  Hallo") ==> 1
count("  Ha llo") ==> 2

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.

typedef enum {
  OK,
  FAIL
} Test;

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.

typedef struct {
  char* input;
  int expected;
} TestCase;

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.

count("") ==> 0
count("Hallo") ==> 1
count("  Hallo") ==> 1
count("  Ha llo") ==> 2

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.

char* leerzeichen(char* input) {
  while(*input == ' ')
    input++;
  return input;
}

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.

char* zeichen(char* input) {
  while(*input != '\0' && *input != ' ')
    input++;
  return input;
}

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:

  1. Überspringe alle Leerzeichen

  2. Fall Zeichen gefunden

    1. zähle Anzahl gefunder Wörter hoch
    2. Gehe zu Wortende

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.

Als Anhang der komplette Programmcode

#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);

}

Als Anhang: Zustandsbasierte Modellierung


#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;
}

Strings - Speicherverwaltung

Speicherveraltung (malloc/free) am Beispiel von strings.

#include <stdio.h>
#include <stdlib.h>

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.

int len(char* s) {
  int i = 0;
  while(*s != '\0') {
    s++;
    i++;
  }
  return i;
}

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.

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

Beachte

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.

Als Anhang der komplette Programmcode

#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;
}

Speicherverwaltung und Pointer am Beispiel von Fibonacci (C)

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.

Fibonacci

In C kann die Fibonacci wie folgt ausgedrückt werden.

int fib(int n){
 int f1, f2;
 if (n<=1) return 1;
 f1=fib(n-1);
 f2=fib(n-2);
 return f1+f2;
}

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).

Run-Time Stack

Der Speicherbereich bei der Ausführung des Programmes ist wie folgt aufgeteilt.

 CODE
 STATIC DATA
 STACK
 ...
 HEAP

Speicheradressen und Run-Time Stack

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;
}

Nochmal Ausführung von Fibonacci

int fib(int n){
 int f1, f2;
 if (n<=1) return 1;
 f1=fib(n-1);
 f2=fib(n-2);
 return f1+f2;
}
 fib(4) -> fib(3)  -> fib(2)  -> fib(1)
                              -> fib(0)
                   -> fib(1)
        -> fib(2)  -> fib(1)
                   -> fib(0)

Reine Funktionen

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.

Fibonacci Array

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.

  fibArr[n].fib = x;
  fibArr[n].status = Full;
  if (fibArr[n].status == Full)
     return fibArr[n].fib;
  else
     return fib(n);

Aufgabe: Was fehlt in der Implementierungsskizze? (Hinweis: Die Länge des Fibonacci Arrays ist begrenzt)

Array versus Pointer

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:

Als nächstes gehen wir die Optimierung von Fibonacci mit Hilfe des Fibonacci Arrays an.

Ausgabe Fibonacci Array

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.

Allokation des Fibonacci Arrays

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);
}

Aufgaben:

void allocFibArray2(int len, FibPtr f)

Fibonacci und dynamische Programmierung

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.

struct FibArray {
  FibPtr f;
  int len;
};

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;
}

Aufgabe: Erklären Sie wieso die Redundant markiert Stelle überflüssig ist. (Erklärung findet sich im Anhang)

Dynamische Erweiterung des Fibonacci Array

Aufgabe: Implementieren Sie eine Variante, die eine dynamische Anpassung des Fibonacci Arrays erlaubt.

int fib4(struct FibArray* fa, int n);

fa wird nun als Pointer übergeben, so können wir von aussen Längenanpassungen sehen.

Eine Implementierung von fib4 findet sich im Anhang.

Zusammenfassung

Wir haben den Umgang mit Arrays, Pointer und manueller Speicherverwaltung am Beispiel von Fibonacci und dynamischer Programmierung geübt.

Anhang: Kompletter Source Code

#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;
}

Erweiterbarer Testrahmen (C++ virtuelle Methoden)

Wir betrachten eine Verallgemeinerung unseres Testrahmens.

Abstrakte Testklasse

Zuerst kapseln den allgemeinen Testfall in einer abstrakten Klasse.

typedef enum {
  OK,
  FAIL
} TestResult;


class TestCase {
  public:

  virtual TestResult execute() = 0;

};

Generische Testausführung

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.

Spezielle Testinstanz

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;
  }

};

Als Anhang der komplette Programmcode

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);


}

Reguläre Ausdrücke und Automaten (C++)

Diese Aufgabe besteht aus folgenden Teilen:

  1. Vereinfachung regulärer Ausdrücke

  2. Transformation Regulärer Ausdrücke in endliche Automaten

  3. 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.

Reguläre Ausdrücke

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:

Die Sprache L(r) beschrieben durch den regulären Ausdruck r ist definiert wie folgt:

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.

Standard Methoden auf Regulären Ausdrücken

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.

  RE* r = new Conc (new Ch('c'), new Alt(new Ch('a'), new Ch('b')));
  cout << r->pretty() << endl;

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.

Teilaufgabe 1: Vereinfachung Regulärer Ausdrücke

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
  1. eps r ==> r

  2. r1 r2 ==> phi falls L(r1)={} oder L(r2)={}

  3. r* ==> eps falls L(r)={}

  4. (r*)* ==> r*

  5. r + r ==> r

  6. r1 + r2 ==> r2 falls L(r1)={}

  7. 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

Weiteres Vorgehen

Wir erweitern Klasse RE wie folgt.

class RE {
 public:
  ...
  virtual bool isPhi()=0;
  virtual RE* simp() { return this; }
};

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.

bool equals(RE* r1, RE* r2) {
  return r1->pretty() == r2->pretty();
}

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;
}

Teilaufgabe 2: Transformation Regulärer Ausdrücke in Automaten

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.

Endliche Automanten

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

  1. Einer Menge Sigma aller Zeichen die vom Automaten behandelt werden, oft wird Sigma auch als das Alphabet des Automaten bezeichnet.
  2. Einer Menge Q aller Automantenzustände.
  3. Einen Initialzustand, auch Startzustand genannt, oft bezeichnet als q0 oder s0.
  4. Einer Menge F von Finalzuständen, auch End- oder Stopzustände, genannt. Initialzustand und Finalzustände sind natürlich Elemente der Menge Q.
  5. Einer Menge T von Transitionen, auch Übergänge genannt, von einem Zustand in den anderen. Ein Übergang (Transition) kann auf folgende zwei Arten beschrieben werden.
    1. Konsumierung eines Zeichens aus Sigma.
    2. Spontaner Übergang, auch “epsilon” Transition genannt.

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.

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.

Von regulären Ausdrücken zu endlichen Automaten

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).

Thompson NFA Algorithmus

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).

C++ Umsetzung

Folgende Klassen sind gegeben. Bauen Sie Ihre Lösung auf Grundlage dieser Klassen auf.

class Transition;
class NFA;
class FSA;

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.

class RE {
 public:
   virtual NFA transform()=0;
   // und restliche Methoden
};
NFA transform(RE* r);

Weitere 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

Teilaufgabe 3: Ausführung NFA

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}

  1. Schritt 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.

  1. Schritt: 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:

Dieser Vorgang wird solange ausgeführt, bis keine Zustände mehr zu current hinzugefügt werden können.

Im Detail:

  1. Iterieren Sie über die Menge der Transitionen.
  2. Mit Hilfe der Methode isEpsilonTransition der Klasse Transition können Sie alle epsilon Transitionen herausfiltern.
  3. Mit Hilfe der Methoden 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 Zustandqhat. Viat.toState(q)` können Sie den Endpunkt der (epsilon) Transition berechnen.
  4. Um zu überprüfen, ob ein Element in einem Vektor ist benutzen Sie die 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.

Weitere Methoden

Methoden reset, isFinal, step und run sind schon gegeben.

Ein reset bedeutet, wir brechen die Ausführung ab, und starten neu:

void FSA::reset() {
    current.clear();
    current.push_back(init);
    closure();
}

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();
}

Testrahmen

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).

NFA transform(RE *r);

Testorakel

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();
}

Testausführung

Ein Testfall besteht aus einem regulären Ausdruck und einem Eingabestring. Testausführung ist wie folgt.

  1. Transformiere RE nach NFA
  2. Baue FSA
  3. Fuehre FSA fuer Eingabestring aus (“run”)
  4. Ueberpruefe ob Eingabestring in RE enthalten
  5. Rueckgabe true falls Ergebnisse 3. und 4. uebereinstimmen. Sprich das Testorakel (Referenzimplementierung) und Ihre Implementierung liefern das gleiche Ergebnis. Ansonsten Rueckgabe false.
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;
}

main

Unten finden Sie ein paar einfache Testfälle. Sie sollten natürlich Ihre Implementierung gegen weitere Fälle testen.

int main() {

  // Testfaelle

  // (ab)*
  RE * r1 = new Star(new Conc(new Ch('a'), new Ch('b')));
  string s1 = "abab";

  cout << testExec(r1,s1) << endl;

  string s2 = "ababa";

  cout << testExec(r1,s2) << endl;
}

Anhang zu Reguläre Ausdrücke und Automaten

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

}

Playing with Templates and Lambdas (C++14)

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();

}

Parser combinators

Highlights:

DSL versus GPL

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.

The problem domain we consider

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:

In the following, we give a (rather naive) encoding of a parser combinator library in C++11.

What is a (functional) parser?

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.

template<typename T>
Result<T> parse(Parser<T> p, string s) {
  return p(s);
}

Basic combinators

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);
    }
  };
}

Building higher-order combinators

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!

Some simple examples

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.

Parser<int> acceptChar(char c) {
  return item<int>(c, [] (char c) { return 1; });
}

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

More expressive parser combinators

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:

Parser<int> acceptAsC() {
  return alt<int>(acceptChar('c'),
              seq<int,int>(acceptChar('a'),
                   [] (int i) -> Parser<int> { return acceptAsC(); }));
}

Some points to note.

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).

Summary

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(); }));
}

Complete source code


// 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");

}