Textkritik wird eine Serie, in der ich Pro und Contra diverser
Programmiersprachen (aus meiner Sicht) beleuchte. Und damit das auch
nur ein wenig glaubwürdig bleibt, fange ich am besten mit meinem
"dokumentierten Favoriten" an, Forth. Da es nicht bloß eine C-Variation
ist, ist eine Beschreibung der Eigenschaften wohl gerechtfertigt.
Forth ist eine Sprache, die sich in den frühen 70ern entwickelt
hat, und zur Klasse der "Concatenative Languages" gehört (die erst
später definiert wurde). Das bedeutet, man kann den Programmfluss an
(fast) jeder Stelle aufteilen in neue Funktionen ("Worte" in
Forthterminologie), die man dann zusammensetzt, ohne dass sich das
Verhalten ändert - eine sehr schöne Eigenschaft für Refactoring. (Zum
Vergleich: "int a; a=5*rand();" einfach aufzuteilen würde scheitern:
"int a;" "a=5*rand();" verliert die Deklaration im zweiten Teil, "int
a; a=5" "*rand()" ist kompletter Murks)
Die Sprache ist
entstanden zu einer Zeit, als es noch keine riesige Betriebssystem- und
Bibliotheksinfrastruktur gab. Entsprechend bietet Forth seine eigene
Systemabstraktion, auch wenn diese sehr dünn ausfällt. Darauf aufbauend
besitzt ein typisches Forth ein Interpreter/Compiler-Gespann, dass sich
die Bälle zuspielt: Es startet üblicherweise im Interpreter (sofern man
nicht direkt in ein anderes Programm springen will), der bei Bedarf den
Compiler aktiviert. Entwicklung ist dadurch "live", die meisten
Befehlssequenzen lassen sich erst am Interpreter testen und bei Erfolg
zu Worten (=Funktionen) compilen, die in der weiteren Entwicklung
verwendet werden können. Auf diese Weise kann gerade Hardwarezugriff sehr schön experimentell getestet werden.
Die Übergabe von Argumenten findet über
einen Stack statt - ein Wort hat also keine Definition "soviele
Argumente nehme ich (vom Typ X), und soviel gebe ich zurück", es wird
stattdessen eine globale Datenstruktur eingesetzt.
Womit ich bei den Problemen wäre:
- (zur Syntax: ": foo ( n -- n | n n) bar baz ;" definiert ein Wort
"foo", dass der Ausführung von "bar" und "baz" entspricht. "( n -- n
n)" ist ein optionaler Kommentar, der üblicherweise angibt, dass foo ein
Element auf dem Stack verbraucht und eins oder zwei zurückgibt.)
- Die Anzahl der Rückgabe-Elemente ist also nicht eindeutig festgelegt, ebensowenig wie die Eingabemenge, was sehr sinnvoll genutzt werden kann (und wird!), wodurch die Einarbeitung in fremden Code aber recht spannend sein kann.
: send-packet ( len data -- result true | false )
0 0 2 PKT_DATA send-phy-packet ;
Was bedeutet das? So wirklich überschaubar ist der Datenfluss nicht (in
diesem Fall sieht es wohl so aus, dass send-packet im Prinzip ein
Wrapper um send-phy-packet ist, der einige konstante Parameter
hinzufügt, und dessen Ergebnis einfach durchreicht). Was zum zweiten
Problem führt:- Datenstrukturen sind bei Forth nicht sonderlich üblich. Daten
werden auf dem Stack übergeben. Und wenn 6 Elemente benötigt werden, um
eine Situation zu beschreiben, liegen halt 6 (anonyme) Elemente auf dem
Stack. Und das ist manchmal eine ziemlich aufwendige Datenschieberei,
um an die einzelnen Elemente zu kommen (idealerweise arbeitet man die
von hinten nach vorn ab, bzw. ordnet die Daten so an, dass das möglich
ist).
In anderen Sprachen wäre hier vermutlich schon eine Datenstruktur zum Einsatz gekommen, die dem ganzen die richtigen Namen gibt.
Natürlich lässt sich das auch in Forth namentlich erledigen, etwa indem Worte konstruiert werden, die einem einen "sinnvollen" Zugriff auf Stackelemente geben (": pkt.len 5 pick ;" - also "wähle das 5-letzte Element vom Stack und kopiere es oben auf den Stack"), was allerdings zu Lasten der Effizienz gehen kann - also eine Abwägung zwischen Effizienz und Lesbarkeit.
Eine andere Möglichkeit wäre, derartige Datenstrukturen außerhalb des Stacks vorzuhalten. Das wiederum bedeutet, jedes Element explizit anzufordern, auf dem Stack zu bearbeiten und in die Datenstruktur zurückzuschreiben, was weder die Effizienz, noch die Lesbarkeit fördert.
Möglichkeit Nummer 3 ist schließlich, solche Datenkolosse gar nicht erst aufkommen zu lassen - was nicht immer möglich ist, zum Beispiel wenn man vorhandene Hardware ansteuern soll (das obige Beispiel ist eine Adaption aus einem USB-Treiber).