Grundsätzliches zur Entwicklung von Programmiersprachen

Eine Programmiersprache zu entwickeln ist nicht trivial. Das liegt weniger an der technischen Herausforderung, es liegt viel mehr daran, dass man sich nur zu gut überlegen muss, wie diese Sprache aussehen soll und welche Ziele man damit zu erreichen gedenkt. Denn es gilt, allzu viele Entscheidungen zu treffen, die das Potenzial einer Sprache signifikant beeinflussen können.

vorlesen Druckansicht 33 Kommentare lesen
Lesezeit: 28 Min.
Von
  • Michael Wiedeking
Inhaltsverzeichnis

Eine Programmiersprache zu entwickeln ist nicht trivial. Das liegt weniger an der technischen Herausforderung, denn nie zuvor war es einfacher, sich mit populären Werkzeuge einen Parser generieren zu lassen. Es liegt viel mehr daran, dass man sich nur zu gut überlegen muss, wie diese Sprache aussehen soll und welche Ziele man damit zu erreichen gedenkt. Denn es gilt, allzu viele Entscheidungen zu treffen, die das Potenzial einer Sprache signifikant beeinflussen können.

Wer sich daran macht – aus welchen Gründen auch immer –, eine eigene Programmiersprache zu entwickeln, muss eines sicherlich mitbringen: Er oder sie muss Entscheidungen treffen und mit Kompromissen leben können. Eine Programmiersprache ist nämlich das Fundament aller darauf aufbauenden Programme. Das ist durchaus vergleichbar mit unserer Sprache, von der Philosoph Ludwig Wittgenstein sagt: "Die Grenzen meiner Sprache bedeuten die Grenzen meiner Welt." Zum Beispiel gestaltet es sich relativ schwierig, Objektorientierung in einer nicht objektorientierten Sprache umzusetzen. Sicherlich gibt es Mittel und Wege, das zu tun, aber es ist dann womöglich nicht effizient genug.

Dem scheint zu widersprechen, dass die ersten Übersetzer für C++ als Präprozessoren für C-Compiler realisiert wurden. Das heißt tatsächlich, dass C (mit seinen Funktionszeigern, funktionsübergreifenden Sprüngen et cetera) sehr wohl für die Implementierung der Objektorientierung geeignet ist, aber es wäre nie jemand auf die Idee gekommen, diese freiwillig einzusetzen, weil C einen nun wirklich nicht dazu einlädt. Alles was eine Sprache nicht beherrscht, lässt sich also entweder gar nicht oder nur mit enormem Aufwand für bestimmte Aufgaben beschreiben. Eine Programmiersprache tut also gut daran, die Zieldomäne mit eigenem Vokabular und Idiomen zu unterstützen. Je spezieller die Domäne ist, desto mehr kann man sich dabei deren Vokabular bedienen; je weiter sie gefasst ist, desto allgemeiner muss auch das Vokabular sein.

Aber egal, ob der Sprachentwickler eine Domain Specific Language (DSL) für ein konkretes Problemfeld schreiben will oder eher eine General Purpose Language (GPL) schaffen möchte, mit der sich viele Anforderungen bewältigen lassen, für alle gelten zunächst die gleichen Anforderungen: Der Entwickler muss wissen, was er der Sprache abverlangen können will.

Zu den am einfachsten und gleichzeitig schwierigsten festzulegenden Eigenschaften einer Sprache gehört die Syntax. Einfach sind diesbezügliche Entscheidung deswegen, weil man "nur" die äußere Form der Sprache bestimmen muss: Bezeichner, Schlüsselwörter, Satzzeichen, Operatoren et cetera. Schwierig wird das Aussehen dadurch, dass es neben den technischen Aspekten nur eine Frage des Geschmacks ist, für was man sich entscheidet. Leider scheint bei Geschmacksfragen jeder eine eigene Meinung zu haben, was immer wieder zu völlig überflüssigen Streitereien führt.

Rein technisch gesehen gibt es schon bei der Wahl der Zeichen schier unbegrenzte Optionen. Aber obwohl es dafür den Unicode gibt, nutzen ihn nicht viele. Damit sind in unseren Breiten neben dem normalen Alphabet nicht nur die Umlaute zulässig, sondern auch Akzente wie beim Vektorpfeil. Dass sie aber kaum Anwendung finden, mag vielerlei Gründe haben, wobei einer davon sicherlich der ist, dass die meisten Anwender erst gar nicht wissen, wie man diese Sonderzeichen eingeben kann.

Java hat hier Federführendes geleistet, indem die Sprache den Unicode nicht nur für den Programmtext akzeptierte. Leider wurde dabei versäumt, das Dateiformat für Textdateien eindeutig festzulegen, sodass es gelegentlich zu Fehlern bei der Interpretation der Zeichen kommt. Darüber hinaus taucht das Problem auf, dass eine Java-Klasse eine korrespondierende Klassendatei gleichen Namens hat. Benutzt man etwa einen Umlaut in einem Klassennamen, mag das zwar noch funktionieren, sobald man aber diese Klassendateien auf ein anderes Betriebssystem kopiert, kann es passieren, dass eben diese Datei unauffindbar ist.

Da jeder spätestens ab dem ersten Schuljahr mit Mathematik konfrontiert wird, bietet es sich an, für alle mathematischen Aufgaben eine vergleichbare Notation zu wählen. So wird aus der Multiplikation a · b eben a * b, weil sich der Asteriskus zum einen auf der Tastatur findet und zum anderen eine gewisse Ähnlichkeit zum Multiplikationspunkt hat.

Bei den Vergleichsoperation tut man sich bereits etwas schwerer, etwas Passendes auf der Tastatur zu finden. Aus <, ≤ und ≥ wird mal <, <=, > beziehungsweise >=, was vielleicht noch eingängig erscheinen mag. Bei Vergleichen mit = und ≠ tut man sich schon etwas schwerer, und so finden sich für die Ungleichheit die Varianten !=, /=, |=, =/= oder <>; was immer die Tastatur und der ASCII-Code hergeben.

Das größte Problem scheint aber die Zuweisung in imperativen Sprachen zu sein. Besonders Anfänger tun sich schwer damit, dass das, was sie als Vergleich kennen (a = b), in sich von C ableitenden Sprachen als Zuweisung gilt und der Vergleich selbst seltsam anmutend mit zwei Gleichzeichen geschrieben wird (a == b). Sprachen mit Algol-Herkunft wie Pascal verwenden deshalb zur Unterscheidung für die Zuweisung :=. Fortran nutzt für die Zuweisung zwar auch das Gleichzeichen (a = b), für alle Vergleiche kommen dafür aber nur Namen zum Einsatz (a .EQ. b oder a .LT. b).

Ohnehin stellt sich die Frage, ob man lieber mit Namen oder Operatoren arbeitet. Beispielsweise kann man sich unter a mod b vielleicht noch etwas vorstellen, derweil es mit a % b fast unmöglich scheint. Mit Ausnahme der Standardoperatoren +, -, · und / tut man sich schon recht schwer mit den Symbolen. In Scala etwa gibt es die Operatoren /: und :\. Wie man gleich sieht, handelt es sich dabei um die beiden Funktionen foldLeft und foldRight. Die imperativen Programmierer können sich vielleicht unter den gesprächigeren Varianten nichts vorstellen, aber die funktionalen Entwickler wissen sehr wohl, was damit gemeint ist.

Ein spezielles Thema im Zusammenhang mit der Syntax ist das Format. Die meisten Sprachen sind formatfrei. Das heißt, dass nichts dagegen spricht, alles ohne Leerzeichen in eine einzige Zeile zu schreiben. Unter den aktuellen Sprachen verlangt einzig Python, dass ein Programm vernünftig gesetzt wird – zumindest was das Einrücken betrifft.

def greet(name):
print 'Hallo', name

greet('Python')

Die vorgegebene Syntax entscheidet also auch darüber, wie konsistent das Erscheinungsbild der Quelltexte ist. Beispielsweise ist es in Perl den Variablen $v, @a, %m und &f sofort anzusehen, dass es sich um ein Skalar, eine Liste, eine Hash-Map beziehungsweise eine Funktion handelt. Oder während sich in Java Instanzvariablen optional durch ein this anführen (this.v) und Klassenvariablen sowohl über die Klasse direkt (C.v) als auch über jedes Objekt dieser Klasse (o.v) ansprechen lassen, beginnen in Ruby Instanzvariablen immer mit einem "Klammeraffen" (@v), derweil statische Klassenvariablen mit zwei davon beginnen (@@v).

Bezüglich der Syntax gehen die Meinungen also weit auseinander. Für die einen ist es das A und O einer Sprache, für andere nur Tinnef. Die Syntax entscheidet allerdings darüber, ob sich ein Programm einfach lesen lässt oder nicht. Und nachdem man Programmen nachsagt, sie werden deutlich öfter gelesen als geschrieben, scheint eine sorgfältige Wahl der Syntax durchaus sinnvoll. Allerdings wären mehrere tausend Mannjahre an Diskussionen darüber erspart geblieben, wohin denn die geschweiften Klammern zu setzen sind, wenn sich die Herren Kernighan und Ritchie nicht nur dazu durchgerungen hätten, die Klammern wie im C-Buch nach oben zu setzen, sondern auch den Compiler dazu motiviert hätten, das zu überprüfen und zu erzwingen.

Mehr Infos

Grammatikalische Spitzfindigkeiten

Selbst wenn alle Ungereimtheiten auf lexikalischer Ebene gelöst sind, kann es immer noch welche auf syntaktischer Ebene geben. So enthält die C#-Spezifikation das folgende Beispiel, das diese Probleme schön demonstriert:

F(G<A, B>(7));

Bei dem Ausdruck kann es sich um die Funktion F handeln, die zwei boolesche Parameter hat, die mit den Argumenten G < A und B > (7) aufgerufen wird. Genauso gut kann es aber auch die Funktion F sein, die nur einen Parameter hat, bei dem es sich um die generische Funktion G<A, B> mit den beiden Typ-Parametern A und B handelt, die einen einzelnen Parameter hat. Die Regeln in C# sagen tatsächlich, dass Letzteres der Fall ist.

F(G<A, B>7);
F(G<A, B>>7);

Variiert man die Ausdrücke und eliminiert beispielsweise die runden Klammern, handelt es sich tatsächlich um eine zweistellige Funktion F mit den Argumenten G < A und B > 7, was auch im zweiten Fall mit B >> 7 gilt.

Ein anderes Beispiel ist, bei dem es sich auch um nichts Generisches handelt, da hier die Leerzeichen ebenso wenig eine Rolle spielen:

x = F<A> + y

Hier sagt die Spezifikation, dass es sich um die Vergleichsoperatoren handelt und der Ausdruck so ausgewertet wird, als stĂĽnde dort:

x = (F < A) > (+y)

An dieser Stelle zeigt sich also, wie schwierig eine Grammatik werden kann, wenn Zeichen beziehungsweise Token für völlig unterschiedliche Bedeutungen wiederverwendet werden.