Dieser sechste Teil der Serie über Modula-2 wird drei Gebiete behandeln: Rekursion, den Datentypen SET und die eingebauten Standardfunktionen.
In der letzten Folge wurden Prozeduren und Funktionen behandelt. Ein Programm rief eine Prozedur auf, diese wurde abgearbeitet und danach kehrte der Rechner an die Stelle nach dem Aufruf zurück. Was passiert nun aber, wenn sich eine Prozedur selber aufruft, wenn im Prozedurkörper von a ein Aufruf a steht?
Einen solchen Selbstaufruf nennt man Rekursion, und diese wäre fast ein eigenes Kapitel wert. Ich will Ihnen die Rekursion an Beispielen nahebringen; das erste soll Zahlen ausgeben so ausgeben, daß von 1 angefangen jeweils die doppelte Zahl folgt, also 1,2,4,8 usw. Schreiben wir zunächst eine Prozedur, die einfach ihren Parameter ausgibt:
PROCEDURE WriteDoppel(n:INTEGER);
BEGIN
WriteInt(n,5);
WriteLn;
END WriteDoppel;
WriteInt und WriteLn müssen dabei natürlich wie bei den anderen Beispielen aus InOut importiert worden sein. Ein Aufruf mit WriteDoppel(1) gibt einmal die Zahl 1 aus und endet dann. Nun soll aber nach der Ausgabe einer Zahl die Ausgabe der nächsten, verdoppelten Zahl folgen. Dafür steht schon eine Prozedur bereit, nämlich WriteDoppel. Also können wir in die Prozedur einen Selbstaufruf einfügen:
PROCEDURE WriteDoppel(n:INTEGER);
BEGIN
WriteInt(n,5);
WriteLn;
WriteDoppel(n*2);
END WriteDoppel;
Was geschieht nun bei einem Aufruf WriteDoppel(1)? Zunächst schreibt WriteInt(n,5) eine 1 auf den Bildschirm. Beim rekursiven Aufruf übergibt n*2 als Parameter eine 2. Der Rechner richtet nun erneut eine Prozedur WriteDoppel ein und führt sie mit dem Parameter 2 aus. Folge ist die Ausgabe von 2 und ein erneuter Selbstaufruf mit dem Parameter 4. Diese Aufruffolge geht immer weiter.
Bei jedem Aufruf richtet der Rechner neuen Platz für die Parametervariable n ein. Das eingerichtete n aus der Write-Doppel-Stufe, von der aus aufgerufen wird, bleibt erhalten. Das “Herabsteigen” in immer wieder eine neue Rekursionsstufe wird “rekursiver Abstieg” genannt.
Mit dieser Prozedurdefinition endet dieser Abstieg allerdings nie. Die Folge ist, daß irgendwann der Platz ausgeht. Hätte der ST unbegrenzten Speicher, würde das Programm niemals enden. Benötigt wird eine Abbruchbedingung, bei der die Selbstaufrufe beendet werden. Dazu nehmen wir hier die Angabe einer Obergrenze für die Ausgabe:
PROCEDURE WriteDoppel(n,ober:INTEGER);
BEGIN
WriteInt(n,5);
WriteLn;
IF (n*2 <= ober) THEN
WriteDoppel(n*2,ober);
END;
END WriteDoppel;
In b wird eine Zuweisung an den Werteparameter i vorgenommen, die zwar bei der Ausgabe in b ihre Wirkung hat, jedoch nicht auf den aktuellen Parameter n aus a Einfluß nimmt.
In c handelt es sich bei i um einen variablen Parameter, dadurch haben die Zuweisungen Einfluß auch auf den übergebenen aktuellen Parameter n.
Die Prozedur kann so formuliert werden:
PROCEDURE Balken(n: INTEGER); VAR i:INTEGER; BEGIN FOR i:=1 TO n DO Write('*'); END; WriteLn; END Balken;
Die Anzahl der auszugebenden Sternchen wird als Parameter n übergeben. Die FOR-Schleife leistet die Ausgabe von n Zeichen. Die Laufvariable wird lokal definiert.
Es werden zwei zusätzliche Parameter nötig, text für die Legende und symbol für das Zeichen, das zur Ausgabe verwendet werden soll:
PROCEDURE Balken (text: ARRAY OF CHAR; symbol: CHAR; n:INTEGER); VAR i:INTEGER; BEGIN WriteString(text); Write(' '); FOR i:=1 TO n DO Write(symbol); END; END Balken;
Die Unterschiede liegen in der zusätzlichen Ausgabe des Legendentextes und der Verwendung des Parameters symbol in dem Write-Statement.
Die Funktion lautet wie folgt:
PROCEDURE Durchschnitt(feld:ARRAY OF INTEGER): INTEGER; VAR summe:LONGINT; index:INTEGER; BEGIN summe:=0; FOR index:=0 TO HIGH(feld) DO summe:=summe+LONGINT(feld[i]); END; RETURN INTEGER(summe/LONGINT(HIGH(feld))); END Durchschnitt;
Das Feld wird als offener Parameter übergeben. Als Ergebnis wird ein INTEGER geliefert. In der Prozedur summiert die FOR-Schleife alle Feldelemente auf, wozu sicherheitshalber ein LONGINT verwendet wird. Das Ergebnis ergibt sich aus der Division der Summe durch die Anzahl der Feldelemente. Die Casts in der Ergebnisberechnung sind notwendig, da HIGH eigentlich ein CARDINAL-Ergebnis liefert und das Divisionsergebis als INTEGER zurückgegeben werden muß.
Ein Aufruf mit WriteDoppel(1,16) gibt nun wie gehabt die Zahlen 1,2,4 usw. aus, stoppt aber die Selbstaufrufe in dem Moment, da beim Verdoppeln des Parameters die Obergrenze überschritten wird. Damit ist die letzte ausgegebene Zahl 16. Die oberste Stufe der WriteDoppel-Aufrufe (also die mit Parameter n gleich 16) wird beendet und der gesamte vom Rechner beim Aufruf für n reservierte Speicher freigegeben. Der Computer rechnet nun nach dem Selbstaufruf der Stufe mit n=8 weiter. Diese Prozedur wird ebenfalls beendet, was sich bis zur letzten Stufe fortsetzt. Dieses “Hochklettern” durch die Rekursionsstufen ist der “rekursive Aufstieg”. Damit ist die Rekursion korrekt zu Ende gebracht.
Die Prozedur soll nun so erweitert werden, daß die Zahlen zunächst ansteigen und dann wieder abfallen, also 1,2,4,8,8, 4,2,1. Durch den rekursiven Aufruf wird diese Erweiterung sehr einfach (siehe nächste Spalte).
PROCEDURE WriteDoppel(n, ober:INTEGER);
BEGIN
WriteInt(n,5);
WriteLn;
IF (n*2 <= ober) THEN
WriteDoppel(n*2,ober);
END;
WriteInt(n,5);
END WriteDoppel;
Ein Durchlauf einer Stufe von WriteDoppel läuft nun so ab: Parameter ausgeben, Selbstaufruf, durch den die höheren Zahlen auf den Bildschirm kommen, und danach erneute Ausgabe des - auf dieser Stufe unveränderten - Parameters. Die Obergrenze wird wieder unverändert von Stufe zu Stufe weitergereicht.
Die Ausführungsreihenfolge mit Ab- und Aufstieg soll nochmals schematisch skizziert werden.
Dabei sind die Bildschirmausgaben fett gedruckt. WD bezeichnet den Aufruf WriteDoppel. Sie sehen, wie der Rechner die einzelnen Rekursionsstufen durchläuft und die Zahlen in der gewünschten Reihenfolge (im Bild von links nach rechts) ausgibt. Rekursion besteht also aus dem Abstieg, dem Rekursionsabbruch und dem Aufstieg. Im Programmtext stehen zunächst alle Aktionen des Abstiegs, dann der von einer Abbruchbedingung abhängige Selbstaufruf und schließlich die Anweisungen, die beim Aufstieg ausgeführt werden sollen.
Neben den schon bekannten Datentypen gibt es in Modula noch die Mengen. Eine Menge kann mehrere Werte gleichzeitig enthalten. Jeder Wert stammt aus einem Aufzählungstypen oder einem Unterbereichstypen. Ein Beispiel zur Deklaration:
TYPE Farben = (Rot, Gelb, Gruen);
Ampel = SET OF Farben;
Mit den Schlüsselwörtern SET OF wird der Typ Ampel zu einer Menge, die Werte vom Typ Farben aufnehmen kann. Einer entsprechenden Variablen kann nun ein Inhalt zugewiesen werden:
VAR DieAmpel : Ampel;
... DieAmpel:=Ampel{Rot,Gelb};
Damit enthält die Menge DieAmpel die Werte Rot und Gelb. Bei der Zuweisung muß der Mengentyp vor der Liste der Elemente notiert werden. Die in die Menge zu übernehmenden Elemente sind durch Kommata getrennt und mit geschweiften Klammem zusammengefaßt. In dem Mengenausdruck selber dürfen nur Konstanten auftreten, also nur die Bezeichner, die in der Typdeklaration Vorkommen. Ein Programm
VAR i:Farben;
...
i:=Gelb;
DieAmpel:=Ampel{Rot,i};
ist also nicht möglich. Um auch variable Werte (fest) in die Menge zu nehmen, existiert die Funktion INCL. Erlaubt wäre:
DieAmpel:=Ampel{Rot}
i:=Gelb;
INCL(DieAmpel,i);
Danach hat DieAmpel den Inhalt Ampel{Rot,Gelb}. Um ein Element zu entfernen, steht EXCL bereit:
EXCL(DieAmpel.i);
Somit hätte DieAmpel wieder den Inhalt Ampel {Rot}. Beide Funktionen sind fester Bestandteil von Modula-2.
Der in der letzten Folge kurz genannte Public Domain Modula-Compiler des Lehrstuhls für Prozeßrechner an der TU München hat sich tatsächlich als brauchbares System erwiesen (ich werde es LPR-Modula nennen). Sie können es ohne weiteres zur Arbeit in diesem Kurs verwenden.
Nach den Benchmarks, die ich auch in anderen Modula-Tests in ST-Computer verwende, ist die Geschwindigkeit der erzeugten Programme nur wenig niedriger als beim SPC-Modula-System. Die vorhandenen Bibliotheken entsprechen in etwa der Ausstattung beim TDI-System und sind damit ausreichend, auch für GEM-Programmierung.
Wie muß das System nun installiert werden? Für Diskettensysteme empfiehlt es sich, eine RAM-Disk einzurichten, in der Sie alle notwendigen Programme und Daten halten. Dies sind die Dateien M2PATHS.TXT, M2LOADER.PRG, M2SHELL.OBJ, NEWSHELL.RSC sowie DEBUG.OBJ und DEBUG.RSC. Zusätzlich müssen Sie noch die Bibliotheken, die Sie benötigen, bereithalten, also jeweils die Dateien mit Endung .OBM und .SBM. LPR-Modula wird dann von der RAM-Disk gestartet und kann so sehr schnell geladen werden. Ihre Arbeitsdateien halten Sie auf einer Diskette.
Die Namen aller Directories, in denen das System nach Dateien suchen soll, schreiben Sie in M2PATHS.TXT. Anfangs die Ordner der RAM-Disk und als letzten Pfad ein A:, falls Ihre Arbeitsdateien auf der oberen Directory-Stufe der Diskette liegen.
Auf der PD-Diskette finden Sie die Programme RAMDISK und MK_COPY, mit denen die Einrichtung der RAM-Disk (nach den Angaben in RAMDISK.INF) und das Kopieren von Dateien in die RAM-Disk (nach den Angaben in COPY.LST) automatisch ablaufen können.
Bei einer Festplatte kopieren Sie einfach alle auf der Diskette vorhandenen Dateien in einen beliebigen Ordner - “\LPR” bietet sich an. Durch Edieren der Datei M2PATHS.TXT geben Sie dem System alle Pfade bekannt, auf denen Dateien gesucht werden sollen. Der letzte Pfad sollte Ihr Arbeitsdirectory sein, in dem Sie Ihre Programme entwickeln wollen. Der Einfachheit halber sollten Sie Ordnerstruktur der Diskette übernehmen.
Auf der Original-PD-Diskette fehlt ein Editor - laut Copyright-Vermerk verbieten es die Autoren sogar ausdrücklich, einen beizulegen. Sie müssen also zusätzlich einen Editor in das Arbeitsdirectory kopieren. Um ihn automatisch von der Shell aus zu starten, sollten Sie ihn noch in EDITOR.PRG umbenennen.
Beim Editoraufruf übergibt die Shell automatisch noch den Namen der Arbeitsdatei. Sollten bei einem Compilerlauf Fehler auftreten, übergibt LPR-Modula zusätzlich den Namen einer Fehlerdatei. Damit kann z.B. TEMPUS beide Dateien gleichzeitig öffnen.
Der Start des Systems erfolgt über M2 LOADER.PRG, das Sie als erstes nach einem Objektmodul zum Ausführen fragt. Dies ist hierbei immer M2 SHELL. OBM, das Objektmodul für die Benutzeroberfläche. Lautet die letzte Zeile in M2PATHS.TXT “@MM2S HELL. OBM”, ist diese Datei vorausgewählt, und Sie brauchen nur noch <Retum> zu drücken.
Damit haben Sie die Shell bzw. die Arbeitsoberfläche von LPR-Modula vor sich. Sie kennt eine Arbeitsdatei, auf die sich die Kommandos beziehen. Ihr Name wird in einer kleinen Box angezeigt. Die drei Menüs (s. Bild) steuern die Shell.
Anfangs ist keine Arbeitsdatei ausgewählt. Durch “Modul auswählen” können Sie dies per File-Select-Box tun. Die zwei Kommandos “.DEF” bzw. “.MOD bearbeiten” stellen die Vorgabe für die Dateiendung ein. Wollen Sie Definitionsmodule bearbeiten, sollten Sie “*.DEF” vorwählen.
“Editieren”, “Übersetzen” und “Starten” beziehen sich immer auf die ausgewählte Arbeitsdatei. Wollen Sie die Arbeitsdatei wechseln, können Sie das auch mit den folgenden drei Menükommandos tun, die jeweils nach einem Dateinamen fragen.
Mit “Compiler, Debug” können Sie einige Optionen setzen (s. Bild). Der Compiler kennt die Einstellungen Bereichsüberprüfung und arithmetischer Überlauf. Bei ersterem wird Code erzeugt, der verwendete Feldindizes auf ihre Gültigkeit entsprechend der Felddefinition überprüft. Ebenso laufen Tests bei CASE-Anweisungen und bei Unterbereichs- und Aufzählungsvariablen. Der arithmetische Überlauf achtet bei allen Ausdrücken auf Einhaltung des Wertebereichs.
Compiler und Debugger können durch die entsprechende Option resident gehalten werden. Damit brauchen sie nur einmal in den Speicher geladen zu werden, was natürlich Zeit spart.
Die Voreinstellung der Suchpfade läßt sich mit “Suchpfade ändern” komfortabel in einer Dialogbox vornehmen. Dies ändert nichts daran, daß vor dem ersten Lauf eine gültige Datei M2PATHS. TXT vorliegen muß, damit das System überhaupt gestartet werden kann.
Lassen Sie Modula-Programme von der Shell aus laufen, so werden alle Ausgaben in ein Fenster mit Namen “Terminal” geschrieben. Um die Ausgaben des letzten Programmlaufs zu überprüfen, läßt sich dieses Fenster mit “Terminal öffnen” auf den Desktop bringen.
“.PRG erzeugen” schließlich ruft den Linker auf, der aus der Arbeitsdatei ein vollständiges Programm erzeugt, das ohne die Modula-Shell lauffähig ist.
Im Menü “Datei” finden sich noch einige Funktionen zur Dateiverwaltung, die die Rückkehr zum GEM-Desktop in einigen Fällen ersparen können.
Da es praktisch kein Handbuch gibt, werden Sie vor der Entwicklung eigener Programme alle Dateien mit der Endung .DEF ausdrucken müssen. Damit kennen Sie die Bibliotheksstrukturen und die Namen und Parameter der einzelnen Prozeduren. Ohne diese Unterlagen, die bei kommerziellen Systemen typischerweise einen umfangreichen Handbuchanhang bilden, wird die Arbeit kaum möglich sein.
Auf den Debugger gehe ich an dieser Stelle aus Platzgründen nicht weiter ein. Er dient nach einem Laufzeitfehler zur Analyse der Fehlersituation. Dabei haben Sie in mehreren Fenstern den Quellcode, die Aufrufreihenfolge und Variableninhalte vor sich. Lesen Sie zur Bedienung bitte die Datei “READ.ME” auf der PD-Diskette.
Eine abschließende Anmerkung: Die implementierten String-Funktionen im Modul “Strings” sind so rudimentär, daß sie praktisch nicht zu gebrauchen sind. Daher werde ich in der nächsten Folge - die sich passenderweise auch mit Modulen beschäftigt - ein entsprechendes TDI-kompatibles Strings-Modul vorstellen.
Das PD-Modula-2-System ist auf den Disketten 209 und 210 in der ST Computer PD-Sammlung erhältlich.
Für Mengen steht eine Reihe von Operatoren bereit, mit denen Ausdrücke formuliert werden können. Zunächst die vier “Grundrechenarten” für jeweils zwei Mengen:
+ Vereinigung: Beide Mengen werden zusammengefügt. Dabei steht jedes Element nur einmal im Ergebnis.
{Rot,Gelb} + {Gruen} = {Rot, Gelb, Gruen}
{Rot,Gelb) + {Gelb,Gruen} ={Rot, Gelb, Gruen}
- Differenz: Aus der ersten Menge werden alle Elemente entfernt, die in der zweiten auftauchen.
{Rot,Gelb} - {Gelb} = {Rot}
{Rot,Gelb} - {Gelb,Gruen} = {Rot}
*** Durchschnitt:** Ergebnis ist die Menge aller Elemente, die in beiden Mengen vorhanden sind.
{Rot,Gelb} * {Gelb,Gruen} = {Gelb}
/ symmetrische Differenz: Im Ergebnis stehen nur die Elemente, die nicht in beiden Mengen stehen.
{Rot,Gelb} / {Gelb,Gruen} = {Rot,Gruen}
Als Relationen mit einem Ergebnis vom Typ BOOLEAN sind die Gleichheit = und Ungleichheit # bzw. <> definiert. <= und => fragen ab, ob eine Menge in der anderen enthalten ist.
Schließlich gibt es noch den Operator IN, der testet, ob ein bestimmtes Element in einer Menge enthalten ist:
IF Rot IN DieAmpel THEN ...
Der Ausdruck vom Typ BOOLEAN ergibt TRUE, wenn das angegebene Element in der Menge enthalten ist. IN kann natürlich auch mit einer Variablen verwendet werden.
Ein in Modula vordefinierter Mengentyp ist BITSET. Ein BITSET ist eine Menge von CARDINALS einer bestimmten Größe, die wiederum implementationsabhängig ist. BITSETs könnten selber definiert werden mit
TYPE BITSET = [0..31];
Auf BITSETs sind die beschriebenen Mengenoperationen mit gleicher Funktionalität definiert.
Die maximale Größe von Mengen ist jeweils implementationsabhängig. Auf ETH-Compilern und deren Abkömmlingen sind sie normalerweise auf 32 Bit beschränkt. Warum 32 Bit? Jedes mögliche Element kann in einer Menge enthalten sein oder nicht. Dementsprechend wird für jedes Element aus dem Wertebereich ein Bit in einer Menge eingerichtet. Ist das Element in der Menge, schreibt der Rechner eine 1 ein, ansonsten 0. Beschränkt man die maximale Mengengröße auf einen für den Prozessor optimalen Wert, können SETs sehr schnell verarbeitet werden. Auf dem 68000 ergeben sich z.B. 16 oder 32 Elemente pro Menge.
Welchen Wert Ihr System erlaubt, müssen Sie im Handbuch nachschlagen. Es gibt teilweise ein Modul BigSets, mit dem auch größere Mengen möglich sind.
Eine Reihe von Standardfunktionen sind fest in Modula eingebaut. Ein paar davon haben Sie schon kennengelernt, heute soll die Liste vervollständigt werden.
CHR(x)
ergibt ein Zeichen vom Typ CHAR mit der Ordnungszahl a. Im ASCII-System liefert CHR(65) das Zeichen “A”.
ORD(x)
ergibt die Ordnungszahl des übergebenen Wertes. Dabei kann x beispielsweise ein CHAR sein: ORD(“A”) liefert 65 im ASCII-System. Für x sind alle Aufzählungstypen zugelassen, also auch ORD(Gruen), was 2 liefern würde. Das Ergebnis ist ein CARDINAL.
CAP(ch)
liefert - wenn möglich - den entsprechenden Großbuchstaben zum CHAR ch. CAP(“a”) ist “a” und CAP(“A”) ergibt “A”
TRUNC(r)
Das INTEGER-Ergebnis ist der ganzzahlige Teil der REAL-Zahl r.
FLOAT(i)
liefert eine REAL-Zahl mit Wert der ganzen Zahl i.
INC(x) INC(x,i)
erhöht die ganzzahlige Variable x um 1 oder - falls angegeben - um i. Je nach Prozessor kann der Compiler schnelleren Code generieren als bei x:=x+l oder x:=x+i. Aufgrund der Befehle des 68000 lohnt sich der Einsatz bis zu einer Erhöhung um 8. Bei INC handelt es sich um eine Anweisung, nicht um eine Funktion!
DEC(x) DEC(x,i)
erniedrigt die ganzzahlige Variable x um 1 oder i. Erzeugt ebenfalls schnelleren Code bis zu einer Substraktion von 8 beim 68000.
EXCL(set,element)
entfernt aus einer Menge das angegebene Element.
INCL(set,element)
fügt das angegebene Element der Menge hinzu.
HIGH(f)
liefert die Anzahl der Elemente (von 0 an gezählt) eines Feldes f.
HALT
stoppt die Programmausführung. Ein HALT zählt in den meisten Systemen als Laufzeitfehler und wird nur in extremen Ausnahmen zu verwenden sein.
In der nächsten Folge komme ich endlich zu dem Konzept, das Modula-2 den Namen gegeben hat: den Modulen. Es geht um interne und externe Module, um Lebensdauer und Sichtbarkeit und um Strukturierung innerhalb größerer Programmprojekte.
RT
Schreiben Sie eine rekursive Prozedur, die alle Elemente eines INTEGER-Feldes aufaddiert und das Ergebnis als LONGINT abliefert. Als Hilfe der Prozedurkopf:
PROCEDURE Summe( Feld ARRAY OF CHAR;Startindex:INTEGER):LONGINT;
Welche Ausgaben liefert die folgende Prozedur beim Aufruf mit Puzzle(1) ?
PROCEDURE Puzzle(n:INTEGER); BEGIN WriteInt(n,5); WriteLn; IF n<10 THEN n:=n-1; Puzzle(n+2); END; WriteInt(n,5); WriteLn; END Puzzle;
Regeln Sie den Verkehr! Sie haben zwei Ampeln, eine dreifarbige für Autos und eine Fußgängerampel mit zwei Farben. Schreiben Sie ein Programm, das einmal einen kompletten Ampelzyklus simuliert. In zwei Mengen stellen Sie die Ampeln dar; mit Read, das Sie vorher aus InOut importieren, können Sie eine Tasteneingabe vom Benutzer abfragen. Jeweils dann sollen die Ampeln weitergeschaltet werden. Schreiben Sie zusätzlich zwei Prozeduren ShowCar und ShowWalker, die jeweils per WriteString (aus InOut) die Lichter auf dem Bildschirm ausgeben, die eingeschaltet sind.
Schreiben Sie eine Prozedur Meldung, die wie in der letzten Folge einen Meldungstext ausgibt und auf eine Benutzereingabe wartet. Nur soll jetzt eine Menge der erlaubten Zeichen als Parameter übergeben werden. Die Prozedur soll solange Zeichen einlesen, bis die Taste in der erlaubten Zeichen enthalten ist. Geben Sie die Eingabe - wenn möglich - als Großbuchstabe in einem CHAR-Ergebnis zurück.