Was ist eine Programmiersprache?
Eine
Programmiersprache
ist eine Sprache zur Formulierung von Rechenvorschriften wie
Algorithmen,
die von einem Computer ausgeführt werden können.
Programmiersprachen bilden die wichtigste Schnittstelle zwischen Benutzern und Computern.
Jeder Benutzer muss die Bearbeitung von Problemen, die einem Computer übergeben werden sollen,
in einer Programmiersprache formulieren.
[INFODUDEN]
Textuelle Notationen zur Beschreibung von Algorithmen nennt man Programmiersprachen.
[INFO94 S. 75]
Low−Level−Programmiersprachen
Bei
Low−Level−Sprachen
muss der Programmierer die Architektur und Schnittstellen sowie die Funktionsweise des Rechners mehr oder weniger genau kennen.
Diese Sprachen abstrahieren nur schwach von der Rechenmaschine selbst.
Der Übergang zu
High−Level−Sprachen
ist jedoch fließend.
High−Level−Programmiersprachen
… oder höhere Programmiersprachen oder auch Computerhochsprachen liegen von der Abstraktion her weit über der Maschine.
Zusätzliche Fähigkeiten kommen mit höheren Operationen hinzu,
wobei viele Operationen, aber auch Eigenschaften, welche rechnerspezifisch sind, dem Entwickler verborgen bleiben.
Manche Sprachen befinden sich auf einer so hohen Abstraktionsstufe,
dass sie scheinbar nichts mehr mit einer konkreten Rechenmaschine zu tun haben.
Andere High-Level-Sprachen bieten dem Entwickler aber auch die Möglichkeit,
Programmfragmente in einer Low-Level-Sprache zu formulieren.
Mensch vs. Computer
„Allem in alles unsere Gehirn machen falsche Sätze richtig.”
Claudius Stauffenberg
Ein Computer hätte aber mit falschen Sätzen echte Probleme.
Die Sprache der Computer ist im Allgemeinen sehr viel einfacher als die menschliche
und erfordert zudem absolute syntaktische Korrektheit.
Sie verfügt über einen weitaus geringeren Wortschatz
(Lexeme),
ihre Grammatik
(Syntax)
wird kaum von Ausnahmen belastet,
und es gibt keine Mehrdeutigkeiten.
Der Mensch,
der eine natürliche Sprache,
die bestimmten Grammatikregeln,
die
– nebenbei bemerkt –
von Menschen für Menschen gemacht wurden
und die Bildung beliebig langer Sätze erlauben,
folgt,
spricht,
versteht seltsamerweise bestimmte Sätze,
die nicht nur grammatikalisch bzw. syntaktisch korrekt sind,
sondern auch inhaltlich keine Fehler,
das heißt,
Sprecher und Hörer gehen vom selben Zusammenhang,
der üblicherweise als Kontext bezeichnet wird,
aus,
enthalten,
aber wie der vorliegende Satz,
der hier als Beispiel eines besonders langen Satzes dient,
extrem lang und somit unverständlich sind,
nicht.
:−(
- Der Mensch,
- der eine natürliche Sprache,
- die bestimmten Grammatikregeln,
- die
– nebenbei bemerkt –
von Menschen für Menschen gemacht wurden
- und die Bildung beliebig langer Sätze erlauben,
folgt,
spricht,
versteht seltsamerweise bestimmte Sätze,
- die nicht nur grammatikalisch bzw. syntaktisch korrekt sind,
- sondern auch inhaltlich keine Fehler,
- das heißt,
- Sprecher und Hörer gehen vom selben Zusammenhang,
- der üblicherweise als Kontext bezeichnet wird,
aus,
enthalten,
- aber wie der vorliegende Satz,
- der hier als Beispiel eines besonders langen Satzes dient,
extrem lang und somit unverständlich sind,
nicht.
:−)
Auch wenn in diesem Satzbeispiel ungeheure Defizite in der Pragmatik zu verzeichnen sind,
so konnte ich Ihnen doch hoffentlich eine offensichtliche Stärke des Computers näherbringen:
Ihm bereiten lange Satzkonstruktionen,
wenn sie grammatikalisch korrekt sind, im Prinzip nicht mehr Probleme als kurze.
Der
Computer
ist eine universell einsetzbare Rechenmaschine, die im Allgemeinen nur eine endliche Folge von Befehlen
(Programm)
abarbeiten kann.
Ihm fehlt Einfühlungsvermögen und die Motivation etwas zu verstehen.
Der
Mensch
hingegen ist ein kommunikatives und
„frei”
denkendes Lebewesen mit Motiven, Emotionen und Bewusstsein.
Seine Fähigkeit
Ideen zu bilden,
Ideen zu präsentieren,
Ideen zu verstehen und
Ideen zu verwirklichen,
ist die Voraussetzung dafür,
Probleme zu lösen oder
etwas Neues zu schaffen.
Dies ist dem Computer leider nicht gegeben, es sei denn, man bringt es ihm bei.
Die menschlichen Ideen können verschiedenster Art sein.
Sie müssen nicht einmal mit den Gegebenheiten der Wirklichkeit verträglich sein,
zum Beispiel können einzelne Bestandteile eines Traumes als Ideen aufgefasst werden.
Meine
Tochter
träumt des öfteren, sie könne zaubern
– was leider nicht ganz der Wirklichkeit entspricht, sonst gäbe es eine bessere Welt.
Ich wiederum liebe besonders die Träume, in denen ich federleicht bin und unendlich weit springen kann.
Und sicherlich haben Sie auch schon ähnliche Ideen geträumt.
So manche Idee kann mithilfe eines Computers realisiert werden.
Die Voraussetzung für eine Idee ist erst gegeben, wenn es eine Welt gibt, die sich in einem bestimmten Zustand
(Ausgangssituation)
befindet.
Eine Idee könnte diese Welt derart verändern, dass sie in einen gewünschten Zielzustand gerät
zum Beispiel durch
Planen im Situationsraum.
Dabei sei erwähnt, dass die Welt nur modellhaft
(d. h. nicht exakt der Wirklichkeit entsprechend)
dem Computer erklärt werden kann.
Man spricht auch nicht von einer zu realisierenden Idee, sondern von einem zu lösenden Problem.
Wenn nun der Mensch mithilfe eines Computers ein Problem lösen möchte,
dann muss er dem Computer in dessen Sprache
(Programmiersprache)
das Problem bzw. eine Lösungsvorschrift zum Problem
(Algorithmus)
formulieren, wobei das Weltwissen nicht fehlen darf
(meist inhärent vorhanden).
Dies ist die einzige Möglichkeit, dem Computer eine Aufgabe zu übertragen.
Dabei ist Vorsicht angesagt, denn nicht jede Aufgabe führt bei Ausführung in endlicher Zeit zum Resultat,
d. h. nicht jede Idee kann mit einem Computer realisiert werden.
Der Computer kann jedenfalls dazu benutzt werden,
so ziemlich jede
„intuitiv berechenbare Funktion”
(vielleicht Ihre Idee)
zu simulieren bzw. auszuführen,
wenn diese in einer dafür geeigneten Programmiersprache formuliert wird.
Was allerdings in der menschlichen Sprache mit ein paar wenigen Worten erklärt werden kann,
muss bei einer Programmiersprache schon um einiges genauer umschrieben werden.
Auch das, was sozusagen zwischen den Zeilen steht, muss dem Computer explizit beschrieben werden.
Wenn Sie in Ihrer eigenen bzw. in einer speziellen Sprache ein Programm für den Computer schreiben möchten,
dann müssen Sie auch über eine entsprechende Programmiersprache verfügen, die Sie sich durchaus auch selbst entwicklen können.
Wie entwickle ich eine eigene Programmiersprache?
Diese Frage kann leider nicht mit einem Satz beantwortet werden.
Wer sich dahingehend näher informieren möchte, sollte sich mit dem
Bau eines Compilers
(Compilerbau, Übersetzerbau)
auseinandersetzen.
Ein
Compiler
übersetzt nämlich Texte einer Ausgangssprache
— vielleicht sogar Ihre —
in Texte einer Zielsprache,
welche zur Ausführung auf einer Maschine gebracht werden können.
Wenn Sie eine eigene Programmiersprache entwickeln, sollten Sie auch den dazugehörigen Compiler selbst schreiben
oder diesen in Auftrag geben.
Bevor Sie aber die Entwicklung einer neuen Programmiersprache einleiten, sollten Sie zunächst bedenken, welche Probleme
{Der Begriff "Problem" wird hier als neutraler Fachbegriff verwendet und kann als Synonym für "Aufgabenstellung" angesehen werden.}
Sie damit bewältigen wollen, mit welcher Problemklasse Sie es zu tun haben.
Erst mit dieser Überlegung ist es Ihnen möglich, eine prägnante Programmiersprache zu formulieren, die genau Ihren Zweck erfüllt
(problemorientiert).
Häufig wird die Schönheit einer Programmiersprache an der Kürze ihrer Ausdrücke bemessen
(wenig Code, große Wirkung).
Um dieser Forderung gerecht zu werden,
muss die neu zu entwickelnde Programmiersprache bezüglich der zugrundeliegenden Problemklasse
und auch bezüglich ihrer Lösungsstrategien designed
(konzipiert)
werden.
In den meisten Fällen ist jedoch die Entwicklung einer neuen Programmiersprache gar nicht nötig, denn es gibt schon so viele.
:−)
Warum gibt es so viele Programmiersprachen?
Genügt denn nicht eine einzige Programmiersprache, mit der man alle Arten von Problemen bewältigen kann?
In der Tat, prinzipell käme man mit einer einzigen, so genannten universellen Programmiersprache wie beispielsweise mit
C++
oder mit
Java
aus.
Doch bei der Verwendung einer Programmiersprache wollen Sie sicherlich ohne viel drumherum auf den Punkt kommen,
dies ist jedoch mit universellen Programmiersprachen eher selten der Fall.
Will man
3D-Grafiken
programmieren, nutzt man besser eine Programmiersprache, die dafür ausgelegt ist wie zum Beispiel
POV−Ray.
Für Datenbankabfragen nutzt man normalerweise die dafür spezialisierte Programmiersprache
SQL
und für konsistente Dokumentgenerierung verwendet man
LaTeX.
Spezialisierte Programmiersprachen sind auf bestimmte Problemräume zugeschnitten,
mit dem Ziel spezielle Probleme prägnant und somit kostengünstig lösen zu können.
Sicherlich gibt es einige überflüssige Programmiersprachen,
doch einige andere spezialisierte Programmiersprachen sind aus der Welt der Informatik nicht mehr wegzudenken.
Folgende Begrifflichkeiten legen den Grundwortschatz eines Compilerbauers fest,
gleichwohl um welche
(formale)
Sprache es sich handeln sollte.
Das Alphabet
… oder auch der
Zeichensatz
der menschlichen Sprache
(wohl eher englisch)
umfasst alle Zeichen, die mit einer Tastatur bzw. mit einer Schreibmaschine erzeugt werden können
(Sonderzeichen eingeschlossen).
Das Alphabet der meisten Programmiersprachen entspricht in der Regel dem der englischen Sprache
(echte Teilmenge des ASCII−Zeichensatzes, neuerdings UTF−8).
Es gibt aber auch Programmiersprachen, die beispielsweise nur acht Zeichen verstehen
(wie zum Beispiel Brainfuck, eine Turing-vollständige Sprache).
Die Maschinensprache kennt nur zwei Zeichen, die
0
und die
1,
also ein Alphabet mit nur zwei Zeichen, das Binäralphabet.
Wie kommuniziert man mit einer Maschine, die nur zwei Zeichen versteht?
Der Wortschatz
Möchte der Mensch eine Fremdsprache erlernen, dann lernt er nicht gleich einhunderttausend Vokabeln auswendig.
Um sich in der neuen Sprache verständigen zu können, genügt eigentlich ein Grundwortschatz von etwa 500 Vokabeln.
Unbekannte Begriffe werden umschrieben, der Text nimmt an Umfang zu.
Umgekehrt gilt natürlich: Je größer der Wortschatz, desto prägnanter die Formulierung.
So verhält es sich auch bei Programmiersprachen.
Ein Wort einer Programmiersprache wird als
Lexem
bezeichnet.
Eine
Low−Level−Sprache
kennt etwa 50 Lexeme, wenn man einmal von den Zahlen absieht.
Sie käme aber im Extremfall mit nur einer Handvoll aus;
ein paar Lexeme für Rechenoperationen und ein paar weitere für Speicherzugriffe.
Kennt der Programmierer alle Lexeme der jeweiligen Programmiersprache, kann er den Code zum einen prägnanter formulieren
und zum anderen das Programm bezüglich seiner Ausführungszeit
(Verbesserung der Laufzeit),
bezüglich seines Platzbedarfs während der Ausführung
(Verbesserung des Speicherbedarfs)
optimieren.
Um eine neue Programmiersprache zu erlernen, nimmt man normalerweise zu allererst ein Beispielprogramm namens
„Hello World”
unter die Lupe, um die ersten Lexeme einer Sprache kennen zu lernen.
Im folgenden
„Hello World”−Programm,
welches in PHP verfasst ist, lernt man zum Beispiel mit dem Lexem
echo
die Ausgabe des Lexems
„Hello World!”
kennen.
Mit diesem Programm könnte man schon fast behaupten, ein
PHP−Experte
zu sein.
:−)
Hello World in PHP
<?php echo "Hello World!"; ?> |
Das Token
Das
Lexem
ist eine Zeichenkette, die in der Regel einem Token zugeordnet wird.
Ein
Token
gibt einer Menge von Lexemen lediglich einen Namen.
Im Folgenden sehen Sie ein Beispiel, das vier Lexeme dem Token
KLAMMER_AUF
zuordnet:
KLAMMER_AUF := {'(', '[', '{', '<'}
Oft definiert man ein Lexem mit einem regulären Ausdruck
wie zum Beispiel:
SIMPLER_TYP := 'boolean'|'byte'|'int'|'char'
UND := 'and'|','|'&'|'&&'
INTEGER := ('0'-'9')+
Mit Tokens abstrahiert man von konkreten Zeichenketten,
um beispielsweise Syntaxdefinitionen
(Grammatiken)
übersichtlicher bzw. abstrakter vornehmen zu können.
Grundsätzlich wird versucht, die benötigten Tokens
(Mengen von Lexemen)
disjunkt zu halten,
so dass sich zum Beispiel bei der Konstruktion einer Grammatik keine ungewollten Mehrdeutigkeiten einschleichen.
Wie Tokens nun verwendet werden, können Sie beispielsweise auf vorliegender Website unter
- Setty−Scanner
und
- Tinyray−Scanner
nachlesen.
Die Syntax
Allein mit dem Alphabet und dem Wortschatz einer Sprache ist es noch nicht getan.
Eine Sprache ist geprägt von ihrer
Grammatik
beziehungsweise von ihrer
Syntax,
das gilt für natürliche Sprachen sowie für Computersprachen gleichermaßen.
Die Grammatik verleiht der Sprache Struktur. Die englische Sprache kennt zum Beispiel die
S−P−O−Regelung
(S für Subjekt; P für Prädikat; O für Objekt),
um Sätze zu bilden.
Nur
S−P−O−formulierte
Sätze sind, von ein paar Ausnahmen abgesehen,
grammatikalisch korrekte Sätze der englischen Sprache,
wobei die Struktur eines Satzteiles wiederum von Grammatikregeln vorgegeben wird.
Ohne Grammatik wäre die Umschreibung bzw. die Definition von weiteren Begriffen gar nicht vorstellbar.
Dabei soll aber nicht der Eindruck entstehen, dass grammatikalisch korrekte Sätze Sinn ergeben müssen,
wie dies zum Beispiel bei der Aussage
„Gestern ist morgen.”
in unserer Realität nicht der Fall ist.
Mit einer Grammatik wird lediglich festgelegt, welche Begriffe und Zeichen
(Token)
in welcher Reihenfolge stehen dürfen.
Einfaches Grammatikbeispiel
Grammatik G = (Nichtterminale, Terminale, Produktionen, Start) mit
Nichtterminale = {Objekt, Prädikat, Satz, Subjekt},
Terminale = {' ', '.', 'frisst', 'Hund', 'Katze', 'mag', 'Maus'},
Produktionen = {
Satz ::= Subjekt ' ' Prädikat ' ' Objekt '.',
Subjekt ::= 'Hund' | 'Katze' | 'Maus',
Prädikat ::= 'frisst' | 'mag',
Objekt ::= Subjekt
},
Start = Satz. |
Die Grammatik
G
beschreibt bzw. erkennt simple
S−P−O−Sätze
mit Hund, Katze und Maus.
Es können mit der Grammatik
G
einige sinnvolle, jedoch auch unsinnige Sätze gebildet bzw. erkannt werden.
- Unsinnig, aber syntaktisch korrekt:
Maus frisst Katze.
Maus mag Katze.
- Sinnvoll:
Katze frisst Maus.
Hund frisst Katze.
Maus mag Hund.
Die Grammatik kümmert sich nicht um Sinn oder Unsinn eines Satzes,
sondern nur um die Struktur.
Die Frage nach Sinn bzw. Unsinn eines Satzes gehört der Semantik an.
Die Sprache
Eine Sprache ist lediglich eine Menge von ausgewählten Zeichenketten.
Ein Element dieser Menge wird manchmal als
Wort
(kein Lexem)
bezeichnet.
- Ein Element der deutschen Sprache ist beispielsweise ein deutschsprachiger Brieftext
oder der gesamte Inhalt eines deutschsprachigen Romans oder auch nur ein simpler, deutscher Satz.
- Ein Element einer Programmiersprache ist ein Programm.
- Die Sprache einer Programmiersprache enthält
alle
Programme, der zugrundeliegenden Programmiersprache.
Offensichtlich existieren sehr viele Sprachen, die unendlich viele Elemente enthalten,
also unendlich sind, d. h. nicht regulär sind,
sodass es einem fast schon schwer fallen könnte, eine endliche Sprache anzugeben.
:−)
Als Beispiel einer endlichen Sprache möchte ich Ihnen die von der oben vorgestellten Grammatik
G
induzierte Sprache
L(G)
vorstellen.
L(G) = {
'Hund frisst Hund.', 'Hund frisst Katze.', 'Hund frisst Maus.',
'Katze frisst Hund.', 'Katze frisst Katze.', 'Katze frisst Maus.',
'Maus frisst Hund.', 'Maus frisst Katze.', 'Maus frisst Maus.',
'Hund mag Hund.', 'Hund mag Katze.', 'Hund mag Maus.',
'Katze mag Hund.', 'Katze mag Katze.', 'Katze mag Maus.',
'Maus mag Hund.', 'Maus mag Katze.', 'Maus mag Maus.'
} |
Die Sprache
L(G)
ist endlich
(nur 18 Elemente bzw. 18 Wörter),
überschaubar
und zweifellos eine Teilmenge der deutschen Sprache.
Später werden Sie sehen, wie die Sprache
L(G)
zur Programmiersprache wird.
Wenn eine Sprache von einer Grammatik
induziert
(erzeugt)
wird, so enthält sie genau die Elemente,
die von der zugrunde liegenden Grammatik beschrieben bzw. erkannt
(akzeptiert)
werden.
Von einer Grammatik induzierbare Sprachen sind
rekursiv−aufzählbare
Sprachen.
Abgrenzend muss ich noch klar stellen, dass es auch Sprachen gibt, die nicht von einer Grammatik induziert werden können,
also nicht
rekursiv−aufzählbar
sind.
Es stellt sich mir gerade die philosophische Frage, ob die vollständige deutsche Sprache überhaupt
rekursiv−aufzählbar
ist.
Oder kurz: Kann sich der Mensch mit dem Computer unterhalten?
Programmiersprachen gehören generell zur Klasse der
rekursiv−aufzählbaren
Sprachen.
Der Computer benötigt nämlich zur Interpretation eines Programms eine Grammatik
— Geht es auch ohne Grammatik? —
der zugrundeliegenden Programmiersprache.
Die Klasse der kontextfreien Sprachen, eine Unterklasse der Klasse
rekursiv−aufzählbarer
Sprachen, ist von zentraler Bedeutung für Bauer und Nutzer einer Programmiersprache.
Die Semantik
Mit der
Semantik
wird festgelegt,
welche Bedeutung den syntaktischen Einheiten zukommt.
Was nun ein grammatikalisch korrekter Satz auf dem Rechner bewirken soll,
wird mit der Semantik der jeweiligen Programmiersprache eindeutig festgelegt.
Die Prüfung der Bedeutung eines Satzes, ob Sinn oder Unsinn, wird als
Semantik−Check
bezeichnet.
Einen
Semantik−Check
für eine endliche Sprache zu realisieren, ist im Prinzip sehr einfach, weil es nur endlich viele Elemente zu prüfen gibt.
Man entfernt aus der endlichen Sprache die Elemente, die keinen Sinn ergeben,
und überprüft, ob sich der Input als Element in der Sprachdifferenz befindet.
Als Beispiel sehen Sie die Sprachdifferenz
D
bezüglich der Sprache
L(G).
D = L(G) / {
'Hund frisst Hund.', 'Hund frisst Maus.',
'Katze frisst Hund.', 'Katze frisst Katze.',
'Maus frisst Hund.', 'Maus frisst Katze.', 'Maus frisst Maus.',
'Hund mag Katze.',
'Katze mag Hund.', 'Katze mag Maus.',
'Maus mag Katze.'
} = {
'Hund frisst Katze.',
'Katze frisst Maus.',
'Hund mag Hund.', 'Hund mag Maus.',
'Katze mag Katze.',
'Maus mag Hund.', 'Maus mag Maus.'
} |
In diesem Beispiel bin ich jedoch vom Basiskontext ausgegangen.
Es könnte auch sein, dass in einem anderen Kontext
(spezielles Wissen, spezielle Zusammenhänge)
auch der Satz
‚Maus mag Katze.’
semantisch korrekt sein kann.
Der Mensch gibt dem Computer vor, was er verstehen soll
— Gewissen und Ethik gehen hierbei im gewissen Sinne vom Menschen aus.
Hat man es aber mit einer unendlichen Sprache zu tun,
muss man sich Strategien überlegen, die semantisch fehlerbehaftete Elemente der Sprache herauszufiltern.
Streng genommen wäre ein
Semantik−Check
gar nicht nötig,
wenn bereits die zugrundeliegende Grammatik keine semantisch fehlerbehafteten Elemente mehr zuließe.
Doch dann würde diese Grammatik in der Praxis kaum handhabbar sein
und eine kontextsensitive Sprache induzieren
— der Kontext müsste der Grammatik bekannt sein.
Abstrakte Maschine
Die Skeptiker unter Ihnen könnten sich jetzt fragen, was nun das
Hund−Katze−Maus−Beispiel
mit Programmiersprachen zu tun hat und das mit Recht,
denn es fehlt noch die Angabe einer Maschine, die die Sprachelemente aus dem
Hund−Katze−Maus−Beispiel
erwartet.
Eine Definition einer
abstrakten Maschine
zur Sprache
L(G)
macht sie automatisch zur Programmiersprache.
Ich definiere eine abstrakte Maschine, die die
Infix−Relationen
‚frisst’
und
‚mag’
gemäß meiner Vorstellung von Fressen und Mögen bildlich darstellt.
Diese Maschine erwartet als Eingabe ein Element der Sprache
L(G).
Die Akteure sind gemäß der Eingabe: Hund, Katze, Maus.
Mit dieser Maschinendefinition wird das hier vorgestellte Sprachbeispiel
L(G)
— wenn auch ein wenig an den Haaren herbeigezogen —
zur Programmiersprache.
Analogie zum Alltag
Es gibt noch primitivere Programmiersprachen als das
Hund−Katze−Maus−Beispiel
sogar für reale Maschinen!
Ich denke da gerade an einen Lichtschalter mit der Programmiersprache
{'an', 'aus'},
der bei Eingabe
'an'
bzw.
'aus'
den Stromkreis mit einer Glühbirne als Verbraucher schließt bzw. unterbricht.
Die Eingabe eines Programms der Programmiersprache
{'an', 'aus'}
wird von einem Kippschalter
(Lichtschalter)
simuliert.
- Das Betätigen eines Lichtschalters ist eine simple Form des Programmierens.
- Eine Person, welche einen Lichtschalter betätigt, ist ein Programmierer.
- Ein Handbuch zur korrekten Benutzung eines Lichtschalters ist ein Programmierhandbuch.
- Viele programmieren tagtäglich extrem viele Programme, ohne sich dessen überhaupt bewusst zu sein.
- Weitere Beispiele programmierbarer Maschinen:
- Haustür
- Heizung
- Mikrowellengerät
- Waschmaschine
- Fernseher
- Computer
- Philosophisch gesehen, kann jede Aktion, die eine bestimmte Reaktion auslöst,
als Akt des Programmierens angesehen werden
(Anhänger Newtons mögen es mir verzeihen).
Wie viele Programme haben Sie heute eigentlich schon programmiert?
:−)
Aufgrund der Vielfältigkeit menschlicher Ideen
(Konzepte und Denkschemata)
haben sich viele unterschiedliche Programmiersprachen etabliert,
welche sich je nach Abstraktionsgrad einteilen lassen.
Maschinensprachen
Die von einem Rechner bzw. Prozessor auf unterster Ebene verarbeiteten Instruktionen bilden die sogenannte
Maschinensprache.
[INFO94 S. 472]
Diese Sprache besteht praktisch nur aus Nullen und Einsen
(Binäralphabet).
Ein Maschinenprogramm, welches in einer Maschinensprache
(Binärcode)
formuliert wird, kann von einem Computer
direkt
ausgeführt werden.
Die Maschinensprache ist speziell auf einen bestimmten Prozessor und seine Möglichkeiten ausgelegt.
[COMPULEX]

Jeder Prozessor nutzt andere Formate und Maschinenbefehle.
Die Vielzahl unterschiedlicher Prozessoren und somit unterschiedlicher Maschinensprachen macht es dem Menschen
praktisch unmöglich, einen Überblick zu behalten,
zumal die Interpretation eines Maschinenprogramms äußerst schwer ist.
Eine für den Menschen unverständliche Sprache
...010100000101111101010101010010111001001010010000101100100001
001101001000010010010100100100101110101101101001000101000110010
101010010100111010111101001010010010101101000101011010001010001
001000100000101000111111011101101010010100000000101010110000... |
Dabei sei bemerkt, dass heutzutage die Maschinenprogramme tausendfach oder gar millionenfach größer sind als dieses Codebeispiel.
Manchmal will der Mensch ein Maschinenprogramm
– man glaubt es kaum –
analysieren, um zum Beispiel einen Computervirus
{Computerviren hängen sich in der Regel an das Ende von ausführbaren Maschinenprogrammen (sog. Wirtsprogrammen) und manipulieren manche Jump-Befehle dergestalt, dass der unerwünschte Virus bei Ausführung des Wirtsprogramms sicher zur Ausführung kommt.}
im Programm ausfindig zu machen.
Dabei muss er die Instruktionen des zugrundeliegenden Prozessors genau kennen.
Instruktionen
sind Maschinenbefehle in Binärschreibweise und können sogar eine variable Länge besitzen
(siehe CISC; Complex Instruction Set Computer).
Dabei bewirkt jede einzelne Instruktion eine bestimmte Operation auf dem zugrundeliegenden Prozessor, wie zum Beispiel:
- Addiere zwei Werte, die sich im Speicher an den Stellen
X
und
Y
befinden und schreibe das Ergebnis in die Speicherstelle
Z.
- Erhöhe den Wert an der Speicherstelle
Z
um
k
und schreibe das Ergebnis wieder in
Z.
- Vergleiche die Werte zweier Speicherzellen
A
und
B
und springe bei Gleichheit dieser Werte im Maschinenprogramm
n
Bytes nach vorne.
Mit Hilfe der Instruktionen, also Teile des Programms, bearteitet die Maschine wiederum das Programm.
Jedes korrekt arbeitende Maschinenprogramm in Binärschreibweise kann in eine Folge von Instruktionen,
die der Mensch in einzelne, verständliche Operationen übersetzen kann, zerlegt werden
(deassemblieren).
Beim Erstellen eines Maschinenprogramms geht man allerdings in umgekehrter Weise vor
(siehe Assemblersprachen).
Assemblersprachen
Für den Menschen ist der Umgang mit Maschinensprachen offensichtlich nicht einfach.
Deshalb formuliert der heutige Programmierer keine binären Maschinenbefehle mehr aus,
sondern verwendet stattdessen höhere Sprachkonzepte,
die es ihm ermöglichen, einen leserlichen und somit korrigierbaren, erweiterbaren Programmcode zu schreiben,
der schließlich von Hilfsprogrammen
(Assembler oder Compiler)
in einen Maschinencode übersetzt werden kann.
Ein
Assembler
ist ein solches Hilfsprogramm, welches einen Assemblercode in Maschinensprache übersetzt.
Der
Assemblercode
ist eng an die Maschinensprache eines Prozessors angelehnt.
Viele Assemblerbefehle sind symbolische
(mnemonische)
Darstellungen der Maschinenbefehle,
also
ADD
statt
0001101100000100.
Meist beziehen sich solche Assemblerbefehle auf ein oder zwei Operanden,
die numerische Werte darstellen oder auf Speicherplätze bzw. Register verweisen.
[COMPULEX]
Die Assemblersprache bildet die nächsthöhere Abstraktionsstufe zur Maschinensprache,
dennoch sind Assemblersprachen maschinennahe Sprachen,
d. h. beinahe jede Prozessorfamilie benötigt ihre eigene Assemblersprache.
Im Folgenden werden einige Assemblerbefehle für die MIPS-Prozessorfamilie
(siehe 3−Adress−Rechner)
erklärt
(die Kommentare werden mit # eingeleitet).
Erläuterungen einiger MIPS−Assemblerbefehle
# Die Addition zweier Registereinträge
ADD $1, $2, $3 # R[1] := R[2] + R[3]
# Die Subtraktion zweier Registereinträge
SUB $1, $2, $3 # R[1] := R[2] - R[3]
# Die Multiplikation zweier Registereinträge
MULT $1, $2, $3 # R[1] := R[2] * R[3]
# Die UND-Operation zweier Registereinträge
AND $1, $2, $3 # R[1] := R[2] & R[3]
# Unbedingter Sprung im Maschinencode
J 100 # goto 100
# Bedingter Sprung im Maschinencode
BEQ $1, $2, 100 # if R[1] = R[2] then goto 100 |
In der Regel generiert der Assembler aus jedem einzelnen Assemblerbefehl genau eine binäre Instruktion.
Jede Instruktion benötigt einen Takt an Rechenzeit, wenn man einmal von komplexen Operationen wie Fließkommaoperationen absieht.
Ein
1−Giga−Hertz−Prozessor
könnte also theoretisch in einer Sekunde etwa
1.000.000.000
Assemblerbefehle abarbeiten, wenn er nicht ständig auf Speicherinhalte vom Hauptspeicher warten müsste.
Um wirklich effiziente Assemblerprogramme schreiben zu können,
muss der Programmierer den jeweiligen Prozessor genau studiert haben und wissen, wie dieser arbeitet.
Umgekehrt trägt das Erlernen einer Assemblersprache wesentlich zum Verständnis der Arbeitsweise eines Prozessors bei.
Der moderne Programmierer möchte jedoch einen Algorithmus weitgehend unabhängig von der verarbeitenden Maschine entwickeln.
Aus diesem Grund entstanden die sogenannten höheren Programmiersprachen wie beispielsweise problemorientierte Sprachen,
die schließlich in eine beliebige Assemblersprache übersetzt werden können.
Imperative Programmiersprachen
… sind befehlsorientierte Sprachen.
Ein imperatives Programm besteht ähnlich wie ein Assemblerprogramm aus einer Folge von Befehlen an den Prozessor
wie z. B.
„Schreibe in die Variable a den Wert 3”,
wobei wir schon beim ersten wesentlichen Merkmal dieser Sprachen wären
– der Verwendung von Variablen.
Speicherzellen können mit Namen versehen werden, den sogenannten
Variablen.
Bei einer Zuweisung beispielsweise wird der Inhalt einer Speicherzelle, also der Wert der Variable, überschrieben.
Während Variablen den Inhalt einer Speicheradresse repräsentieren, repräsentieren Zeiger
– die sogenannten Pointer –
Speicheradressen.
Pointer können auf Variablen, also auf Speicherinhalte, zeigen,
aber auch auf andere Pointer, welche wiederum als Speicherinhalte aufgefasst werden können.
Die Befehle eines imperativen Programms bezeichnet man als Anweisungen,
sogenannte
Statements,
welche induktiv definiert sind:
- Die
leere Anweisung
ist ein Statement.
skip;
oder auch
nop;
- Die
Abbruchanweisung
ist ein Statement.
abort;
- Die
Zuweisung
ist ein Statement.
a := EXPRESSION;
Die Variable
a
erhält das Ergebnis von
EXPRESSION
als Wert.
- Die
sequenzielle Komposition
ist ein Statement.
STATEMENT1; STATEMENT2;
- Die
Fallunterscheidung
ist ein Statement.
if CONDITION then STATEMENT1 else STATEMENT2;
- Die
Wiederholungsanweisung
ist ein Statement.
while CONDITION do STATEMENT;
Diese Statements werden in sogenannten Programmblöcken
(Prozeduren oder auch Unterprogramme)
verwendet, die dann bei konventioneller Programmierung modular zur Ausführung gebracht werden können.
Nicht nur deshalb werden imperative Programmiersprachen oft auch als
prozedurale Programmiersprachen
bezeichnet,
sondern auch wegen der prozeduralen Sichtweise, zu der dieses Sprachkonzept erzieht:
- Der Benutzer sagt,
was
das Problem ist, und der Benutzer kontrolliert,
wie
es gelöst werden soll.
[PROGINLOG S. 15]
Je nach Art der Problemstellung sieht der Programmierer im Programmcode vor,
mit welcher Prozedur das Problem gelöst werden soll
(z. B.
„Löse nach Verfahren-A und nicht nach Schema-F!”).
Eine imperative Programmiersprache stellt in der Regel die nächsthöhere Abstraktionsstufe zur Assemblersprache dar.
Spezielle Übersetzerprogramme, die
Compiler
genannt werden,
formulieren aus einem Programmcode
ein
(hoffentlich!)
optimiertes Assemblerprogramm, welches wiederum in eine Maschinensprache überführt wird.
Manchmal muss aber ein Programmierer bestimmte Zusicherungen bzgl. der Effizienz garantieren.
In diesem Fall wird dem Compiler nicht vertraut, sondern an den kritischen Stellen der Assemblercode explizit ausformuliert.
Dies wird auch von den meisten imperativen Programmiersprachen unterstützt.
Im Vergleich zu anderen Programmiersprachen unterscheiden sich imperative Programmiersprachen
bis auf ein paar Kleinigkeiten nicht wesentlich von Assemblersprachen.
Von der Art des Prozessor- und Speicherzugriffs wird nur geringfügig abstrahiert
(z. B. mit der Verwendung von Variablen),
wohingegen bei den prädikativen und funktionalen Programmiersprachen die Begriffe Prozessor und Speicher
nur in Ausnahmefällen gebraucht werden.
Die Welt der Informatik hat schon recht früh erkannt,
dass die imperative Programmierung in der Praxis einen üblen Nachteil mit sich bringt,
nämlich eine aufwendige Programmkorrektur.
Die Korrektur eines imperativen Quellprogramms kann zu einer wahren Odyssee werden,
wenn beispielsweise viele globale Variablen vorkommen.
Bei der imperativen Programmierung benötigt man zwangsläufig globale Variablen,
um den aktuellen Programmzustand halten zu können.
Der Wert einer globalen Variable kann praktisch an jeder Stelle des Programms nach Belieben geändert werden.
Der Programmierer
(möglicherweise ein Neuer im Team)
wird bei der Neubelegung einer Variable nicht programmtechnisch gezwungen,
irgendwelche Nebenbedingungen oder Spezifikationen einzuhalten.
Ein Beispiel für einen Spezifikationsbruch sehen Sie im folgenden Codebeispiel.
Das Übel globaler Variablen
int geburtsjahr; // Globale Variable
geburtsjahr = 2001; // Spezifikation: vollständige Jahreszahl
...
if (geburtsjahr > 1682) print("Geboren nach Blaise Pascal!");
...
geburtsjahr = 74; // gemeint war aber 1974
// Programmtechnisch erlaubt, aber Bruch der Spezifikation!
// Programm wird stellenweise nicht mehr korrekt ablaufen.
... |
Bis ein Programmierer ähnliche Fehler
(Bruch der Spezifikation)
entdecken wird, kann es lange dauern.
Wertvolle Arbeitszeit muss in die Fehlersuche investiert werden
– und das alles nur, weil der Programmierer eine Kleinigkeit übersieht und der Compiler trotzdem nicht meckert.
Die Antwort auf das Übel der globalen Variablen ist die objektorientierte Programmierung
(kurz OOP).
Objektorientierte Programmiersprachen
Bei diesen Sprachen werden alle zum Lösen eines Problems notwendigen Informationen
(Daten und Anweisungen bzw. Regeln)
als Objekte aufgefasst.
Objekte können durch Senden von
Nachrichten
(Mitteilungen an andere Objekte)
miteinander Informationen austauschen.
Für jedes Objekt sind die Nachrichten, die es verstehen kann, festgelegt.
Empfängt ein Objekt eine Nachricht, so antwortet es mit einem anderen Objekt.
[INFODUDEN]
Objektorientierte Programmiersprachen können als Erweiterung der imperativen Programmiersprachen aufgefasst werden.
Sie zeichnen sich besonders durch ihr elegantes Speichermanagement aus.
Beispielsweise regelt jedes Objekt mit seinen Methoden den externen Zugriff auf seine privaten Membervariablen.
So kann das Objekt von außen nicht in einen inkonsistenten Zustand versetzt werden
– ein Bruch der Spezifikation ist deshalb kaum noch möglich.
Der
OOP−Programmierer
kann für Methoden, Attribute und Konstanten die Sichtbarkeit mit diversen Schlüsselwörtern
(public, protected, private etc.)
festlegen.
Manchmal ist es einem gar nicht bewusst, dass man mit der Festlegung der Sichtbarkeit den Speicher managt.
Funktionale Programmiersprachen
Programme berechnen Funktionen, die Eingabedaten in Ausgabedaten abbilden.
In der funktionalen Programmierung beschreibt man daher die Beziehung zwischen Ein- und Ausgabedaten mithilfe mathematischer
Ausdrücke, indem man elementare Ausdrücke für einfache Funktionen verwendet und mit Operationen,
die auf Funktionen definiert sind, komplexere Funktionen darstellt
– insbesondere auch Funktionen, die auf Funktionsräumen definiert sind
(das Integral ist z. B. eine solche höhere Funktion,
da es eine stetige Funktion als Parameter enthält und als Ergebnis wiederum eine Funktion, die Stammfunktion, liefert).
Das wichtigste Konstruktionsprinzip ist hierbei die
Rekursion.
Ein Programm besteht aus einer Menge von Ausdrücken, die Funktionen definieren.
Eine Berechnung ist die Anwendung einer Funktion auf eine Liste von Werten oder Ausdrücken.
Eine solche Anwendung einer Funktion auf einen Ausdruck nennt man Applikation, weshalb man auch von
applikativen Programmiersprachen
spricht.
Dass dieser Ansatz tatsächlich geeignet ist, alle algorithmischen Aufgabenstellungen bearbeiten zu können
(Church'sche These),
zeigt der
Lambda−Kalkül,
auf dem das funktionale Programmieren letztlich basiert.
[INFODUDEN]
Prädikative Programmiersprachen
Bei diesen Sprachen wird Programmierung als das Beweisen in einem System von Tatsachen und Schlussfolgerungen aufgefasst:
Der Anwender gibt eine Menge von Fakten
(gültige Prädikate)
und Regeln
(d. h. wie man aus Fakten neue Fakten gewinnt)
vor, und die Aufgabe des Rechners ist es, eine gestellte Frage richtig
(true)
oder falsch
(false)
zu beantworten.
[INFODUDEN]
Ein kleines OnlineProlog ohne lästige Installation mit vielen Beispielen finden Sie unter
SimpleProlog
bzw.
SimplePrologPlus.
Skriptsprachen
Bei der Programmierung eines Programms muss sich der Programmierer oft mit lästigen Detailfragen aufhalten.
Selten kann sich der Programmierer mit dem Wesentlichen beschäftigen.
Skriptsprachen werden auf einem sehr hohen abstrakten Niveau angesiedelt
und nehmen dem Programmierer in aller Regel lästige Detailimplementierungen ab.
Beispielsweise ist es dem Skriptprogrammierer egal, ob er nun eine
ArrayList
oder eine
LinkedList
als Liste zur Verfügung hat.
Listen werden in den Skriptsprachen gerne als assoziative Arrays oder HashTables realisiert,
um eben extrem gute Zugriffskomplexitäten gewährleisten zu können.
Um welche konkrete Datenstruktur es sich bei einer Liste tatsächlich handelt,
bekommt der Skriptprogrammierer eigentlich gar nicht mit
– es interessiert ihn auch nicht wirklich.
Aufgrund des hohen Abstraktionsniveaus einer Skriptsprache ist es dem Skriptprogrammierer möglich,
extrem kurze und vor allem prägnante Programme zu erstellen.