Bernd Leitenbergers Blog

Die glorreichen 10 – Programmiersprachen

Ich wollte mal eine Reihe in dieser Rubrik über Programmiersprachen machen. Zuerst dachte ich daran eine Liste nach meinen persönlichen Favoriten zu erstellen. Anfangs befürchtete ich, dass ich gar nicht auf 10 komme, aber es sind tatsächlich mehr, wenngleich ich in vielen Sprachen nur kleine Programme verfasst habe oder mich nur wenig mit ihnen beschäftigt habe.

Aber das wäre zum einen wohl so persönlich, dass es für die meisten uninteressant wäre und der Erkenntnisgewinn, der ja bei diesem Blog im Vordergrund steht, käme zu kurz. Also dachte ich nach und kam dann auf die Idee 10 Kriterien zu erarbeiten, nach denen man alle Programmiersprachen kategorisieren kann und das ist nun der heutige Blog. Anders als sonst bei den glorreichen 10 ist es aber keine Reihenfolge, es gibt also keinen Platz 10 und keinen Platz 1. Ich habe deswegen auch die Nummern weggelassen. Stattdessen arbeite ich mich geschichtlich nach vorne, wenngleich nicht exakt nach Jahreszahlen.

Der Artikel geriet etwas lang, so lest ihr heute den Teil 1 und morgen geht es dann weiter.

Einteilung nach Generationen

Als ich mich zum ersten Mal mit Programmieren beschäftigte, also in den frühen Achtziger Jahren war dies die übliche Einteilung von Programmiersprachen, damals gab es auch noch weniger Sprachen. Die Einteilung ging damals bis zur dritten Generation, später wurde das bis zur fünften erweitert, wobei die beiden folgenden schon nicht mehr einheitlich definiert werden und heute redet von Generationen keiner mehr.

Die erste Generation wurde auf den ersten Computern eingesetzt. Als „Nullte Generation“, also eigentlich keine Programmiersprache, sieht man den Maschinencode an, also die Bitmuster die der Prozessor als Befehle ansieht. Wer in den Achtzigern einen Heimcomputer hatte und Computerzeitschriften lass, fand da oft seitenlange Listings mit Maschinencode meist in Hexdezimalnotation die er abtippen musste. Da die Rechner so langsam waren, schrieben viele wichtige Routinen in Assembler und generierten einen Hexdump des erzeugten Maschinencodes.

Die erste Generation war dann Assembler. Jeder Maschinenbefehl bekam einen Namen, meist ein schwer einprägsames Buchstabenkürzel und Register bekamen Nummern oder auch Namen. Das war viel einfacher als sich die Binärkombination des Befehls zu merken. Daneben konnte man Sprungmarken definieren, Datenbereiche benennen für Variablen, Konstanten Namen geben und bessere Assembler beherrschten auch Makros also Befehlsfolgen, denen man Parameter übergab, die im Quelltext durch Suchen/Ersetzen im Makro ausgetauscht werden konnten. Das Vereinfachte vieles und eliminierte Fehlermöglichkeiten.

Assembler und Maschinensprache sind aber prozessorspezifisch. Ein Assemblerprogramm oder der Maschinencode für einen Intel-Prozessor läuft nicht auf einem ARM Prozessor. Dass, was wir eigentlich als Programmiersprache verstehen entstand erst mit der zweiten Generation, den ersten echten Programmiersprachen, die populärsten sind COBOL und FORTRAN. Ein Quelltext dieser Programmiersprache kann von jedem Computer ausgeführt werden, wenn man das nötige Übersetzungsprogramm hat. Allerdings meist nicht ohne Anpassungen an die Hardware des Computers. Beide Sprachen wurden für einen bestimmten Anwendungszweck geschaffen: COBOL zur Verarbeitung von Daten in fester Form, z.B. den Einträgen für die Buchhaltung. FORTRAN zur numerischen Lösung von mathematischen Problemen, dazu gehören auch physikalische Probleme wie der genaue Ablauf bei einer Supernova oder wie morgen das Wetter wird. Mathematik geht in COBOL, ist aber sehr umständlich zu handhaben und auf die Grundrechenarten beschränkt und das Verarbeiten von Daten ist in FORTAN wiederum recht umständlich gelöst. FORTAN ist zudem noch relativ maschinennah, so wird bei Funktionen nicht der Datentyp eines Parameters angegeben man kann einer Funktion, die einen Ganzzahlwert erwartet, auch eine Fließkommazahl übergeben. Intern differiert aber deren Darstellung und so kommen dann schon sehr komische Ergebnisse zustande.

Die dritte Generation und das sind fast alle Sprachen, die danach entstanden sind Universalsprachen. Mit ihnen kann man jedes Problem lösen, sie verfügen über einen breiten Befehlsvorrat, auch wenn die Möglichkeiten je nach Sprache unterschiedlich gut oder schlecht sind. Sie abstrahieren auch noch etwas besser als Fortran von der Hardware.

Die vierte Generation ist nicht mehr einheitlich definiert. Eine gute Definition finde ich ist die, die frühe Programmiersprachen für KI einsetzten, wie Prolog. In ihnen wird in der Sprache nicht der Algorithmus definiert, also wie ein Problem zu lösen ist, sondern es werden Daten angegeben und die Beziehungen der Daten untereinander. Gibt man dann neue Daten ein so berechnet das System über einen Backtracking-Mechanismus die wahrscheinlichste Lösung. Eine in den späten Achtzigern als KI bezeichnete Implementierung wertete seismische Daten von Probebohrungen nach Erdöl aus. Das System kannte die Gesteinsschichten und wie sie seismische Signale veränderten und gab man neue Daten ein so ermittelte das System den Aufbau des Untergrundes bis zu einer bestimmten Tiefe.

Als fünfte Generation etablierten sich dann Abfragesprachen für Datenbanksysteme, das populärste Beispiel ist SQL. Anstatt eine Datenbank über ein Programm sequentiell abzusuchen und die Daten dann weiterzuverarbeiten z.b. zu gruppieren formuliert man eine Abfrage, was man haben möchte, z.B. alle Personen deren Nachname „Mayer“ ist und die über 50.000 Euro im Jahr verdienen, gruppiert nach Städten, in denen sie wohnen. Für diese Aufgabe müsste man schon einen komplexen Algorithmus in einer Sprache der dritten Generation schreiben.

Ich denke KI kann man als Sprache der sechsten Generation ansehen, denn dort kann man ein Problem umgangssprachlich formulieren, ohne selbst Code selbst zu schreiben. Ich habe das mal für euch gemacht und ChatGPT folgende Aufgabe gestellt:

Erstelle ein Pascal-Programm das die momentane Geschwindigkeit eines Satelliten berechnet, wenn gegeben: Masse des Himmelskörpers, Apo- und Peripunkt sowie Position deren Geschwindigkeit berechnet werden soll“.

Das System müsste dann auf die vis-viva Gleichung stoßen und diese in Pascalcode umsetzen. Und das tat es dann auch, hier die Antwort:

Hier ist ein Pascal-Programm, das die Bahngeschwindigkeit eines Satelliten an einer beliebigen Position seiner elliptischen Umlaufbahn berechnet.

Grundlage ist die Vis-Viva-Gleichung:

v=\sqrt{GM\left(\frac{2}{r}-\frac{1}{a}\right)}

mit:

Die große Halbachse ergibt sich aus Apo- und Perizentrum:

a=\frac{r_{apo}+r_{peri}}{2}

Pascal-Programm

program SatellitenGeschwindigkeit; 
uses   Math; 
const   Gravitationskonstante = 6.67430E-11;  { m^3 kg^-1 s^-2 } 
var   MasseDesHimmelskoerpers : Double;    
AbstandApopunkt         : Double;    
AbstandPeripunkt        : Double;    
AktuellerAbstand        : Double;    
GrosseHalbachse         : Double;    
Geschwindigkeit         : Double; 
begin    Writeln('Berechnung der Satellitengeschwindigkeit');   Writeln;   
Write('Masse des Himmelskoerpers [kg]: ');   Readln(MasseDesHimmelskoerpers);   
Write('Abstand Apopunkt vom Zentrum [m]: ');   Readln(AbstandApopunkt);   
Write('Abstand Peripunkt vom Zentrum [m]: ');   Readln(AbstandPeripunkt);   
Write('Aktueller Abstand vom Zentrum [m]: ');   Readln(AktuellerAbstand);   
GrosseHalbachse :=     (AbstandApopunkt + AbstandPeripunkt) / 2.0;   
Geschwindigkeit :=Sqrt(Gravitationskonstante *MasseDesHimmelskoerpers * 
((2.0 / AktuellerAbstand) -(1.0 / GrosseHalbachse)));
Writeln;   
Writeln('Grosse Halbachse: ', GrosseHalbachse:0:3, ' m');    
Writeln('Momentane Geschwindigkeit: ',           Geschwindigkeit:0:3, ' m/s');   
Readln; 
end.

Beispiel (Erde)

Eingaben:

Ausgabe:

Wichtig: Apo- und Peripunkt sowie die aktuelle Position müssen als Abstand vom Zentrum des Himmelskörpers eingegeben werden. Falls nur die Höhe über der Oberfläche bekannt ist, muss zuvor der Radius des Himmelskörpers addiert werden.

Der eigentlich wichtige Code steckt nur in der Zeile Geschwindigkeit:= … Aber Chatgpt ist etwas schwafelig. Mann kann aber nachhelfen:

Bitte nur die Berechnung in einer Funktion, ohne Hauptprogramm“

uses

Math;

 

function BerechneSatellitenGeschwindigkeit(

MasseDesHimmelskoerpers : Double;

AbstandApopunkt : Double;

AbstandPeripunkt : Double;

AktuellerAbstand : Double

) : Double;

const

Gravitationskonstante = 6.67430E-11; { m^3 kg^-1 s^-2 }

var

GrosseHalbachse : Double;

begin

GrosseHalbachse :=

(AbstandApopunkt + AbstandPeripunkt) / 2.0;

 

Result :=

Sqrt(

Gravitationskonstante *

MasseDesHimmelskoerpers *

(

(2.0 / AktuellerAbstand) –

(1.0 / GrosseHalbachse)

)

);

end;

Ist noch etwas lang, ich würde auch die Formel in eine Zeile packen, aber mathematisch und programmtechnisch korrekt.

Compiliert – Interpretiert

Von nun an werden die Absätze kürzer, weil ich anfangs viel erklären muss und dann immer weniger. Die ersten Sprachen waren alle compiliert. Das Übersetzungsprogramm von Quellcode in der Programmiersprache nach Maschinencode nennt man Compiler. Ein Compiler erzeugt aus dem Quellcode ein auf dem Rechner ausführbare Datei, die den Maschinencode enthält. Die meisten Apps, die sie installieren, sowie alle Betriebssysteme sind so erstellt worden. Bei Windows haben sie die Dateiendung „,exe“ Das hat Vorteile: Man erhält so die maximal mögliche Geschwindigkeit und kann ein Programm verteilen ohne befürchten zu müssen, dass es jemand verändert und als sein Programm verkauft. Zudem braucht der Kunde keinen Compiler, der früher eine Menge Geld kostete. Allerdings ist das Erstellen ziemlich zeitaufwendig. Früher schrieb man den Quelltext in einem Editor, einer einfachen Textverarbeitung (als Notepad immer noch in Windows enthalten), beendete diesen, rief den Compiler auf, der oft Fehler meldete, untersuchte mit einem Debugger das Programm auf diese Fehler und veränderte den Quelltext erneut mit dem Editor. Turbo Pascal führte dann die IDE (Integrated Development Environment) ein, bei der alle drei Teile in einem Programm waren, sodass zumindest das Wechseln wegfiel, zeitaufwendig war das trotzdem. Man konnte dem Compiler zuschauen wie er langsam die Zeilennummern hoch zählte. Bei größeren Programmen bin ich immer einen Kaffee holen gegangen. Seitdem konsumiere ich etwa 1+ Liter Kaffee pro Tag.

Für Programmieranfänger ist das frustrierend und für Lehrsprachen wurde der Interpreter geschaffen, auch eine einfache IDE. Bekannteste interpretierte Sprache ist BASIC. Man schreibt den Quelltext in einem einfachen, zeilenorientierten Editor und startet das Programm mit dem Befehl RUN. Auch hier stoppt der Interpreter bei Fehlern, und zwar in der Zeile die ihn enthält. Ist doch viel benutzerfreundlicher oder? Es gibt aber zwei Nachteile: der erste: der Quelltext und das Programm sind gleichzeitig im Speicher. Compilierte Programme sind meist viel kürzer als der Quelltext. Der größere Nachteil ist aber das jede Zeile dann übersetzt wird, wenn die Ausführung über sie stolpert. Programme enthalten immer viele Schleifen, sonst könnte man ja auch Excel zur Berechnung nehmen. Wenn eine Schleife also eine Folge von Befehlen enthält, die wiederholt ausgeführt werden, compiliert wird, dann wird jede Zeile einmal compiliert, bei einem Interpreter wird sie dagegen pro Schleifendurchlauf einmal „compiliert“ also bei 1000 Durchläufen 1000-mal. Interpretierte Sprachen sind daher in der Ausführung viel langsamer als compilierte und man kann nur den Quelltext weitergeben, sozusagen eine frühe Form von Open Source.

Der Unterschied kann dramatisch sein. Ich entdeckte in Astrozeitschrift einen Artikel über die Berechnung von Planetenbahnen über die Runge-Kutta-Methode und im Link ein Python-Programm. Das habe in Pascal übersetzt und zum Test die Planetenkonstellationen sowohl in Python wie in Pascal berechnet – Python ist interpretiert, Pascal compiliert: Das Pascalprogramm war 117-mal schneller fertig.

Procedural – Functional

Eine weitere Unterscheidung ist wie die Programmiersprachen Probleme lösen. Was ich bisher beschrieb, ist die prozedurale Programmierung, bei der es als charakteristisches Element Schleifen gibt. In funktionalen Sprachen gibt es diese nicht. Stattdessen werden Funktionen genutzt die miteinander verkettet werden. In beschränktem Maße geht das auch in prozeduralen Sprachen so kann man die Fakultät einer Zahl wie folgt berechnen:

Fak:=1

For I:=1 to n do Fak:=Fak*i

oder so:

Function Fak(n : integer) : integer;

if n=1 then Fak:=1 else

fak:=Fak(n-1)*n;

In der letzten Implementierung ruft sich die Funktion Fak rekursive auf so lange auf bis N=1 ist, übergibt dann 1 als Rückgabewert der dann mit 2 multipliziert wird bis man zu n zurückkehrt.

Ich bin mit funktionalen Sprachen nie so richtig warm geworden, weil mein Denken dem prozeduralen Ansatz entspricht. Nach einem Programmierparadigma kann man jedes Problem auf beide Weisen lösen, nur ist meist eine Methode die einfachere. Die erste funktionale Sprache war LISP, erscheinen 1958.

Objektorientiert oder nicht

Ein weiterer Trend ist die Programmiersprachen immer mehr sich von den Möglichkeiten der Maschine entfernten und sich dem menschlichen Denken annäherten. Frühe Programmiersprachen kannten nur wenige elementare Datentypen wie die Zeichenkette (String), Ganzzahlen oder Fließkommazahlen. Sprachen wie Pascal und C nutzen die Möglichkeiten Daten zusammenzufassen in Records. So gehören Vorname und Nachname zusammen und können gemeinsam in einer Variable abgelegt werden und sie erlauben das Einschränken von Wertebereichen, z.B. Jahreszahlen auf einen Bereich von -6000 bis 2100, anstatt den gesamten Integerbereich der bei 32 Bit von -2 Mrd bis +2 Mrd geht oder es gibt Aufzählungstypen wie für das Geschlecht (männlich, weiblich, divers) wo nur einer der drei Werte möglich ist.

So was macht nicht nur das Programm lesbarer, es vermeidet auch Fehler. Ein Compiler würde schon meckern, wenn man versucht einem Datumswert das Jahr 2130 zuzuweisen oder einem Geschlecht den Wert „Zwitter“.

Was aber immer noch möglich ist, ist das man eine Routine aufruft mit Daten, die sie eigentlich nicht verarbeiten sollte. Der objektorientierte Ansatz sieht Daten und den Code, der sie verarbeitet als eine Einheit. Ein Objekt hat einen Datenteil und Code zur Verarbeitung. Man kann auch Routinen verstecken, also nur innerhalb des Objektes nutzen, aber nicht vom Code außen zugreifen und man kann Objekte vererben. Ein Personenobjekt mit den obigen vier Datenfeldern (Vorname, Nachname, Geschlecht, Geburtsjahr) kann z.B. zu einem Adressobjekt erweitert werden, indem noch Daten für Straße, Telefonnummer, Stadt und Land hinzukommen. Dann muss man nur den neuen Code schieben und kann den alten Code wiederverwenden.

Solche Programmiersprachen nennt man objektorientiert. Eine der ersten war Smalltalk die im Xerox Parc Anfang der Siebziger Jahre entwickelt wurde. Das Konzept war so erfolgreich, das seitdem viele Sprachen um diese Features erweitert wurden, manchmal entstand auch eine neue Sprache wie aus C das objektorientierte C++.

Die mobile Version verlassen