Modernes Urgestein
Fortran, die Mutter der höheren Programmiersprachen
Mit Fortran startete die Ära der höheren Programmiersprachen. Die Sprache gehört auch heute noch zu den mächtigsten Werkzeugen für numerisch intensive Rechnungen. Hier lernen Sie ganz leicht, Ihre ersten Fortran-Programme zu schreiben. Gut möglich, dass Sie sich in die Sprache vernarren.
Die Geschichte von Fortran, die Abkürzung steht für FORmula TRANslation, beginnt im Jahr 1956 und damit für IT-Verhältnisse in grauer Vorzeit. Obwohl Fortran eine der ersten höheren Programmiersprachen ist, wird sie in der modernen Forschung weiterhin genutzt. Bis heute gilt sie für numerisch intensive Rechnungen als sehr mächtige und schnelle Programmiersprache. Seit ihrer Einführung haben sich die Sprache, ihre Compiler und Bibliotheken stets weiterentwickelt. Dadurch ist Fortran an den heutigen Stand der Wissenschaften und Technik angepasst und wird auch in Programmen für Supercomputer großer Forschungseinrichtungen eingesetzt.
Unter anderem in der Teilchenphysik ist Fortran beliebt und Fortran-Programme kommen sowohl für die Vorbereitung von Experimenten durch hochkomplexe numerische Simulationen als auch für die Analyse großer multidimensionaler Datensätze zum Einsatz. Solche Aufgaben erfordern Programme, die effizient im Umgang mit Mehrfachintegralen, komplexen Zahlen und der nichttrivialen numerischen Anpassung von Funktionen an Daten sind.
Installieren und anfangen
Wenn Sie jetzt in Fortran einsteigen wollen, haben wir gute Nachrichten: Noch nie war der Einstieg so einfach. Sie brauchen keinen Zugang zum Rechenzentrum einer Universität oder einer Forschungseinrichtung mehr, um den Code auszuführen. Open-Source-Software und ein Büro-PC reichen aus. Die Programmbeispiele in diesem Artikel sind für das Kompilieren mit dem quelloffenen Compiler GFortran (entwickelt vom GNU Fortran Project) ausgelegt.
Auf Debian-basierten Linux-Systemen wie Ubuntu installieren Sie GFortran – sofern nicht schon vorhanden – mit einem Befehl auf der Kommandozeile über den Paketmanager apt:
sudo apt install gfortran
Unter macOS kommen Sie am schnellsten über den Paketmanager Homebrew an einen Fortran-Compiler. Er steckt im Paket gcc. Installiert wird mit:
brew install gcc
Windows steht auf der Liste des GFortran-Projekts nicht ganz oben auf der Prioritätenliste. Es gibt Projekte von anderen Entwicklern, um GFortran unter Windows zum Laufen zu bringen (siehe ct.de/ytdy). Am schnellsten kommen Sie aber mit dem Windows Subsystem for Linux (WSL) zum Ziel, das Microsoft ab Windows 10 ausliefert. In einer Ubuntu-WSL-Umgebung greifen Sie wie oben beschrieben zum Paketmanager apt, um GFortran zu installieren.
Für Ihr erstes Programm brauchen Sie eine Textdatei, einen Editor und eine Kommandozeilensitzung. Legen Sie am besten einen Ordner für Ihr erstes Projekt an, navigieren auf der Kommandozeile in diesen Ordner und öffnen ihn in einer IDE Ihres Vertrauens; ein simpler Texteditor funktioniert auch, die Annehmlichkeiten einer IDE wie Syntax-Highlighting wollen Sie zum Entwickeln aber nicht missen. Die Open-Source-IDE Visual Studio Code zum Beispiel wird zusammen mit der Erweiterung „Modern Fortran“ (siehe ct.de/ytdy) zu einer guten Entwicklungsumgebung.
Legen Sie Ihre erste Fortran-Datei mit dem Namen helloworld.f90 an. Die Endung verrät dem Compiler, dass es sich um Code einer jüngeren Fortran-Ausgabe handelt, bei der der Programmcode direkt am Zeilenanfang beginnt. Warum das nicht immer so war und warum man in freier Wildbahn auch .f als Dateiendung findet, lesen Sie im Kasten Versionssprünge auf Seite 140.
Programme kompilieren
Ein Programm umrahmen Sie mit den Zeilen
program name [...] end program name
Den Wert für name dürfen Sie selbst wählen, zum Beispiel helloworld. Der Befehl, um im Terminal die Zeichenkette Hello, world! auszugeben, ist schnell getippt:
write(*,*) 'Hello, world!'
Die drei Zeilen Ihres ersten Programms können Sie anschließend kompilieren und das übersetzte Programm ausführen. Navigieren Sie in den Ordner und starten den Compiler:
gfortran helloworld.f90
Ohne Angabe eines Dateinamens generiert GFortran die ausführbare Datei als a.out. Diese führen Sie mit ./a.out aus. Auf dem Terminal erscheint die Zeile:
Hello, world!
Wenn Sie genau hinsehen, werden Sie ein Leerzeichen vor der Ausgabe finden – wie Sie das loswerden, erfahren Sie später. Den Namen der ausführbaren Datei können Sie vorgeben, indem Sie den Parameter -o anhängen: gfortran helloworld.f90 -o hello.out
Schreiben mit Format
Zurück zum Code: Der Befehl write() hat zwei Argumente, die für das Beispiel beide mit * befüllt wurden. Beim ersten Argument handelt es sich um die Angabe des Ausgabeziels. Mit dem Wert * oder 6 schreibt das Programm direkt ins Terminal, mit 0 in die Standardfehlerausgabe (stderr in Unix-Systemen). Solche festen Belegungen sind in Fortran durchaus üblich.
Wählen Sie eine Zahl größer 9 für den ersten Parameter, zum Beispiel 101, dann landet die Ausgabe in einer Textdatei mit dem Namen fort.101. Auf diese Weise können Sie verschiedene nummerierte Ausgabedateien anlegen, zum Beispiel für Zwischenergebnisse. Sie können aber auch den Namen der Datei explizit wählen, indem Sie eine Datei mit der gewünschten Nummer als Handle öffnen, sie beschreiben und wieder schließen:
open(102,file='hello.txt') write(102,*) 'Hello, world!' close(102)
Das zweite Argument von write() ist im Beispiel ebenfalls * ; es steht für die Formatierung. Ohne explizite Angabe einer Formatierung wählt write() das zum übergebenen Wert passende Format, im Beispiel ein String. Zu Formatierungen gleich mehr.
Deklaration von Variablen
Fortran gehört zu den statisch typisierten Programmiersprachen, Variablen müssen also mit einem Typ angelegt werden. Das geht implizit und explizit.
Wenn Sie keine Angaben machen, werden alle Variablen, die mit den Buchstaben i, j, k, l, m oder n anfangen, implizit als integer deklariert, also als ganze Zahlen. Alle andere Anfangsbuchstaben bedeuten, dass die Variable vom Typ real(4) ist, also eine reelle Zahl, die in vier Byte abgelegt wird. Gespeichert werden solche Zahlen nach dem Standard IEEE 754 mit einer Mantissenlänge von 23 Bit und 8 Bit für den Exponenten. Wenn Sie die impliziten Typisierungen vermeiden wollen, gibt es verschiedene Optionen. Sie können einzelne Variablen explizit deklarieren:
integer test
Alternativ können Sie den Standard für einen Anfangsbuchstaben ändern:
implicit integer (t)
sagt dem Compiler, dass alle mit t beginnenden Variablen Integer sein sollen. Auch wenn die implizite Deklaration praktisch und kurz ist und zu Lochkartenzeiten ihre Berechtigung hatte, führt sie sehr leicht zu Bugs. Deshalb ist es ratsam, diesen Automatismus abzustellen. Alle Beispiele in diesem Artikel setzen voraus, dass Sie die folgende Zeile verwenden:
implicit none
Sie verbietet dem Compiler Typen zu implizieren und erzwingt die Angabe eines Typs bei jeder Deklaration. Die Variablendeklarationen müssen immer am Anfang der Programmumgebung stehen:
program name implicit none integer test [...] end program name
Zusätzlich zu den Typen integer und real(4) lernen Sie in den folgenden Beispielen die Typen real(8) (Mantissenlänge von 52 Bit und Exponent mit 11 Bit) und character(n) (ein String mit n Zeichen) kennen. Wenn Sie eine Variable eines bestimmten Typs deklariert haben, müssen Sie die richtige Syntax beachten, wenn Sie ihr später im Code einen Wert zuordnen:
integer i1 real(4) r1 real(8) r2 character(2) c1 i1=10 r1=10.0 r2=10.d0 c1='10'
Eine Variable vom Typ integer nimmt wie erwartet einfach den Wert einer ganzen Zahl an. Indes muss eine Variable vom Typ real(4) mindestens eine Nachkommastelle enthalten, selbst wenn der Wert einer ganzen Zahl entspricht. Bei real(8) wird die doppelte Genauigkeit mit der Syntax .d statt . notiert. Strings wiederum können entweder mit einfachen ('String') oder mit doppelten ("String") Anführungszeichen eingegrenzt werden, beide Varianten verhalten sich gleich.
Formatierung: lesen und schreiben
Mit diesem Wissen können Sie nun auch besser einschätzen, wie Sie die Formatierung in write()-Befehlen wählen wollen. Die passende Formatangabe für ganze Zahlen lautet Iw , wobei Sie für w die Weite einsetzen: write(*,'(I4)') räumt vier Zeichen Platz für Ziffern ein. Eine Angabe in der Syntax Fw.d brauchen Sie für reelle Zahlen, wobei Sie für w wieder die Weite und für d die Anzahl der Nachkommastellen wählen. write(*,'(F8.2)') lässt acht Zeichen Platz für die gesamte Zahl und den Dezimalpunkt, wobei zwei Zeichen für die Nachkommastellen eingeplant sind. Aw ist das Format für Strings, wobei w die Angabe der Weite ist. Ein kleines Beispiel:
write(*,'(I3)') 100 write(*,'(F6.2)') 100.45 write(*,'(A4)') "Test"
Beim Formatieren lohnt es sich nicht, allzu geizig zu sein. Fortran rundet die Zahlen nicht, lassen Sie also besser ausreichend Platz. Einen String beliebiger Länge formatieren Sie mit dem Buchstaben a; dann verschwindet auch das Leerzeichen am Zeilenanfang. Ein sicherer Umgang mit diesen Formatierungskniffen von Fortran ist für die Datenauswertung sehr hilfreich. Dieses Wissen braucht man nicht nur zum Schreiben, sondern auch zum Lesen von Daten.
Denn auch der read()-Befehl nimmt zwei Argumente an: von wo eingelesen wird und in welchem Format. Am einfachsten ist es, das Format für den Einstieg wieder auf * zu setzen. Wenn Sie zum Beispiel eine TSV- (tabulatorgetrennte Tabelle) oder CSV-Datei (kommaseparierte Tabelle) namens Daten.dat auswerten wollen, die drei Spalten über zwei Zeilen enthält, dann können Sie sie wie folgt einlesen und auf sechs Variablen verteilen:
real(8) a1,a2,a3 real(8) b1,b2,b3 open(100,file='Daten.dat') read(100,*) a1,a2,a3 read(100,*) b1,b2,b3 close(100)
Die ersten beiden Zeilen deklarieren die sechs Variablen, Zeile drei öffnet die Datei und gibt ihr intern das Handle 100. Für beide Zeilen folgt dann ein read()-Befehl. Die Magie zum Auslesen einer CSV-Datei bringt Fortran direkt mit, sie ist aber nicht sonderlich robust. Sobald die Daten nicht zum erwarteten Format passen, bricht das Programm mit einem Fehler ab.
Schleifen und Arrays
Ein solches Einlesebeispiel ist perfekt, um die Konstrukte Array und Schleife einzuführen, die auch Fortran beherrscht. Denn der obenstehende Code ist alles andere als optimal. In großen Datentabellen wollen Sie nicht jede einzelne Variable explizit deklarieren und für jede Zeile einen read()-Befehl schreiben. Deshalb bietet es sich an, mit mehrdimensionalen Arrays und Schleifen zu arbeiten.
Wenn Sie eine Datentabelle aus 34 Zeilen und 3 Spalten einlesen wollen, brauchen Sie ein zweidimensionales Array, das Platz dafür bietet. Diese Variable soll im Beispiel dat heißen, Sie deklarieren sie mit
real(8), dimension(34,3) :: dat
Nun können Sie die Daten mit einer do-Schleife einlesen. Wenn Sie bereits mit anderen Programmiersprachen gearbeitet haben, kennen Sie das Konzept vermutlich unter dem Namen for-Schleife. Damit Sie innerhalb der Schleife bestimmen können, in welcher Zeile sich das Programm befindet, brauchen Sie eine Variable für den Schleifen-Index:
integer i
Dann können Sie die Datei öffnen und die Schleife beginnen. Diese endet mit dem Befehl enddo:
open(100,file='Daten.dat') do i=1,34 read(100,*) dat(i,1),dat(i,2),dat(i,3) enddo close(100)
Wenn Sie einen Erfolg sehen und das Gelernte anwenden wollen, bauen Sie sich zum Test eine CSV-Datei namens Daten.dat mit ein paar Zeilen Beispieldaten (Dezimalzahlen) in drei Spalten und ändern Sie die Länge von Schleife und Array auf die Zeilenanzahl Ihrer Daten. Versuchen Sie dann, das eingelesene Array dat mit dem Befehl write() aufs Terminal zu schreiben.
Funktionen
Einlesen von Daten und Schreiben von Hello, world! allein ist noch nicht allzu spannend, es wird Zeit für die ersten Berechnungen mit Fortran. Eine Berechnung, die Sie öfter in Ihrem Code nutzen, sollten Sie wie in jeder Programmiersprache in eine Funktion verpacken. Eine Funktionsdeklaration beginnt mit der Angabe des Rückgabetyps, gefolgt vom Schlüsselwort function, dem Namen der Funktion und der Liste der Argumente. Welchen Typ die Argumente haben, legen Sie innerhalb der Funktion fest:
real(8) function plusdurch(a,b,c) implicit none real(8) a,b,c,plus plus=a+b plusdurch=plus/c end function plusdurch
Den Inhalt einer Funktion können Sie für bessere Lesbarkeit einrücken, für den Compiler macht das keinen Unterschied. Einen Return-Befehl, den Sie vielleicht aus anderen Sprachen kennen, gibt es nicht. Stattdessen wird die Variable zurückgegeben, die so heißt wie die Funktion selbst.
Diese Funktion können Sie nun im Hauptprogramm aufrufen, etwa folgendermaßen:
program helloworld implicit none real(8) plusdurch real(8) ergebnis character(12) hi hi='Hello, world' ergebnis=plusdurch(5.d0,6.d0,2.d0) write(*,'(A12,F5.2)') hi,ergebnis end program helloworld
Das bringt im Terminal die folgende Zeile hervor:
Hello, world 5.50
Dem String von 12 Zeichen folgt eine reelle Zahl. Für Letztere wurde ein Format gewählt, das fünf Zeichen Weite erlaubt, wovon zwei für Nachkommastellen freigehalten werden.
Sie können frei wählen, ob Sie die Funktion plusdurch in derselben Textdatei wie das Programm helloworld definieren wollen, oder ob Sie eine neue Textdatei dafür anlegen wollen. Das ist bei längeren Programmen von Vorteil, um besser den Überblick behalten zu können. In dem Fall können Sie eine neue Datei, zum Beispiel funktionen.f90, anlegen und darin plusdurch definieren. Dann müssen Sie nur noch am Anfang der Datei helloworld.f90 die folgende Zeile einfügen:
include "funktionen.f90"
Subroutinen
Neben Funktionen gibt es ein weiteres Konstrukt, um mehrfach verwendeten Code auszulagern. Während Funktionen ihr übergebene Argumente verarbeiten und einen Wert, zum Beispiel das Ergebnis einer Rechnung, zurückgeben, verändern sogenannte Subroutinen direkt die Werte, die man ihnen übergibt. Solche Subroutinen kann man einsetzen, um ein Programm zu strukturieren. Doch Achtung: Weil Subroutinen die Variablen ändern, die man hineinsteckt, kann man sich auch leicht Fehler einbauen.
Als Beispiel folgt eine Subroutine namens plusdurch56(durch,resultat), die die oben deklarierte Funktion plusdurch(a,b,c) mit a=5 und b=6 aufruft und für c das Argument durch einliest, um letztendlich das Ergebnis dem Argument resultat zuzuordnen:
subroutine plusdurch56(durch,resultat) implicit none real(8) plusdurch,durch,resultat resultat=plusdurch(5.d0,6.d0,durch) end subroutine plusdurch56
Ähnlich wie Funktionen können auch Subroutinen im Hauptprogramm aufgerufen werden, man muss lediglich den Befehl call voranstellen:
program hello [...] call plusdurch56(4.d0,ergebnis) write(*,'(F5.2)') ergebnis end program hello
Im Terminal erscheint dann der Wert 2.75, was das korrekte Resultat der Rechnung (5+6)/4 ist.
Fazit
Mit diesem Rechenbeispiel endet diese Einführung in Fortran. Sie haben eine Entwicklungsumgebung eingerichtet, die Besonderheit der impliziten Variablendeklaration kennengelernt, Datentabellen eingelesen, Variablen formatiert, gerechnet und Zahlen sowie Text wieder ausgegeben.
In den Fortran-Dokumentationen im Internet (siehe ct.de/ytdy) werden Sie schnell weitere Rechenoperationen und Beispiele finden, die Sie auch mit eigenen Daten anwenden können.
In einer der nächsten Ausgaben der c’t gehen wir darauf ein, indem wir ein echtes Fortran-Programmierbeispiel aus der Teilchenphysik vorstellen: Sie dürfen sich darauf freuen, mit Originaldaten vom „Large Hadron Collider“ (LHC) des CERN und durch Anpassen von Kurven mittels Fortran-Code neue Teilchen zu entdecken. (jam@ct.de)
Programmbeispiele: ct.de/ytdy