Fortran im Wandel der Zeit

Trotz ihres stolzen Alters von knapp sechzig Jahren ist Fortran heute noch an allerlei Orten anzutreffen. Besonders für Simulationsaufgaben steht die Sprache aufgrund ihres guten Performance noch immer hoch im Kurs.

Sprachen  –  32 Kommentare
Fortran im Wandel der Zeit

Wer heute Informatik-Studenten mit der Programmiersprache Fortran kommt, erntet Unwissen und – wenn er ein typisches Legacy-Codebeispiel zeigt – Gelächter. Konstrukte wie arithmetisches if, eine Menge von goto-Anweisungen und obskure Schleifen in do-continue-Form wirken wie aus der Zeit gefallen und widersprechen nahezu allen Paradigmen guter, strukturierter oder gar objektorientierter Softwareentwicklung.

Wer auf der anderen Seite die Liste der Codes in der aktuellen Benchmark-Sammlung der Standard Performance Evaluation Corporation (SPEC) für Gleitkommaarithmetik CFP2006 [1] studiert, findet dort zur Hälfte Fortran-Codes, neben solchen in C und C++. Noch Ende der Achtzigerjahre galten Fortran-Codes beziehungsweise die entsprechenden Compiler mit numerischen Rechnungen (also vorwiegend in Gleitkommaarithmethik) den entsprechenden C-Varianten in der Performance als überlegen. Konsequenterweise hat, wer mit eher technischen oder naturwissenschaftlichen Anwendungen wie Strömungs- oder Klimasimulationen arbeitet, fast zwangsläufig mit Fortran-Legacy-Code zu tun.

Fortran, der "Formula Translator", war die erste höhere Programmiersprache und wurde von John Backus entwickelt, auch bekannt durch die Backus-Naur-Form der Beschreibung von Programmiersprachen. Die erste Version erschien bereits 1957. Die Idee hinter der Sprache war, Programmierern, die Formeln implementieren wollten, eine komfortablere Schnittstelle zur Verfügung zu stellen als sie Assembler-Anweisungen darstellen. Der treibende Gedanke lässt sich daher mit der von späteren numerischen Lineare-Algebra-Paketen wie LINPACK/LAPACK und Matlab vergleichen: Komplexe Algorithmen und Bibliotheken sollten sich durch einfache Schnittstellen nutzen lassen, vor allem um den Nutzern die Programmierung zu vereinfachen. Auf höherer Abstraktionsebene verfolgen die Entwickler unterschiedlicher Domain Specific Languages (DSL) dieses Anliegen noch heute. Die Bedeutung von Fortran wird in der Wahl des optimierenden Fortran-Compilers als einem der zehn besten Algorithmen des 20. Jahrhunderts durch die Society for Industrial and Applied Mathematics [2] deutlich.

Kurze Geschichtsstunde

Wenn man das Veröffentlichungsdatum von Fortran berücksichtigt und sich in die Computerwelt von damals hineinversetzt, lassen sich viele heute seltsam wirkende Sprachelemente besser verstehen: Es war die Zeit der Lochkarten. Feste Formatierung (die besondere Bedeutung, die die ersten sechs Spalten jeder Zeile in alten Fortran-Dialekten haben und die maximale Zeilenlänge von 72) sowie implizite Typisierung von Variablen und das erwähnte arithmetische if sind durch das Einsparen von Zeilen und Zeichen im Code motiviert.

Ihre Verbreitung und die Bedeutung der Sprache für numerische Simulationen hat allerdings dazu geführt, dass Fortran kontinuierlich weiterentwickelt und wiederholt neu standardisiert wurde. Heute verfügt sie über nahezu alle Eigenschaften einer modernen und objektorientierten Sprache, und noch einige praktische mehr.

Insofern gibt es zwei Sichtweisen auf Fortran. Bei Code kommt es wesentlich darauf an, wann beziehungsweise nach welchem Standard er entstanden ist. Wegen der Kompatibilität treten unterschiedliche Dialekte der Sprache oft gemischt in Legacy-Code auf, was nicht gerade zur Klarheit und Lesbarkeit beiträgt. Man kann grob in Code bis einschließlich Fortran 77 und ab Fortran 90 einteilen, weil dort der größte Umbruch im Standard stattfand. Ganz alter Code im Standard vor Fortran 77, bei dem es teilweise noch keine Schleifen gab, ist vergleichsweise selten. Wenn man hingegen ein neues Programm oder ein ergänzendes, sauber getrenntes Modul schreiben will, ist die Historie unbedeutend. Es kommt dann nur darauf an, welche Möglichkeiten Fortran im aktuellen Standard bietet.

Fortran 77

Genauer betrachtet

Fortran-Compiler gibt es viele, die meisten bieten Optionen zur Wahl des Sprachstandards an. Für den Artikel wurde der GNU-Compiler gfortran verwendet. Aber auch Intel, Portland, NAG, Lahey, Absoft, IBM und andere bieten entsprechende Tools an. Sie unterscheiden sich teilweise dadurch, ob sie Variablen mit Nullen initialisieren, was bei einer Portierung schon in manchem Legacy-Projekt für Überraschungen gesorgt hat.

Der weit verbreitete Standard Fortran 77 ist eine kompakte und übersichtliche Sprache, der man ihr ursprüngliches Ziel anmerkt: mit knappen Anweisungen numerische Operationen leicht programmieren zu können. Listing 1 zeigt ein Beispielprogramm, das zwei simple Berechnungen durchführt und dafür die wesentlichen Sprachkonstrukte benutzt:

1        program simple_fortran
2 c berechne Maschinengenauigkeit:
3 x = 1.0
4 do 100
5 y = x
6 x = x/2.0
7 if (1.0+x .eq. 1.0) goto 200
8 100 continue
9 200 print*, 'Maschinengenauigkeit = ', y
10
11 c berechne Summe:
12 write(*,*) 'Bitte n eingeben:'
13 read(*,*) n
14 sum = 0.0
15 do 300 k=1, n
16 sum = sum + 1.0/k
17 300 continue
18 write(*,10), 'Summe der Kehrwerte der ersten ',
19 & n, ' natuerlichen Zahlen = ', sum
20 10 format(a,i3,2x,a,f10.2)
21 end

Listing 1: Fortran-77-Programm

Schnell aufgezählt sind die Besonderheiten von Fortran 77: Die Standardendung für Quelldateien ist .f und es wird im Quelltext nicht zwischen Groß- und Kleinschreibung unterschieden. In altem Code lassen sich oft durchgehend großgeschriebene Schlüsselwörter finden. Zudem ist die sogenannte "Fixed Form" zu nennen. Sie ist durch die besondere Bedeutung der Spalten 1 (ein beliebiges Zeichen markiert die Zeile als Kommentar, siehe Zeilen 2 und 11 in Listing 1), 2-5 für Labels (Zeilen 8,9,17,20) und Spalte 6 für eine "Continuation Mark" (das die Zeile mit der vorangehenden verbindet, siehe Zeile 19) gekennzeichnet.

Jedes kompilierbare Programm benötigt einen Block program, vergleichbar mit der main-Funktion in C, C++ oder Java. Eine weitere Modularisierung lässt sich durch Funktionen mit (function) und ohne Rückgabewert (subroutine) erreichen.

Die elementaren Kontrollstrukturen in Fortran 77 sind if-else-Konstrukte (Zeile 7 in der einfachsten Form) und die do-Schleife. Sie dient zum einen als Zählschleife, vergleichbar einem for in anderen Sprachen (Zeilen 15-17 in Listing 1). Zum anderen sind mit einem goto ein Sprung und damit beliebige Varianten von (do-)while-Schleifen möglich, wie in den Zeilen 4 bis 8 zu sehen.

Fortran ist eine Sprache mit impliziter Typisierung: Alle Variablen, die mit i bis n beginnen, sind ganzzahlig (Datentyp Integer), wie im Beispiel die Zählvariable k und die eingelesen Obergrenze der Schleife n. Alle anderen Variablen sind Gleitkommazahlen mit einfacher Genauigkeit (Real, im Beispiel x,y und sum). Variablen lassen sich explizit deklarieren, was im Sinne einer guten Programmierpraxis zu empfehlen ist.

Erst Fortran 90 führte das implicit none-Statement ein, dass die implizite Typisierung verbietet und immer verwendet werden sollte. Gleitkommazahlen in doppelter Genauigkeit sind vom Typ Double Precision oder real*8 beziehungsweise real(8), was alles äquivalent ist. In neueren Standards kommt letztere Schreibweise zum Einsatz.

In Zeile 16 findet sich eine automatische Typkonvertierung, die Fortran durchführt: Bei der Division der real-Konstanten 1.0 durch einen Integer-Wert führt Fortran eine Konvertierung in den Typ Real durch. Das entspricht dem Vorgehen in C.

Zeilen 9, 12, 18 und 19 zeigen die beiden Möglichkeiten der Ausgabe. Bei der write-Anweisung lässt sich das erste Argument zur Angabe eines Dateideskriptors nutzen, wenn in eine Datei zu schreiben ist. Analog ermöglicht der read-Befehl nicht nur das Lesen einer Standardeingabe (wie in Zeile 13), sondern zudem das aus einer Datei. Die format-Anweisung aus Zeile 20 ist ebenfalls oft in Legacy-Code zu finden. Die dort angegebene Parametersequenz kann, in Hochkommas und runden Klammern als String, auch direkt für das Label 10 als zweiter Parameter der write-Anweisung in Zeile 18 eingesetzt werden.

Ein in ähnlicher Form oft zitiertes und skurriles Beispiel für einen durch einen Tippfehler und die implizite Typisierung entstehenden Fehler zeigt Listing 2:

1        program test
2
3 do 5 K=1. 3
4 write(*,*) K
5 5 continue
6 write(*,*) do5K, d a t e
7
8 end

Listing 2: Tippfehler

In ihm steht in der do-Anweisung statt des korrekten Kommas ein Punkt. So wird aus dem Schleifenbeginn eine Zuweisung an die implizit deklarierte Variable do5K. Das continue weiter unten bewirkt daher gar nichts, es stört den Compiler aber auch nicht. Fortran ignoriert sogar Leerzeichen, was in einem Handbuch der Firma Sun [3] treffend kommentiert wird: "Consistently separating words by spaces became a general custom about the tenth century A.D., and lasted until about 1957, when FORTRAN abandoned the practice." (gefunden in [4]).

Die Ausgabe des Programms aus Listing 2 ist damit eine Knobelaufgabe. Ihre Lösung: In Zeile 3 weist das Programm der implizit deklarierten Variable do5K den Wert 1.3 zu. In Zeile 4, die jetzt eine einfache Anweisung und nicht Teil einer Schleife ist, wird der Wert der ebenfalls implizit deklarierten Integer-Variable k ausgegeben. Da sie nicht initialisiert wurde, steht in ihr das, was eben gerade dort im Speicher steht, interpretiert als ganzzahliger Wert. In Zeile 6 gibt das Programm nun den Wert von do5K aus, und aus d a t e entsteht durch Ignorieren der Leerzeichen die Variable date, implizit vom Typ Real. Sie ist uninitialisiert, daher gibt die Software an der Stelle ebenfalls den als Gleitkommazahl interpretierten Inhalt des Speichers aus. Eine Internetrecherche nach "famous Fortran bug" schreibt einem solchen Fehler sogar den Absturz der Raumsonde Mariner 1 zu, was andernorts dann wieder dementiert wird.

Beispiele

Beispiele en masse

Listing 3 enthält eine Funktion, ihren Aufruf in einem Hauptprogramm und die Deklaration und Benutzung von Feldern. Es ist möglich, beide Codeteile in einer Datei zu speichern, was im Sinne einer sauberen Strukturierung jedoch nicht zu empfehlen ist.

1        program function_example
2 double precision x(10), y(10)
3 integer k
4 double precision scalar_product
5 do 100 k=1,10
6 x(k) = k**2
7 y(k) = sin(x(k))
8 write(*,*), x(k), y(k)
9 100 continue
10 write(*,*) scalar_product(x, y, 10)
11 end
12
13 double precision function scalar_product(x, y, n)
14 double precision x(n), y(n)
15 integer n, k
16 scalar_product = 0.0d0
17 do 100 k=1,n
18 scalar_product = scalar_product + x(k)*y(k)
19 100 continue
20 end

Listing 3: Funktion mit Aufruf

Im Listing sind alle Variablen explizit deklariert und für die Gleitkommazahlen kommt doppelte Genauigkeit zum Einsatz. Im Hauptprogramm ist die Funktion mit dem Typ ihres Rückgabewertes (Zeile 4) ebenfalls deklariert, was nötig ist, da sie sonst sonst implizit als Typ real angesehen würde, was einen Type-Mismatch nach sich zöge. In der Signatur der Funktion in Zeile 13 ist der Typ des Rückgabewertes dem Schlüsselwort function vorangestellt. Alternativ lässt der Name der Funktion auch in ihr wie eine Variable mit dem entsprechenden Typ deklarieren. In den Zeilen 5 und 6 sind zwei Beispiele für die in Fortran vorhandenen mathematischen Funktion zu erkennen (** bezeichnet die Potenzfunktion).

Listing 4 stellt eine Subroutine zum Vertauschen der Werte zweier Variablen und ihre Verwendung in einem Hauptprogramm dar:

1        program subroutine_example
2 integer i, j
3 i = 1
4 j = 2
5 write(*,*) "vorher: i= ", i, "j = ", j
6 call swap(i, j)
7 write(*,*) "nachher: i= ", i, "j = ", j
8 end
9
10 subroutine swap(i, j)
11 integer i, j, k
12 k = i
13 i = j
14 j = k
15 end

Listing 4: Wertetausch

Es zeigt, dass Fortran mit Referenzen der Parameter arbeitet (Call by reference). Das sorgt dafür, dass auch im Hauptprogramm nach dem Aufruf der Subroutine die Werte der Variablen i und j vertauscht sind. In vergleichbarem C-Code wäre das ohne das Verwenden von Referenzen oder Zeigern nicht der Fall.

Ein weiteres charakteristisches und weit verbreitetes Element in Fortran-77-Programmen sind die sogenannten Common-Blöcke, die globale Variablen zusammenfassen. Jeder Block bezeichnet jedoch nur einen gemeinsamen Speicherbereich. Wie Entwickler ihn in einzelnen Subroutinen und Funktionen auf Variablen verteilen, steht ihnen frei. Listing 5 zeigt ein (nicht empfehlenswertes) Beispiel:

1        program common_blocks
2 common / cb / x, y
3 real x, y
4 x = 1.0
5 y = 2.0
6 write(*,*) 'im Hauptgrogramm: ', x, y
7 call sub()
8 end
9
10 subroutine sub()
11 common / cb / z
12 double precision z
13 write(*,*) 'in der Subroutine: ', z
14 end

Listing 5: common-Block

Im Hauptprogramm und in der Subroutine kommt der gleiche Common-Block mit dem Namen cb zum Einsatz, das Hauptprogramm interpretiert ihn jedoch mit zwei Gleitkommazahlen x, y einfacher und die Subroutine mit nur einer (z) von doppelter Genauigkeit. Beidesmal wird auf den gleichen gemeinsamen (common) Speicher zugegriffen, er wird aber Variablen unterschiedlichen Typs zugewiesen. Dass Fehler in dem Kontext besonders häufig passieren, ist klar.

In der Vor-Fortran-90-Welt gibt es weder dynamische Felder noch selbstdefinierte, strukturierte Datentypen. Auch Zeiger gehören nicht zum Standard. Viele Compiler-Hersteller kennen jedoch Cray-Pointer: ganzzahlige Variablen, die mit C-Pointern vergleichbar und kompatibel sind. Mit einer include-Anweisung lassen sich Dateien einbinden, die dann oft mit der Endung .inc versehen sind. Oft werden in Legacy-Code so globale Variablen in Common-Blöcken eingebunden.

Ganz alter Code (vor dem Standard Fortran IV, weit vor Fortran 77) verfügt noch nicht über die do-Schleife. Das führt zu abenteuerlichen Konstrukten: Listing 6 enthält die reale Codestruktur einer alten Funktion in einem Klimamodell:

1        IF(A.LE.1.0E-08) GO TO 10
2 B=ATAN(C/D)
3 IF(D) 11,10,12
4 11 B=B+E
5 GO TO 13
6 12 IF(C) 14,13,13
7 14 B=B+2.0*E
8 GO TO 13
9 10 IF(C) 15,16,17
10 15 B=1.5*E
11 GO TO 13
12 16 B=0.0
13 GO TO 13
14 17 B=E/2.0
15 13 F=B/G

Listing 6: Klimamodell

In ihm kommt das erwähnte arithmetische if zum Einsatz (Zeilen 3, 9). Je nachdem ob der Wert in der Klammer hinter dem if kleiner, gleich oder größer als Null ist, springt das Programm zu dem entsprechenden der drei dahinter angegebenen Labels. Solcher Code widerspricht so ziemlich allem, was man heute unter guter, strukturierter Programmierpraxis versteht und ist kaum lesbar.

Auf alle Details von Fortran 77 soll hier nicht eingegangen werden. Wer in die Welt vor Fortran 90 eintauchen will oder mit altem Code oder Codeteilen zu tun hat, kann in alten Auflagen entsprechender Bücher (z.B. [5]) stöbern. Viele davon sind antiquarisch für Centbeträge erhältlich.

Insgesamt hat Fortran 77 damit einige Möglichkeiten, die dazu verleiten, unsauberen und damit fehleranfälligen Code zu programmieren. Andererseits kann bei einer konsequent expliziten Typisierung und dem Verzicht auf abenteuerliche Konstrukte ein schneller, leicht lesbarer Code entstehen.

Ein Vorteil von Fortran 77 ist die leichte Ähnlichkeit zu C-Code. Da die Datentypen gegenseitige Entsprechungen haben (C: float – Fortran: real, doubledouble precision oder real(8), intinteger) gibt es an der Stelle keine Probleme. Man sollte die Entsprechungen jedoch bei den jeweiligen Compilern genau überprüfen. Auch Felder lassen sich von einer Sprache zur anderen etwa bei Funktionsaufrufen übergeben, da die C-Pointer mit den Referenzen der Fortran-Felder kompatibel sind. Zu beachten ist bei mehrdimensionalen Feldern die umgekehrte Speicherung: C speichert sie zeilen-, Fortran spaltenweise. Listing 7 zeigt ein Beispiel für einen Aufruf der Fortran-Subroutine aus Listing 4 von einem C-Programm:

1  #include <stdio.h>
2
3 extern int swap_(int*, int*);
4
5 int main() {
6 int i=1, j=2;
7 printf("vorher: i = %i, j =%i\n",i,j);
8 swap_(&i,&j);
9 printf("nachher: i = %i, j =%i\n",i,j);
10 return 0;
11 }

Listing 7: Aufruf Fortran aus C

Man beachte die Adressoperatoren für die integer-Variablen in C, denn Fortran kennt eben nur "Call by reference". Der eingesetzte GNU-Compiler übersetzte zuerst mit gfortran -c swap.f die Fortran-Subroutine, die für sich alleine in einer Datei swap.f steht, und erzeugte so die Objektdatei swap.o. Im Anschluss wurde der analoge Schritt für das C-Hauptprogramm aus Listing 7 mit gcc ausgeführt. Danach ließ sich mit gfortran listing7.o swap.o aus beiden Objektdateien ein ausführbares Programm generieren. Je nach Compiler ist der Unterstrich an den Namen der Fortran-Subroutine (Zeilen 3 und 8 im C-Programm) anzuhängen.

Fortran 90

Next Generation Fortran

Mit der Version 90 beginnt gewissermaßen die zweite Epoche der Sprache Fortran. Mit ihr wurden viele der genannten Schwächen und Mängel beseitigt. Die meisten Compiler identifizieren Fortran-90-Programme anhand der Endung .f90. Um den Code besser abgrenzen zu können, sind entsprechende Programme häufig durchgängig kleingeschrieben, obwohl die Sprache weiterhin keinen Unterschied zwischen Groß- und Kleinschreibung macht. Oft lässt sich zudem auf den Vorschlag treffen, die Rümpfe von Subroutinen und Funktionen nicht einzurücken. Apples Entwicklungsumgebung Xcode, die ein Fortran-Syntax-Highlighting hat, formatiert Fortran-Code zum Beispiel automatisch so, was allerdings nicht unbedingt zur Lesbarkeit beiträgt. Eine strenge Regel gibt es nicht, sodass sich Entwickler bei Legacy-Code auf allerhand mehr oder weniger wilde Formatierungen einstellen sollten.

Die meisten Compiler ermöglichten mit Einführung der neuen Version ebenfalls einen vorgeschalteten Durchlauf des C-Präprozessors, meist bei Verwendung der Dateiendung .F. Damit wurden die alten Fortran-include-Statements oft durch #include-Präprozessor-Direktiven ersetzt. Präprozessoranweisungen werden allerdings oft mit ihren #if-Direktiven zum bedingten Kompilieren verwendet, was für extrem schwer lesbaren Code sorgen kann. In manchen Programmcodes finden sich Subroutinen, die direkt mit einem #if beginnen und dem zugehörigen #endif enden. Der Compiler übersetzt den gesamten Code folglich nur bei entsprechendem Wert in der in der Direktiven genannten Variable. Ein sinnvoller Einsatz von Optionsdateien, die Parameter setzen mit denen sich zur Laufzeit der Aufruf entsprechender Funktionen steuern lässt, ist leider nicht immer gegeben.

Wesentlich in Fortran 90 ist zunächst die Einführung des sogenannten Free Format, bei dem die alte Regelung der Spaltenbedeutungen aufgehoben wurde. Die Continuation Mark ist nun ein <I>&<I> am Ende der Zeile, die mit der nachfolgenden zu verbinden ist. Eine modernere Strukturierung von Schleifen ermöglichen die besser lesbaren do-Anweisungen mit einem enddo statt der Kombination aus Label und continue am Ende sowie die do-while-Variante. Hinzu kommen eine break-Anweisung zum vorzeitigen Verlassen von Schleifen und ein case/switch-Konstrukt. Sie bieten Fortran 90 die Optionen, die in anderen Sprachen wie C vorhanden sind.

Listing 8 zeigt die Fortran-90-Version des Programms aus Listing 1 im Free Format:

1  program fortran90
2 implicit none
3 real x, y, sum
4 integer n, k
5 ! berechne Maschinengenauigkeit:
6 x = 1.0
7 do while (1.0+x > 1.0)
8 y = x
9 x = x/2.0
10 enddo
11 print*, 'Maschinengenauigkeit = ', y
12
13 ! berechne Summe:
14 write(*,*) 'Bitte n eingeben:'
15 read(*,*) n
16 sum = 0.0
17 do k=1, n
18 sum = sum + 1.0/k
19 enddo
20 write(*,'(a,i3,2x,a,f10.2)'), ↲
'Summe der Kehrwerte der ersten ', ↲
n, ' natuerlichen Zahlen = ', sum
21 end program

Listing 8: Fortran-90-Programm

Es gibt nun eine do-while-Schleife (Zeilen 7-10), das continue ist durch ein enddo ersetzt und die Labels werden überflüssig. Ein ! leitet Kommentare ein: Alles, was in der Zeile folgt, ignoriert der Compiler. Die alten Vergleichsoperatoren bleiben gültig, werden aber durch die gebräuchlichere Form ergänzt (> alternativ zu .gt. für "greater than" im Beispiel, vgl. das .eq. für equal in Listing 1). Zudem kommt die implicit none-Anweisung zum Einsatz, sie erzwingt die Deklaration aller Variablen und muss direkt zu Beginn jedes Programms und jeder Funktion beziehungsweise Subroutine stehen, für die sie gilt.

Die Einführung dynamischer Felder (Arrays) ist eine weitere wesentliche Neuerung in Fortran 90. Ein Beispiel ist in Listing 9 zu sehen:

1  program arrays
2 implicit none
3 integer :: n = 3
4 real(8), allocatable :: x(:)
5 ! real(8), allocatable, dimension(:) :: x
6 real(8), allocatable :: A(:,:)
7 real(8), allocatable, target :: y(:)
8 real(8), pointer :: p(:)
9 real(8) d
10 allocate(x(n))
11 allocate(y(n))
12 allocate(A(n, n))
13 x = 1.0d0
14 y = 2.0d0*x - 3.0d0
15 p => y
16 A = 3.0d0
17 d = dot_product(x, y)
18 A = matmul(A, A)
19 write(*,*) d
20 write(*,*) x
21 write(*,*) y
22 write(*,*) p
23 write(*,*) A
24 deallocate(x, y, A)
25 end program

Listing 9: Dynamische Felder

Das Programm erzeugt in Zeile 4 ein dynamisches Feld, gibt ihm in Zeile 9 Speicherplatz und gibt letzteren in Zeile 23 wieder frei. Die doppelten Doppelpunkte in der Deklaration sind jetzt notwendig, weil das Schlüsselwort allocatable anzugeben ist. Im Listing findet die erwähnte moderne Schreibweise real(8) für Gleitkommazahlen doppelter Genauigkeit Verwendung. Die auskommentierte Zeile 5 zeigt eine äquivalente Schreibweise. In Zeile 6 ist das Anlegen eines zweidimensionalen Felds zu sehen und in Zeile 8 ein Zeiger, der auf ein eindimensionales Feld zeigen soll, was er in Zeile 15 tut. Variablen, auf die ein Pointer zeigen soll, müssen das target-Attribut erhalten. Die Fortran-90-Pointer sind nicht mit denen aus C vergleichbar oder kompatibel. Die Umsetzung dynamischer Arrays ist insgesamt weniger fehleranfällig als in C, weil dafür selbst keine Pointer nötig sind. Darüber hinaus wurden Operationen auf ganzen Feldern ermöglicht (Zeilen 13, 14 und 16) und diverse Funktionen auf Feldern eingeführt (im Listing in Zeilen 17, 18 für das Skalar- und Matrixprodukt), was für wesentlich kürzeren und besser lesbaren Code sorgt und der Matlab-Syntax ähnelt. Die Endung d0 bei den Konstanten in Zeilen 13, 14 und 16 ist die korrekte Schreibweise im wissenschaftlichen Format für doppelt genaue Konstanten (daher d), mit Mantisse (vor dem d) und Exponent (hinter dem d). Das wäre an der Stelle nicht nötig, da sonst wie auch in C eine automatische Konvertierung von einfacher Genauigkeit in den Typ der Variablen (also doppelte Genauigkeit) stattfände.

Derived Types und Module

Eine wichtige Ergänzung in Fortran 90 sind selbstdefinierte, strukturierte Datentypen, sogenannte "Derived Types", die im Verlauf noch beschrieben werden. Außerdem hat das Standardisierungsgremium das Konzept der Module eingeführt, die unter anderem ein Ersatz für Common-Blöcke sind. Wie in Letzteren lassen sich dort globale Variablen zusammenfassen. Danach ist es möglich, auf sie von jeder Funktion oder Subroutine mit der Anweisung use <Modulname> zuzugreifen. Mit use <Modulname>, only: <Variablenname> ist die Nutzung ebenfalls eingeschränkt möglich. Listings 10 und 11 zeigen die Alternative zu den Common-Blöcken aus Listing 5 beim Verwenden der Module: Das Modul in Listing 10 enthält die Deklaration der beiden Variablen, in Listing 11 wird das Modul eingebunden und es lässt sich auf globale Variablen zugreifen.

1  module xy_module
2 real x, y
3 end module

Listing 10: Definition eines Moduls

1  program modules
2 use xy_module
3 x = 1.0
4 y = 2.0
5 write(*,*) 'im Hauptgrogramm: ', x, y
6 call sub()
7 end program
8
9 subroutine sub()
10 use xy_module
11 write(*,*) 'in der Subroutine: ', x, y
12 end subroutine

Listing 11: Verwendung des Moduls

Zu beachten ist, dass das Modul zuerst zu kompilieren ist. Die entstehende .mod-Datei ist im Anschluss zum Kompilieren der das Modul benutzenden Codeteile nötig.

Dabei entspricht der Name der .mod-Datei dem des Moduls, nicht dem der Quelldatei des Moduls. Das Generieren von Makefiles mit korrekten Abhängigkeiten ist damit nicht ganz trivial. Die .mod-Dateien sind außerdem zwischen den Compilern nicht unbedingt kompatibel, was bei plattformübergreifendem Übersetzen ein Problem darstellen kann.

Das simple Nutzen von Modulen als Ersatz von Common-Blöcken und damit als Ort der Deklaration globaler Variablen findet man oft in Legacy-Code. Module werden unübersichtlich, wenn sie wieder use-Anweisungen enthalten und andere Module einbinden, was eine Verschachtelung bewirkt. Oft ist Detektivarbeit nötig, um in komplexem Code wirklich die Abhängigkeiten von in Modulen definierten und mit ihnen eingebundenen globalen Variablen zu finden. Nicht ohne Grund warnt jede gute Programmierausbildung vor dem Einsatz Letzterer. Doch Module können mehr, denn mit ihnen beginnt gewissermaßen das Zeitalter der Objektorientierung in Fortran. Erst mit ihr werden sie wirklich sinnvoll.

Objektorientierung

Fortran entwickelt sich weiter

Beginnend mit dem Standard Fortran 90 bis zum aktuellen (Fortran 2008) hielten nach und nach Elemente der objektorientierten Programmierung Einzug in die Sprache. Um zu zeigen, was durch sie aktuell möglich ist, folgt die Beschreibung des Implementierens und Verwendens einer Klasse. In der Objektorientierung vereint eine Klasse Daten (Attribute) und Operationen (Methoden). In Fortran lässt sich das durch einen sogenannten Derived Type in einem Modul mit Funktionen und Subroutinen realisieren, die hinter einer contains-Anweisung stehen.

Weitere objektorientierte Konzepte können Entwickler umsetzen, wenn Zugriffsmodifikatoren zur Datenkapselung sowie Überladen von Funktionen und Operatoren dazu kommen. Listing 12 zeigt als Beispiel ein Modul mit der grundlegenden Struktur einer Klasse, mit der sich Brüche, also rationale Zahlen mit ganzzahligem Zähler und Nenner, darstellen und verrechnen lassen:

1   module RationalClass
2 implicit none
3
4 type, public :: Rational
5 private
6 integer num, den
7 end type Rational
8
9 interface Rational
10 module procedure construct
11 module procedure constructInteger
12 end interface
13
14 interface assignment(=)
15 module procedure assignInteger
16 end interface
17
18 interface operator(*)
19 module procedure times
20 module procedure timesRational
21 module procedure timesInteger
22 end interface
21
22 interface abs
23 module procedure absRational
24 end interface
25
26 interface writeRational
27 module procedure writeRationalScalar
28 end interface
29
30 contains
31
32 ! constructors:
33 elemental type(Rational) function construct(num, den)
34 integer, intent(in) :: num
35 integer, intent(in) :: den
36 construct%num = num
37 construct%den = den
38 end function
39
40 elemental type(Rational) function constructInteger(num)
41 integer, intent(in) :: num
42 constructInteger = construct(num, 1)
43 end function
44
45 ! getters:
46 elemental real(8) function getnum(r)
47 type(Rational), intent(in) :: r
48 getnum = r%num
49 end function
50
51 elemental real(8) function getden(r)
52 type(Rational), intent(in) :: r
53 getden = r%den
54 end function
55
56 ! setters:
57 elemental subroutine setnum(r,num)
58 type(Rational), intent(inout) :: r
59 integer, intent(in) :: num
60 r%num = num
61 end subroutine
62
63 elemental subroutine setden(r,den)
64 type(Rational), intent(inout) :: r
65 integer, intent(in) :: den
66 r%den = den
67 end subroutine
68
69 ! assignment operator:
70 elemental subroutine assignInteger(r, num)
71 type(Rational), intent(inout) :: r
72 integer, intent(in) :: num
73 r%num = num
74 r%den = 1
75 end subroutine
76
77 ! multiplication operators:
78 elemental type(Rational) function times(r1, r2)
79 type(Rational), intent(in) :: r1, r2
80 times%num = r1%num * r2%num
81 times%den = r1%den * r2%den
82 end function
83
84 elemental type(Rational) function timesRational(r, i)
85 type(Rational), intent(in) :: r
86 integer, intent(in) :: i
87 timesRational = times(r, Rational(i))
88 end function
89
90 elemental type(Rational) function timesInteger(i, r)
91 type(Rational), intent(in) :: r
92 integer, intent(in) :: i
93 timesInteger = times(Rational(i), r)
94 end function
95
96 ! abs function:
97 type(Rational) function absRational(r)
98 type(Rational), intent(in) :: r
99 absRational = Rational(abs(r%num),abs(r%den))
100 end function
101
102 ! output:
103 subroutine writeRationalScalar(r)
104 type(Rational), intent(in) :: r
105 write(*,*) 'Rational object: ', r%num, '/', r%den
106 end subroutine
107
108 end module RationalClass

Listing 12: Definition einer Klasse

Listing 13 enthält ein einfaches Hauptprogramm dazu. In Listing 12 erkennt man die Deklaration eines selbstdefinerten Datentyps in den Zeilen 4-7. Dort kommen die Zugriffsmodifikatoren public für den Typ selbst und private für die beiden Attribute num (für Numerator, Zähler) und den (für Denominator, Nenner) des Bruches zum Einsatz. Die Definition des Typs Rational erlaubt es nun, im Hauptprogramm (Listing 13, Zeile 5) Variablen dieses Typs anzulegen:

1  program RationalTest
2 use RationalClass
3 implicit none
4
5 type(Rational) :: r1, r2
6 type(Rational), allocatable :: r3(:)
7 integer :: i, n = 3
8
9 r1 = Rational(1, 2)
10 r2 = Rational(3)
11 call writeRational(r1)
12 call writeRational(r2)
13 r2 = r1*r2
14 call writeRational(r2)
15
16 allocate(r3(n))
17 r3 = Rational(0)
18 r3 = r3 * r3
19 do i=1,n
20 call writeRational(r3(i))
21 enddo
22 deallocate(r3)
23
24 end program

Listing 13: Benutzung der Klasse

Hinter der Typdefinition folgen im Modul nun diverse Interface-Blöcke. Im ersten (Zeilen 9-12) findet die Definition des Interface Rational mit zwei im Modul definierten Prozeduren statt. Der Begriff procedure steht an der Stelle für Fortran-Funktionen oder Subroutinen.

Ruft das Hauptprogramm (Listing 13, Zeilen 9, 10) Rational auf, wird in den im Interface-Block aufgeführten Modulprozeduren diejenige gesucht, die mit ihren Parametern passt. Im vorliegenden Fall ruft Zeile 9 des Hauptprogramms so über das Interface Rational die Funktion construct aus Zeile 35 des Moduls auf, die zwei Integer-Parameter bekommt. Zeile 10 des Hauptprogramms ruft dagegen diejenige mit nur einem Parameter auf (Zeile 42 im Modul). Auf völlig identische Weise lassen sich nun Funktionen und Operatoren, wie hier der Multiplikationsoperator, überladen. Bei den Prozeduren im Modul wurden zusätzlich die jeweiligen Parameter als intent(in) beziehungsweise out und inout gekennzeichnet. Der Compiler prüft diese in Fortran 90 eingeführte Spezifizierung, die einen zusätzlichen Schutzmechanismus darstellt.

Im Hauptprogramm lässt sich erkennen, dass es direkt ein Feld (Variable r3) des neuen Datentyps erzeugen, initialisieren und mit ihm rechnen konnte. Das kann funktionieren, wenn die entsprechenden Prozeduren wie im Modul als elemental gekennzeichnet sind. Das ermöglicht eigenen Datentypen und Klassen eine komfortable Syntax, die der für Felder primitiver Datentypen entspricht.

Auch Vererbung ist in Fortran möglich. Da man solche modernen Konstrukte in Legacy-Code eher nicht findet und auch die Bedeutung von Vererbung in numerischen Simulationscodes umstritten ist, wird hier allerdings nicht näher darauf eingegangen.

Eine sehr kompakte, aber dennoch umfassende Darstellung des modernen Fortran ist in [4] zu finden – nicht nur im Hinblick auf Klimamodelle, wie der Titel andeuten könnte. Daneben sei Interessierten das Buch "Modern Fortran Explained"[6] empfohlen, das weniger Lehrbuch als Nachschlagewerk zum jeweils aktuellen Sprachstandard ist. Die Autoren sind an dessen Gestaltung beteiligt. In "Computing for Scientists" [7] wird ein interessanter Vergleich zwischen Fortran 90 und C++ im Hinblick auf Objektorientierung gezogen, die beim Erscheinen des Buches in Fortran aber noch nicht vollständig realisiert wurde.

Fazit

In Legacy-Code sind moderne objektorientierte Konstrukte naturgemäß selten. Was man zumindest in Klimamodellen findet, ist der oft unübersichtliche Einsatz von Modulen und Präprozessoranweisungen. Das ist jedoch gerade die typische Herausforderung komplexer Legacy-Projekte: Man kann das gesamte Programm nicht mal eben durch eine Neuimplementierung ersetzen, und ein Umschreiben in Teilen ist auch nicht leicht möglich, weil oft saubere Schnittstellen fehlen. Bei Tools zur Softwareentwicklung sieht es dagegen gut aus: Das aktuelle Eclipse in der Version für parallele Anwendungen verfügt über eine gute und komfortable eingebaute Fortran-Unterstützung. Dem Ausprobieren steht daher nichts im Wege. (jul)

Thomas Slawig
ist Professor für Algorithmische Optimale Steuerung am Institut für Informatik der Universität Kiel und beschäftigt sich vor allem mit Klimamodellen. Mit der Sprache Fortran arbeitet er seit etwa 20 Jahren.

Literatur

  1. Floating Point Component of SPEC CPU2006; www.spec.org/cpu2006/CFP2006
  2. B. A. Ciapra; The Best of the 20th Century: Editors Name Top Ten Algorithms; Society for Industrial and Applied Mathematics (SIAM) News Vol. 33 No. 4, 2000
  3. FORTRAN 77 4.0 Reference Manual, SunSoft, Mountain View 1995.
  4. D. B. Chirila, G. Lohmann; Introduction to Modern Fortran for the Earth System Sciences, Springer 2015.
  5. H. Wehnes; FORTRAN 77. Strukturierte Programmierung mit FORTRAN 77; Hanser Verlag, 5. Auflage 1989.
  6. M. Metcalf, J. Reid, M. Cohen; Modern Fortran Explained; Oxford University Press 2011.
  7. R. J. Barlow, A. R. Barnett; Computing for Scientists, Principles of Programming with Fortran 90 and C++; Wiley 1998.