Grundsätzliches zur Entwicklung von Programmiersprachen

Seite 3: Paradigmen

Inhaltsverzeichnis

Immer bestimmt die Anwendungslandschaft die Wichtigkeit von Eigenschaften. Damit stellt sich die Frage, wie man seine Aufgaben zu lösen gedenkt. Auch wenn das imperative Paradigma gefällig ist, um Abläufe zu programmieren, hilft die Objektorientierung doch dabei, diese Programme besser zu strukturieren und in eigenverantwortliche Objekte zu gruppieren. Die Objektorientierung hat aber ebenfalls nicht immer gehalten, was sich der eine oder andere davon versprochen haben mag. Die Welt in Objekten zu sehen ist sicherlich manchmal hilfreich, hat aber auch gravierende Nachteile. Da die Objekte auf einem Zustand operieren, treten nämlich Nebeneffekte auf, die im Zusammenhang mit nebenläufigen Anwendungen zunehmend problematischer werden.

In der funktionalen Programmierung tauchen solche Nebeneffekte im Gegensatz dazu nie auf, da Funktionen definitionsgemäß für gleiche Eingabewerte immer das exakt gleiche Ergebnis liefern. Und das sogar unabhängig davon, in welcher Reihenfolge man die Eingabewerte berechnet – also auch wenn sie gleichzeitig berechnet werden. So hat die funktionale Programmierung mit Scala oder F# auf den modernen Plattformen eine Renaissance erlebt, die auch den "Oldies" wie Haskell oder Lisp wieder zu neuen Ehren verhilft.

Wie erwähnt lassen sich sowohl objektorientierte als auch funktionale Features in anderen Sprachen
formulieren, aber erst mit einer vernünftigen Unterstützung lässt sich das selbstverständlich und effizient umsetzen. Die Wahl des Paradigmas spielt also eine entscheidende Rolle, wie Aufgaben "natürlich" gelöst werden. Viele Probleme lassen sich viel besser deklarativ als imperativ programmieren. Zum Beispiel kann man Grammatiken von Sprachen einfach in Form von Deklarationen definieren. Sicherlich lässt sich das auch oft über Bibliotheken umsetzen, aber dann sind zur Laufzeit Fehlerprüfungen durchzuführen, die normalerweise schon ein Compiler prüfen kann.

Die Liste der zur Auswahl stehenden Programmierparadigmen, fĂĽr die es jeweils mehrere Programmiersprachen gibt, ist auch auszugsweise schon lang:

  • imperatives Paradigma mit strukturierter, prozeduraler und modularer Programmierung und abstrakten Datentypen
  • funktionales Paradigma
  • deklarative Paradigmen mit logischer Programmierung und der Programmierung mit Constraints
  • objektorientiertes Paradigma
  • Programmieren mit Komponenten und Agenten
  • aspektorientierte Programmierung
  • generative Programmierung
  • generische Programmierung
  • Daten- und datenstromorientierte Programmierung

Nur wenige Programmiersprachen bedienen sich dabei tatsächlich mehrerer Paradigmen. Man darf nicht vergessen, dass jedes Paradigma eigene Idiome hat, die erst einmal gelernt werden wollen. Darüber hinaus ist für jede Aufgabe zu überlegen, welches Paradigma am besten geeignet ist. Wer nur einen Hammer hat, für den ist alles ein Nagel, heißt es. Das gilt auch für die konkrete Auswahl der zur Verfügung stehenden Paradigmen. Wer einen ganzen Werkzeugkasten hat, ist dann vielleicht überfordert.

Ungeachtet der Domäne ist zu überlegen, wie sich ein Programm ausführen lässt. Eine Variante ist die Übersetzung des Quelltexts in ein ausführbares Artefakt. Dabei spielt es keine Rolle, ob dieses direkt auf der Hardware laufen kann oder eine virtuelle Maschine (VM) zum Einsatz kommt. Entscheidend ist dabei, dass in der Regel ein expliziter Übersetzungsschritt einzuleiten ist, der nur bei erfolgreicher Durchführung das Artefakt liefert, das dann explizit auszuführen ist.

Der Erfolg vieler Skriptsprachen ist dem Umstand geschuldet, dass deren Interpreter den Editier-und-Ablauf-Zyklus minimieren. Im Zusammenhang mit Webapplikationen sind etwa nur ein paar Dateien in ein Verzeichnis zu kopieren, und schon ist die Anwendung verfügbar. Im Extremfall lässt sich also eine fehlerhafte Datei einfach an Ort und Stelle korrigieren: kein Übersetzen, kein Verpacken, kein Deployment und auch sonst nichts. Sicherlich hat das auch Nachteile, aber die gefühlten Vorteile scheinen zu überwiegen.

Skriptsprachen haben immer einen besonderen Reiz, wenn es schnell gehen muss. Das haben auch die Entwickler von D oder Skala erkannt, denn deren Sprachen lassen sich sowohl in Skriptform nutzen als auch zu ausführbaren Dateien übersetzen. In D verhindert zudem ein geeigneter Caching-Mechanismus, das einmal abgelaufene Dateien – solange sie unverändert sind – erneut "übersetzen" werden.

Darüber hinaus haben viele Skriptsprachen Eigenschaften, die in den "statischen" Sprachen eher seltener zu finden sind. Beispielsweise liefert in Perl der Ausdruck 1 + "1" den Wert 2, denn + arbeitet dort mit Zahlen, und die Zeichenkette "1" lässt sich in eine Zahl konvertieren; 1 + "a" liefert dementsprechend 1. Viele dieser Sprachen dulden auch, dass sich einer Variablen nacheinander Werte unterschiedlicher Typen zuordnen lassen. Ob das sinnvoll ist oder nicht, entscheidet einzig und alleine der Sprachdesigner.

Ein Typsystems soll sicherstellen, dass der Anwender keinen Unfug anstellen kann, den der Sprachdesigner nicht billigt – egal ob beabsichtigt oder aus Versehen. Ein Typsystem kann in der Sprache integriert sein, lässt sich aber genau so gut als externes Werkzeug anbieten. Es bestimmt, wie Typen zugeordnet und interpretiert werden: Typen lassen sich explizit angeben oder aus dem Kontext ermitteln; sie können statisch (zur Übersetzungszeit) oder dynamisch (zur Laufzeit) zugeordnet werden; Typen können streng oder eher schwach sein; Die Gleichheit von Typen lässt sich über den Namen oder die Struktur bestimmen; Typen können von anderen Größen abhängig sein. Die Möglichkeiten kann der Entwickler nach Belieben kombinieren und gegebenenfalls weiterentwickeln (Pluggable Type Systems).

Das Typsystem entscheidet unter anderem darüber, wie viele Fehler und in welchem Detaillierungsgrad zur Übersetzungszeit oder Laufzeit erkannt werden können. Unabhängig vom Typsystem kann man natürlich nicht auf Tests verzichten, aber möglicherweise sind bei einem schwächeren Typsystem bestimmte Tests verstärkt durchzuführen, um Laufzeitfehler in der Produktion zu vermeiden.

Da viele Entwickler erfahrungsgemäß speziell bei redundanter Information schreibfaul sind, ist Typinferenz ein beliebtes Feature. Dabei versucht der Übersetzer oder Interpreter so viel wie möglich eigenständig, über beteiligte Typen zu erfahren. Der Vorteil dabei ist, dass sich bei komplizierten Typen tatsächlich der vollständige Typ ermitteln lässt. Der Nachteil ist der, dass der Leser nicht mehr lesen kann, um welchen Typ es sich handelt; aber da helfen einem ja die Entwicklungsumgebungen.

Apropos redundante Information: In vielen Sprachen muss man gelegentlich explizit einen Typ zuweisen, damit der Compiler glĂĽcklich und zufrieden ist.

if (o instanceof C) {
((C) o).methodOfC();
}

In dem Java-Beispiel ist dem Leser klar, dass nach der instanceof-Prüfung die Variable o vom Typ C ist. Dennoch ist für den Java-Compiler eine Typumwandlung vorzunehmen, sodass sich die Methode aus der Klasse C benutzen lässt. Das ist aus Sicht der neueren Sprachen einfach nicht mehr nötig und erlaubt

if (o instanceof C) {
o.methodOfC();
}

zu schreiben. Denn der Compiler, der meist ohnehin eine Flussanalyse durchfĂĽhren muss, kann den Typ von o sicher ermitteln und so den Aufruf ohne explizite Typumwandlung erlauben.

Wie auch immer das Typsystem aussehen mag, es beantwortet nicht automatisch die Frage, welche Typen mit der Sprache ausgeliefert werden sollen. Typischerweise enthält eine Sprache immer ganze Zahlen mit den typischen Operationen. Aber ob Fließkommazahlen, komplexe Zahlen, Brüche, Vektoren, Matrizen et cetera nötig sind, ist schon fraglich. An der Stelle ist also immer zu entscheiden, was in die Sprache kommt und was in die Bibliothek – vorausgesetzt, dass es überhaupt einen Mechanismus zur Erweiterung des Funktionsumfangs gibt.