Modern Fortran for today and tomorrow
Up To Date
Fortran has been one of the key languages for technical applications since almost forever. With the rise of C and C++, the popularity of Fortran waned a bit, but not before a massive library of Fortran code had been written. The rise of scripted languages such as Perl, Python, and R also dented Fortran a bit, but it is still in use today for many excellent reasons and because of a HUGE library of active Fortran code.
Fortran 90 took Fortran 77 from the dark ages by giving it new features that developers had wanted for many years and by deprecating old features – but this was only the start. Fortran 95 added new features, including High Performance Fortran (HPF), and improved its object-oriented capabilities. Fortran 2003 then extended the object-oriented features; improved C and Fortran integration, standardizing it, and making it portable; and added a new range of I/O capabilities. Features and capabilities still on the developers' wish lists led to Fortran 2008 and concurrent computing.
I still code in Fortran for many good reasons, not the least of which is performance and readability.
Origins
Fortran was originally developed as a high-level language so code developers didn't have to write in assembly. It was oriented toward "number crunching," because that was the predominant use of computers at the time (Facebook and YouTube weren't around yet). The language was fairly simple, and compilers were capable of producing highly optimized machine language. Fortran had data types that were appropriate for numerical computations, including a complex data type (not many languages have that), and easily dealt with multidimensional arrays, which are also important for number crunching. For all of these reasons, Fortran was the language of numerical computation for many years.
The advent of C caused a lot of new numerical code to be written in C or C++, and web and systems programming also moved to other languages, such as Java, Perl, and Python. However, Fortran is definitely not dead, and a lot of numerical code is still being written today using modern Fortran.
Fortran 77
Fortran began in about 1957 with the advent of the first compiler. It was used a great deal because it was much simpler than assembly language. In my experience, Fortran 77 (F77) brought forth the features that made the language easier to use to create many algorithms. As a result, a lot of older Fortran code was rewritten for F77.
In the past, Fortran was not case sensitive, so a lot of code was written in uppercase, creating a precedent followed for many years. You will find that Fortran coders of some lineage code in uppercase. However, in this article I mix upper- and lowercase in the examples.
In F77, a very useful way to share data across functions and subroutines was the use of common
blocks, in which you define multiple "named" blocks that contain variables. (They could be of mixed type, including arrays.) A typical scenario was to put the common blocks in an include
file and put each function or subroutine into its own file. Each file that needed access to the common block "included" the file of common blocks.
Another feature, or limitation, of Fortran is that all arrays have to be fixed in size at compile time (no dynamic memory), so if you declare an array x(100,100)
, you cannot change the dimensions or size after the code has been compiled. One trick was to define one very large vector and then "give" parts of it to various portions of the code. Although a little messy, you could simulate dynamic memory if the array was large enough.
Fortran also had fixed formatting. Some languages sort of have this today, such as Python, which requires indentation. In Fortran, the first column of each line could contain a C
or c
, indicating the beginning of a comment line, although you couldn't put comments just anywhere in the code. It could also be used for a numerical label. Column 6 was reserved for a continuation mark, so that lines that were longer than one line could be continued. The code began in column 7. Columns 1 to 5 could be used for statement labels (Listing 1; note that the first and last three lines are not code, but guides to show how code aligns in columns).
Listing 1
Sample Fortran 77 Code
11111111112222222222 12345678901234567890123456789 ----------------------------- SUM = 0.0 D0 100 I=1,10 SUM = SUM + REAL(I) 100 CONTINUE ... Y = X1 + X2 + X3 + 1 X4 + X5 + X6 ----------------------------- 11111111112222222222 12345678901234567890123456789
By default, F77 defines variables starting with (upper- or lowercase) i
, j
, k
, l
, m
, and n
as integers, so they are used as do
loop counters. Variables that use any other letter of the alphabet are real unless the code writer defines them to be something else (they could even define them to be integers). You could turn off this implicit definition with IMPLICIT NONE
just after the program, subroutine, or function name, which forces defining each and every variable (not necessarily a bad thing).
Fortran 90
The next Fortran standard, referred to as Fortran 90 (F90), was released in 1991 as an ISO standard, and in 1992 as an ANSI standard. Fortran 90 is a huge step up from F77, with a number of new features added and a number of older features deprecated. Fortran 90 was the first big step in creating modern Fortran.
The first new feature of importance was free-form source input. You were now allowed to put the code anywhere you wanted, and you could label statements, for example:
sum = 0.0 all: do i=1,10 sum = sum + real(i) enddo all
The next big feature in F90 is my personal favorite – allocatable arrays. In Listing 2, line 3 shows how to define an allocatable array. In this case, array a
is a 2D array defined by (:,:)
. The allocation does not occur until line 7. Along with the array allocation is a "status" that returns a nonzero value if the allocation was unsuccessful or 0
if it was successful.
Listing 2
Allocatable Arrays
01 PROGRAM TEST1 02 IMPLICIT NONE 03 REAL, ALLOCATABLE :: a(:,:) 04 INTEGER :: n 05 INTEGER :: allocate_status 06 n=1000 07 ALLOCATE( a(n,n), STAT = allocate_status) 08 IF (allocate_status /= 0) STOP "Could not allocate array" 09 ! Do some computing 10 DEALLOCATE( a ) 11 END PROGRAM TEST1
Allocatable arrays use heap memory and not stack memory, so you can use a lot more memory. For almost any large arrays, I always use allocatable arrays to make sure I have enough memory.
After performing some computations, you then deallocate the array, which returns the storage to the system. Line 9 is a comment line. Fortran 90 allows you to put a comment anywhere by prefacing it with an exclamation mark.
Another feature added to F90 was array programming, which allowed you to use array notation rather than do
loops. For example, to multiply two, 2D arrays together, use c = matmul(a, b)
, where a
, b
, and c
are compatible arrays. You could also use portions of an array with d(1:4) = a(1:4) + b(8:11)
.
Unlike CPU-specific code, typically with loop unrolling to achieve slightly better performance, Fortran coders used array notation because it was so simple to write and read. The code was very portable, because compiler writers created very high performance array intrinsic functions that adapted to various CPUs. Note that this included standard intrinsic mathematical functions such as square root, cosine, and sine.
Fortran 90 also introduced derived data types (custom data types). The simple example in Listing 3 shows how easy it is to create several derived data types (lines 8-15). The derived (customer data) type
has variables (e.g., i
, r
, and r8
), a fixed-size array (array_s
, which is allocated on the stack), an allocatable array (array_h
, which is allocated on the heap), and another derived type, meta_data
. Using derived types within derived types allows you to create some complex and useful custom data types.
Listing 3
Derived Data Type
01 program struct_test 02 type other_struct 03 real :: var1 04 real :: var2 05 integer :: int1 06 end type other_struct 07 08 type my_struct ! Declaration of a Derived Type 09 integer :: i 10 real :: r 11 real*8 :: r8 12 real, dimension(100,100) :: array_s ! Uses stack 13 real, dimension(:), allocatable :: array_h ! Uses heap 14 type(other_struct), dimension(5) :: meta_data ! Structure 15 end type my_struct 16 17 ! Use derived type for variable "a" 18 type(my_struct) :: a 19 ! ... 20 write(*,*) "i is ",a%i 21 22 ! Structures (variables) of the the derived type my_struct 23 type(my_struct) :: data 24 type(my_struct), dimension(10) :: data_array 25 26 end program struct_test
To access a specific part or member of a derived type, you simply use a percent sign (%
), as shown in line 20. You can also make an allocatable derived type (lines 23-24).
A convenient feature called modules, and generically referred to as "modular programming" [1], was added to F90. This feature allows you to group procedures and data together. Code can take advantage of a module, and you can control access to it through the use of simple commands (e.g., public
, private
, contains
, use
). For all intents and purposes, it replaces the old common blocks of F77.
Modules are extremely useful. They can contain all kinds of elements, such as parameters (named constants), variables, arrays, structures, derived types, functions, and subroutines. The simple example in Listing 4 defines the constant pi
and then uses the module (line 7) to make the contents available to the program.
Listing 4
Modules
01 module circle_constant 02 real, parameter :: pi = 3.14159 03 end module circle_constant 04 05 program circle_comp 06 ! make the content of module available 07 use circle_constant 08 real :: r 09 ! 10 r = 2.0 11 write(*,*) 'Area = ', pi * r**2 12 end program circle_comp
The example in Listing 5 uses contains
within a module to access content outside of the module. You can go one step further and denote public
components (the default) that can be used outside the module and private
components that can only be used within the module, giving you a lot more control over what can be accessed by other parts of the code.
Listing 5
Accessing External Content
01 module circle_constant 02 real, parameter :: pi = 3.14159 03 04 type meta_data 05 character(len=10) :: color 06 real :: circumference 07 real :: diameter 08 real :: radius 09 real :: area 10 end type meta_data 11 12 contains 13 subroutine meta_comp(r, item) 14 type(meta_data) :: item 15 item%diameter = 2.0 * r 16 item%area = pi * r **2 17 item%circumference = 2.0 * pi * r 18 end subroutine 19 20 end module circle_constant 21 22 program circle_comp 23 ! make the content of module available 24 use circle_constant 25 real :: r 26 integer :: iloop 27 type(meta_data), dimension(10) :: circles 28 ! 29 r = 2.0 30 circles(1)%radius = 4 31 circles(1)%color = "red" 32 call meta_comp(r, circles(1)) ! Call the module function 33 34 circles(2:10)%color = "blue" ! array operation 35 r = 4.0 36 circles(2:10)%radius = r ! array operation 37 do iloop=2,10 38 call meta_comp(r,circles(iloop)) 39 end do 40 41 end program circle_comp
POINTER
was a new type of variable in F90 that references data stored by another variable, called a TARGET
. Pointers are typically used as an alternative to allocatable arrays or as a way to manipulate dynamic data structures, as in linked lists.
A pointer has to be defined as the same data type and rank as the target and has to be declared a pointer. The same is true for the target, which has to have the same data type as the pointer and be declared a target. In the case of array pointers, the rank, but not the shape (i.e., the bounds or extent of the array), has to be specified. The following are simple examples of a declaration
INTEGER, TARGET :: a(3), b(6), c(9)INTEGER, DIMENSION(:),POINTER :: pt2
and multidimensional arrays:
INTEGER, POINTER :: pt3(:,:) INTEGER, TARGET :: b(:,:)
You cannot use the POINTER
attribute with the following attributes: ALLOCATABLE
, EXTERNAL
, INTRINSIC
, PARAMETER
, INTENT
, and TARGET
. The TARGET
attribute is not compatible with the attributes EXTERNAL
, INTRINSIC
, PARAMETER
, and POINTER
.
Using pointers is very simple, having only two operators (Listing 6). In some code, pointers are also used with blocks of dynamic memory (lines 13-17), for which you still have to use the allocate
statement (function). In this example, PTR2
points to a single real value, and PTRA
points to a block of dynamic memory for 1,000 real values. After you're done using the block of memory, you can deallocate
it. In this regard, the pointers behave very much like allocatable arrays. One other common use for pointers is to divide arrays into smaller sections with the pointer pointing to that subsection (Listing 7).
Listing 6
Pointers
01 PROGRAM PTR_TEST1 02 INTEGER, POINTER :: PTR1 03 INTEGER, TARGET :: X=42, Y=114 04 INTEGER :: N 05 REAL, POINTER :: PTR2:wq:w 06 REAL, POINTER :: PTRA(:) 07 ! ... 08 PTR1 => X ! PTR1 points to X 09 Y = PTR1 ! Y equals X 10 PTR1 => Y ! PTR1 points to Y 11 PTR1 = 38 ! Y equals 38 12 13 ! Dynamic memory blocks 14 N = 1000 15 ALLOCATE( PTR2, PTRA(N) ) 16 ! Do some computing 17 DEALLOCATE(PTR2, PTRA) 18 19 END PROGRAM PTR_TEST1
Listing 7
Pointers and Arrays
01 program array_test 02 implicit none 03 real, allocatable, target :: array(:,:) 04 real, pointer :: subarray(:,:) 05 real, pointer :: column(:) 06 integer :: n 07 integer :: allocate_status 08 ! 09 n = 10 10 allocate( array(n, n), stat = allocate_status ) 11 if (allocate_status /= 0) stop "Could not allocate array" 12 ! 13 subarray => array(3:7,3:7) 14 column => array(:,8) 15 ! 16 nullify(subarray) 17 nullify(column) 18 deallocate(array) 19 20 end program array_test
A linked list [2], a sequence of "nodes" that contain information and point to the next node in the sequence, is a very useful data structure. Variations allow the node to point to the previous node. Although it's not difficult to write for specific cases, generic linked lists [3] are more difficult. One instructive example online creates a linked list manager [4].
Fortran 90 created a way of specifying precision to make the code more portable across systems. The generic term used is kind
. Real variables can be 4-byte (real*4
), 8-byte (real*8
, usually referred to as double precision), or 16-byte (real*16
). In this example, I assign various kinds of precision to variables.
real*8 :: x8 ! kind=8 or doule precision real*4 :: x4 ! kind=4 or single precision real(kind=4) :: y4 ! integer*4 :: i4 ! integer single precision (kind=4) integer(kind=8) :: i8 ! Integer double precision
By using kind
or the equivalent *<n>
notation, you make your code much more portable across systems.
At a high level, in addition to the features mentioned, F90 also introduced in-line comments, operator overloading, unlabeled do
loops (constructs such as do
, if
, case
, where
, etc. may have names.), and the case
statement.
In F90, the language developers also decided to deprecate, or outright delete, some outdated F77 features [5], including the arithmetic IF
, non-integer DO
parameters for control variables, the PAUSE
statement, and others. You can easily discover what features have been deprecated or deleted by taking some F77 code, switching the extension to .f90
, and trying to compile it with a F90 compiler.
Fortran 90 was only the start. The next two iterations – Fortran 95 and 2003 – pulled Fortran into a new era of programming languages.
Buy this article as PDF
(incl. VAT)