Forth – die ewig junge Programmiersprache
Forth ist so ungewöhnlich, dass Entwickler beim Versuch, ihr Wissen über Programmiersprachen auf Forth anzuwenden, sich oft irritiert abwenden. Das ist schade.
- Carsten Strotmann
Forth ist eine "Concatenative"-Programmiersprache. Das heißt, es gibt nur minimale Syntaxregeln, und Befehle beziehungsweise Funktionen (im Forth-Sprachgebrauch "Wörter") werden mit Leerzeichen getrennt hintereinander geschrieben. Alle Bestandteile, die in populären Programmiersprachen durch Syntax umgesetzt sind (Kommentare, Variablendefinitionen, Kontrollstrukturen wie Schleifen oder Entscheidungen mit IF-THEN-ELSE) sind unter Forth ganz normale "Wörter".
Im Umkehrschluss bedeutet das, dass Programmierer "ihr" Forth selbst in beliebiger Weise anpassen können. Forth ist wie ein Baukasten für eigene Programmiersprachen, die es Programmierern und Programmiererinnen erlauben, möglichst elegant die gewünschte Anwendung zu implementieren.
Forth zum Anschauen
Der Forth-Quellcode ist gewöhnungsbedürftig. Die Struktur wird nicht von der Sprache vorgegeben, es ist Programmierern überlassen, den Quellcode lesbar zu formatieren. Da in den ersten Jahren von Forth auch der Platz für Quellcode auf Speichermedien stark begrenzt war, hatte sich Forth leider den Ruf einer Write-only-Programmiersprache zugezogen. Es braucht ein wenig Disziplin, um schön lesbare Forth-Programme zu schreiben.
Um ein Gefühl für Forth zu bekommen, wird im nachfolgenden Auschnitt ein Teil des Forth-Quellcodes aus dem "Tetris for terminals" des GNU/Forth (Datei tt.fs) näher erklärt. Der Code implementiert einen Pseudo-Zufallszahlengenerator (Pseudo-Random-Number-Generator oder PRNG), der im Tetris-Spiel zum Einsatz kommt. Im Quellcode werden bestehende Forth-Wörter wie dup
, +
und !
zu neuen Wörtern (Funktionen, Unterroutinen) zusammengesetzt:
\ stupid random number generator
variable seed
: randomize time&date + + + + + seed ! ;
$10450405 constant generator
: rnd ( -- n ) seed @ generator um* drop 1+ dup seed ! ;
Die erste Zeile ist ein Kommentar, eingeleitet mit dem Forth-Wort \
. Dabei ist \
keine Syntax, wie man es von anderen Programmiersprachen kennt, sondern ein Forth-Wort (eine Funktion), die beim Lesen des Quelltexts direkt ausgeführt wird. Diese Funktion liest alle Zeichen im Quelltext bis zum Ende der Zeile und verwirft diese Zeichen. Damit ist der Kommentartext für das Forth-System nicht mehr sichtbar, und die Funktion eines Kommentarzeichens ist implementiert. Haben Programmierer eigene Vorstellungen, welches Zeichen einen Kommentar einleiten sollte, fügen sie einfach dem Forth-System ein eigenes, neues Kommentarwort hinzu.
Die Zeile variable seed
ist einfach zu verstehen. Sie erzeugt eine Variable. Eine Forth-Variable kann einen numerischen Wert speichern, welcher der nativen Datenbus-Größe der CPU entspricht. Also 16, 32, 64 oder im Fall von RISC-V auch schon 128 Bit. Dabei ist das Forth-Wort variable
keine spezielle Syntax, sondern nur eine in Forth geschriebene Funktion, die Speicher reserviert und mit dem angegebenen Namen verbindet. Der Name der Variable, hier seed
, ist danach ein neues Forth-Wort, das bei der Ausführung die Adresse der Variable auf den Datenstapel (Data-Stack) legt (hierzu später mehr). Diese Variable ist bei den meisten Forth-Systemen schon die einzige Datenstruktur, die das System mitliefert. Arrays, Structs, Objekte und andere Datenstrukturen erzeugen Forth-Programmierer über eigene, individuelle Forth-Wörter. Das Erzeugen eigener Datenstrukturen in Forth ist nicht schwer, und auch bei populären Programmiersprachen sind komplexere Datenstrukturen oft selbst zu erstellen. Also warum nicht gleich von Anfang an.
Das Forth-Wort :
(Colon) aktiviert den Forth-Compiler. Alle nachfolgenden Wörter, bis zum abschließenden Forth-Wort ;
(semis), werden nicht direkt ausgeführt, sondern in ein neues Forth-Wort einkompiliert. Der Name (oder Bezeichner) des neuen Worts folgt direkt auf :
.
Dieses neue Forth-Wort hat die Aufgabe, den Zufallszahlengenerator für das Tetris-Spiel zu initialisieren (der hier gezeigte Zufallszahlengenerator ist sehr schlicht, reicht für Spiele, aber sollte nicht für sicherheitskritische Anwendungen benutzt werden).
Das Forth-Wort time&date
legt sechs Werte (Sekunde, Minute, Stunde, Tag, Monat, Jahr) auf den Stack des Forth-Systems ab. In einem Forth-System werden alle Ein- und Ausgabeparameter für Funktionen über einen Daten-Stack implizit übergeben. "Implizit" heißt hier, dass die Anzahl der Parameter bei der Definition des Forth-Worts, und auch beim Aufruf, nicht angegeben werden. Der Stack ist eine eingebaute LIFO-Datenstruktur (Last-In – First-Out) eines jeden Forth-Systems. Es erfordert ein wenig Übung, die Bewegung der Parameter auf dem Datenstapel mitzuverfolgen. Anfänger können sich mit ausführlichen Kommentaren helfen. Die Anzahl der Parameter ist im Quelltext nicht erkennbar.
: randomize
time&date
+ + + + +
seed !
;
Die nachfolgenden Additionszeichen +
sind Forth-Wörter, deren Funktion es ist, die jeweils oberen zwei Elemente auf dem Stack miteinander zu addieren und das Ergebnis auf den Stapel zurückzulegen. Forth bedient sich bei mathematischen Funktionen der Postfix-Schreibweise. Dabei werden erst die Werte auf den Stack gelegt und danach die mathematische Funktion aufgerufen, welche die Werte von Stack "konsumiert" und die Ergebnisse wieder auf dem Stack hinterlässt.
Hier wird aus dem aktuellen Datum und der aktuellen Uhrzeit Uhrzeit ein Startwert (Seed) für den Zufallszahlengenerator ermittelt. Das Wort zur Variablen seed
legt die eigene Speicheradresse auf den Stack, und das nachfolgende Wort !
, ausgesprochen "Store", speichert den errechneten Wert in die Speicherstelle der Variable seed
. Dabei ist anzumerken, das !
nicht nur in Variablen schreiben kann, sondern über entsprechende Eingabewerte auf dem Stack in beliebige Speicherbereiche schreibt.
Die folgende Zeile ist wieder einfacher zu verstehen:
$10450405 constant generator
Sie erzeugt ein neues Forth-Wort mit dem Namen generator
. Es ist eine numerische Konstante, die mit dem Wert Hexadezimal 10450405 belegt ist. Wird dieses neue Forth-Wort im weiteren Programm benutzt, legt es diesen Wert auf den Stack.
In der letzten Zeile des kleinen Beispiels wird nun ein Forth-Wort erzeugt, das bei der Ausführung eine neue Zufallszahl erzeugt und diese auf dem Stack hinterlässt:
: rnd ( -- n ) \ erzeuge Zufallszahl
seed @ \ lade aktuelle Saat des PRNG
generator um* drop 1+ \ errechne Zufallszahl
dup \ Zahl verdoppeln
seed ! \ und eine Kopie als neue Saat speichern
;
Colon (:
) schaltet wieder den Compiler an, und rnd
ist der Name des neuen hier definierten Forth-Worts. Die Klammern (
und )
stellen eine weitere Form eines Kommentars dar (wobei (
ein Forth-Wort ist, das alle Zeichen im Quelltext bis zum Zeichen )
konsumiert, verwirft und damit dem Forth-System vorenthält). Das ist ein Stack-Kommentar, der als Dokumentation für Programmierer dient und beschreibt, welche Eingabe- und Ausgabeparameter diese Funktion erwartet beziehungsweise erzeugt. In dem Fall wird eine natürliche (Zufalls-)Zahl n
nach dem Aufruf von rnd
auf dem Stack hinterlassen.
Ab seed
beginnt der Körper der neuen Funktion. seed
als Variable von oben legt die eigene Speicheradresse auf den Stack. Das Forth-Wort @
(gesprochen "fetch") ist das Gegenstück zu !
(store) und konsumiert die Adresse als obersten Eintrag auf dem Stapel und legt als Ergebnis den an der Adresse im Hauptspeicher gespeicherten Wert zurück auf den Stack.
Das Forth-Wort generator
(eine numerische Konstante) legt den Wert Hexadezimal 10450405 auf den Stack, welches das Wort um*
(unsigned double multiply "u-m-star") mit dem Wert aus seed
multipliziert und als doppelt-genaue Zahl auf dem Stack hinterlässt.
Das Wort drop
verwirft den oberen Wert des Stacks, hier den höherwertigen Teil der doppelt-genauen Zahl (der Zufall spielt sich im unteren Bereich der Zahl ab). Dieses Ergebnis, der untere Teil der doppelt-genauen Zahl, wird mit dem Wort 1+
um eins erhöht und dann mit dem Wort dup
dupliziert. dup
liest den obersten Eintrag auf dem Datenstapel und legt eine Kopie davon auf ebenjenen. Die Kopie des Werts wird nun als neue Saat in die Variable seed
gespeichert, während der originale Wert auf dem Stack verbleibt und als Ergebnis der Funktion auf weitere Verwendung im Programm wartet. Das Wort ;
(semicolon) schließt die Definition des neuen Forth-Worts rng
ab, schaltet den Forth-Compiler aus und übergibt die Kontrolle wieder an das interaktive Forth-System.