Best Practices in Modern Fortran

Overview

Teaching: 60 min
Exercises: 0 min
Questions
  • How to move out of Fortran 77 into Modern Fortran styles?

Objectives
  • Learn the difference between deprecated styles and the modern ways of Fortran coding.

Best Practices in Modern Fortran

Fortran is a language with a long history. Fortran is one of the oldest Programming Languages still in active use. That old history means that the language has evolved in expression and scope over more than 60 years.

The way you programmed Fortran back in the 60s, 70s, and 80s is completely inadequate for modern computers. Consider for example the length of the lines. Back in the 70s and 80s, people programmed computers using punched cards and later dumb terminals connected to mainframes. Those terminals have a limited number of characters in one line. It is from that time that a limit of 72 characters on one line and characters beyond column 72 were ignored. The first five columns must be blank or contain a numeric label. Continuation lines were identified by a nonblank, nonzero in column 6. All these limitations were present in what is called the Standard Fixed Format of Fortran 77.

Over time, these limitations and many others in the language became obsolete but were still in use for decades even after Fortran 90 was the new Standard. Back in the 90s, there were no free Fortran 90 compilers, people continue to use Fortran 77 with restrictions relaxed and some characteristics of Fortran 90 were integrated little by little into the compilers.

The fact is that there are many codes written in Fortran 77. If you search for Fortran tutorials and documentation, there are many pages in academics that still teach Fortran 77. Fortran 90 was gaining space and later Fortran 95. Today almost main Fortran compilers support Fortran 2003 entirely and Fortran 2008 to a fairly complete degree. New features in Fortran 2018 are still not implemented in the latest version of major compilers.

In previous sections, we have discussed the basic elements so you can read and write code in Fortran. We learn about variables assignments subroutines and modules and we explored those elements using the modern ways of writing code in Fortran. In this section we will have to learn a bit about the “old ways” in such a way that when you encounter those pieces of code, you can recognize them knowing which are the alternatives and best practices used today. This is in this way an intermediate class in Modern Fortran. I assume you know the basics and are able to write useful code in Fortran. The idea is to recognize old Fortran 77 and Fortran 90 styles and be able to translate those into modern Fortran 2003 and 2008 Standards. By doing that you are moving the code into more flexible, readable, extensible, and potentially with more performance.

The old Fortran 77

Fortran 77 was a powerful language back in its time. It was so powerful that stay as the standard de facto well in the 90s. Many elements of the language still in use today were added to Fortran 77 to address the limitations of FORTRAN 66. For example the IF and END IF statements including ELSE and ELSE IF were the first steps into structured programming. These statements were an important move to move GOTO into oblivion. GOTOwas one of the reasons why codes were so difficult to follow and almost unpredictable when were the source of a bug. Another powerful element of the language were intrinsic functions such as SQRT that accept complex or double precision numbers as arguments.
All these niceties today are taken for granted but back then were big improvements to the language. However, the language has important flaws. One important is the lack of dynamic memory allocation. The code still promotes a code that is cluttered, with missing blanks, and in general hard to read.

Now the question is if a code works in Fortran 77 why change it? Investing some time now in rewriting a code in a new language could return in big gains later on. A new structure could emerge that uses better memory (with dynamic allocation) to structure the code in ways that make it easier to read and extend. There are good reasons and rewards for better and cleaner code.

Free Format

Back to the first versions of Fortran, the first 5 columns of characters were dedicated as label fields. A C in column 1 means that the line was treated as a comment and any character in column 6 means that the line was a continuation of the previous statement.

Today those restrictions are no longer needed. Statements can start in the first column. An exclamation mark starts a comment and comments could be inserted any place in the line, except literal strings. Blanks help the readability of the code. Variables cannot have spaces, it is a better practice to use underscore for multi-word variables. Use & as the last character for continuing on the next line. Declaring multiple statements on the same line is possible with a semicolon (;) but use it only for very short statements.

Example (example_01.f90):

program free_form
   print *, 'This statement starts in column 4 (with indentation)'
   x = 0.1; y = 0.7 ! Two small statements in one line
                    ! Comment with an exclamation mark
   tan_x_plus_y = tan(x) + tan(y) / &  ! Line with continuation
           (1- tan(x)*tan(y))
end program free_form

Use blank characters, blank lines and comments to improve the readability of your code. A cluttered code is hard to read and you are not doing any good trying to fit most of your code in one screen page.

Instead of this (example_02_bad.f90):

!BAD CODE
program log_sqrt
x=99.9
y=log10(sqrt(x))
if(y.lt.1.0) print *,'x is less than 100'
end program

This code is too compact, it is not hard to read because is too small, but for bigger codes became really hard to follow.

An alternative is like this (example_02_bad.f90):

!GOOD
program log_sqrt

   x = 99.9
   y = log10(sqrt(x))

   if (y .lt. 1.0) &
           print *, 'x is less than 100'

end program log_sqrt

We include spaces to clarify variable assignments from the conditional. Indentations inside the program and end program also help to visualize the scope of blocks.

Old style DO loops

In old FORTRAN 77, do loops have a numerical identifier to jump to a continue statement to cycle the loop, that is completely obsolete and must be avoided in modern coding (example_03_bad.f90):

      PROGRAM square
      DO 100 I=1,100
      X=I
      X2=X*X
      IF(X2.LT.100) print *, 'X=', I, ' X^2 will have less than 3 digits
100   CONTINUE
      END

This old style of coding wastes 6 columns of space, uses a labeled statement for cycling and is written with full capitalization.

An alternative is something like this (example_03_good.f90):

program square

   implicit none

   real :: x, x2
   integer :: i

   do i = 1, 100
      x = i
      x2 = x*x
      if (x2 < 100) &
         print *, 'X=', I, ' X^2 will have less than 3 digits
   end do

end program

This code uses indentations and has an end do that is clearer for the user when the loops grow or became nested. We will discuss the implicit down below

Variable Attributes

Fortran 95 introduce variable attributes and they were extended in Fortran 2003 and 2008. The most common attributes are parameter, dimension, allocatable, intent, pointer, target, optional, private, public, value and bind

From those, we will demonstrate the first 3. parameter is important for declaring constants that otherwise will require going beyond the language and using the preprocessor. dimensionis used to define the length of arrays or the dimension using colons and commas to declare it. allocatable is used for the dynamic allocation of arrays an important feature introduced in Fortran 90 and expanded in Fortran 95 and beyond. This is an example using these 3 attributes that should be frequently used (example_04.f90).

program f95attributes

   implicit none

   integer :: i, j
   integer, parameter :: n = 100
   real :: x
   real, parameter :: pi = 3.141592653
   real, dimension(n) :: array
   real, dimension(:, :), allocatable :: dyn_array2d

   allocate (dyn_array2d(2*n, 2*n))

   do i = 1, 2*n
      do j = 1, 2*n
         dyn_array2d(j, i) = j + 0.0001*i
      end do
   end do

   do i = 1, 10
        print '(10(F9.4))', dyn_array2d(i,1:10)
   end do

end program

implicit none

By default all variables starting with i, j, k, l, m and n are integers. All others are real.

Despite of this being usually true for small codes, it is better to turn those defaults off with implicit none Variables in large codes will have names with scientific meaning and the defaults can bring bugs that are hard to catch.

program implicit
   ! use to disable the default
   implicit none

   real :: J_ij !Interaction factor in Ising Model

end program

Loops, exit and cycle

Use exit to abandon a loop and cycle to jump to the next iteration. These are better replacements to complicated GOTO statements from old FORTRAN. You can use exit and cycle in bounded and unbounded loops.

The file example_05.f90 and example_06.f90 show the effect of exit and cycle respectively.

program do_exit

   implicit none

   integer :: i
   real, parameter :: pi = 3.141592653
   real :: x, y

   do i = 0, 360, 10
      x = cos(real(i)*pi/180)
      y = sin(real(i)*pi/180)

      if (abs(x) < 1E-7) then
         print *, "Small denominator (exit)"
         exit
      end if
      print *, i, x, y, y/x
   end do

   print *, 'Final values:', i, x

end program
program do_cycle

   implicit none

   integer :: i
   real, parameter :: pi = 3.141592653
   real :: x, y

   do i = 0, 360, 10
      x = cos(real(i)*pi/180)
      y = sin(real(i)*pi/180)

      if (abs(x) < 1E-7) then
         print *, "Small denominator (cycle)"
         cycle
      end if
      print *, i, x, y, y/x
   end do

   print *, 'Final values:', i, x

end program

In the case of nested loops, the solution is to name the loop. Constructs such as do, and if as case can be named and you can use the name to leave outer loops from inside an inner loop.

...
outer: do i = 0, 360, 10
   inner: do j = 0, 360, 10
      x = cos(real(i)*pi/180)
      y = sin(real(j)*pi/180)

      if (abs(x) < 1E-7) then
         print *, "Small denominator (exit)"
         exit outer
      end if
      print *, i, x, y, y/x
   end do inner
end do outer
...
...
outer: do i = 0, 360, 10
   inner: do j = 0, 360, 10
      x = cos(real(i)*pi/180)
      y = sin(real(j)*pi/180)

      if (abs(x) < 1E-7) then
         print *, "Small denominator (cycle)"
         cycle outer
      end if
      print *, i, x, y, y/x
   end do inner
end do outer
...

The case construct

The case construct in Fortran works different from its homologous in C language. In C you need to use a break, otherwise the code will test every single case. In Fortran once one case passes the condition, the others will be skipped. In Fortran case take ranges apart from a single element as other languages. Example:

integer :: temp_gold

! Temperature in Kelvin!
select case (temp_gold)
case (:1337)
write (*,*) Solid
case (1338:3243)
write (*,*) Liquid
case (3244:)
write (*,*) Gas
case default
write (*,*) Invalid temperature
end select

The kind of variables

There are at least 2 kinds of reals: 4-byte, 8-byte. Some compilers offer the third kind with 16-byte reals. The kind numbers are usually 4, 8, and 16, but this is just a tradition of several languages and not mandatory by the language. The kind values could be 1, 2 and 4.

There is an intrinsic module called iso_fortran_env that provides the kind values for logical, character, integer, and real data types.

Consider this example to get the values used (example_07.f90)

program kinds

   use iso_fortran_env

   implicit none

   print *, 'Logical  : ', logical_kinds
   print *, 'Character: ', character_kinds
   print *, 'Integer  : ', integer_kinds
   print *, 'Real     : ', real_kinds

end program kinds

Different compilers will respond with different kinds values. For example, gfortran will return this:

Logical  :            1           2           4           8          16
Character:            1           4
Integer  :            1           2           4           8          16
Real     :            4           8          10          16

As Fortran have evolve over the years, several ways were created to declare the storage size of different kinds and consequently the precision of them. This example explores some of those old ways that you can still encounter in codes.

Consider this example illustrative of the multiple ways of declaring REAL variables (example_08.f90):

program kinds

   use iso_fortran_env

   implicit none

   integer :: i, my_kind

   real :: x_real              ! the default
   real*4 :: x_real4           ! Real with 4 bytes
   real*8 :: x_real8           ! Real with 8 bytes
   DOUBLE PRECISION :: x_db    ! Old way from FORTRAN 66

   integer, parameter :: k9 = selected_real_kind(9)
   real(kind=k9) :: r

   print *, 'Kind for integer            :', kind(i)
   print *, 'Kind for real               :', kind(x_real)
   print *, 'Kind for real*4             :', kind(x_real4)
   print *, 'Kind for real*8             :', kind(x_real8)
   print *, 'Kind for DOUBLE PRECISION   :', kind(x_db)
   print *, ''

   my_kind = selected_real_kind(9)
   print *, 'Which is the "kind" I should use to get 9 significant digits?  ', my_kind

   my_kind = selected_real_kind(15)
   print *, 'Which is the "kind" I should use to get 15 significant digits? ', my_kind

   r = 2._k9;
   print *, 'Value for k9', k9
   print *, 'Square root of 2.0 for default real      :', sqrt(2.0)   ! prints 1.41421354
   print *, 'Square root of 2.0 for DOUBLE PRECISION  :', sqrt(2.0d0) ! prints 1.41421354
   print *, 'Square root of 2.0 for numer of kind(k9) :', sqrt(r)     ! prints 1.4142135623730951

end program

The original REAL data type have received multiple variations for declaring floating point numbers based on rather ambiguous terms such as DOUBLE PRECISION which actually does not mean what literally says. Other variations use the kind assuming that the numbers 4, 8, and 16 represent the number of bytes used by each data type, this is not standard and Salford f95 compiler used kinds 1,2, and 3 to stand for 2- 4- and 8-byte.

Notice that storage size is not the same as precision. Those terms are related and you expect that more bytes will end up giving more precision, but REALS have several internal components such as the size of mantissa and exponent. The 24 bits (including the hidden bit) of mantissa in a 32-bit floating-point number represent about 7 significant decimal digits.

Even though, this is not the same across all the real space. We are using the same number of bits to represent all normalized numbers, the smaller the exponent, the greater the density of truncated numbers. For example, there are approximately 8 million single-precision numbers between 1.0 and 2.0, while there are only about 8 thousand numbers between 1023.0 and 1024.0.

Beyond the standard representation, you can also change the storage size during compilation. Below, the same code was compiled using arguments that change the storage size of different variables.

$> gfortran example_08.f90
$> ./a.out
 Kind for integer            :           4
 Kind for real               :           4
 Kind for real*4             :           4
 Kind for real*8             :           8
 Kind for DOUBLE PRECISION   :           8

 Which is the "kind" I should use to get 9 significant digits?             8
 Which is the "kind" I should use to get 15 significant digits?            8
 Value for k9           8
 Square root of 2.0 for default real      :   1.41421354    
 Square root of 2.0 for DOUBLE PRECISION  :   1.4142135623730951     
 Square root of 2.0 for numer of kind(k9) :   1.4142135623730951     
$> gfortran -fdefault-real-16 example_08.f90
$> ./a.out
 Kind for integer            :           4
 Kind for real               :          16
 Kind for real*4             :           4
 Kind for real*8             :           8
 Kind for DOUBLE PRECISION   :          16

 Which is the "kind" I should use to get 9 significant digits?             8
 Which is the "kind" I should use to get 15 significant digits?            8
 Value for k9           8
 Square root of 2.0 for default real      :   1.41421356237309504880168872420969798      
 Square root of 2.0 for DOUBLE PRECISION  :   1.41421356237309504880168872420969798      
 Square root of 2.0 for numer of kind(k9) :   1.4142135623730951     
$> gfortran -fdefault-real-16 -fdefault-double-8 example_08.f90
$> ./a.out
 Kind for integer            :           4
 Kind for real               :          16
 Kind for real*4             :           4
 Kind for real*8             :           8
 Kind for DOUBLE PRECISION   :           8

 Which is the "kind" I should use to get 9 significant digits?             8
 Which is the "kind" I should use to get 15 significant digits?            8
 Value for k9           8
 Square root of 2.0 for default real      :   1.41421356237309504880168872420969798      
 Square root of 2.0 for DOUBLE PRECISION  :   1.4142135623730951     
 Square root of 2.0 for numer of kind(k9) :   1.4142135623730951      

Fortran 2008 includes standard kinds real32, real64, real128 to specify a REAL type with a storage size of 32, 64, and 128 bits. In cases where target platform does not support the particular kind a negative value is returned.

This example shows the new kind parameters (example_09.f90).

program newkinds

   use iso_fortran_env

   implicit none

   real(kind=real32) :: x32
   real(kind=real64) :: x64
   real(kind=real128) :: x128

   print *, 'real32  : ', real32, achar(10), &
           ' real64  : ', real64, achar(10), &
           ' real128 : ', real128

   x32 = 2.0
   x64 = 2.0
   x128 = 2.0

   print *, 'SQRT(2.0) using kind=real32  :', sqrt(x32)
   print *, 'SQRT(2.0) using kind=real64  :', sqrt(x64)
   print *, 'SQRT(2.0) using kind=real128 :', sqrt(x128)

end program

The storage size of these variables is no longer affected by the compiler arguments used above.

$> gfortran example_09.f90
$> ./a.out
 real32  :            4
 real64  :            8
 real128 :           16
 SQRT(2.0) using kind=real32  :   1.41421354    
 SQRT(2.0) using kind=real64  :   1.4142135623730951     
 SQRT(2.0) using kind=real128 :   1.41421356237309504880168872420969818      
$> gfortran -fdefault-real-16 -fdefault-double-8 example_09.f90
$> ./a.out
 real32  :            4
 real64  :            8
 real128 :           16
 SQRT(2.0) using kind=real32  :   1.41421354    
 SQRT(2.0) using kind=real64  :   1.4142135623730951     
 SQRT(2.0) using kind=real128 :   1.41421356237309504880168872420969818      

You can still change those kinds during compile time using command line arguments -freal-4-real-10, -freal-8-real-10 and similar ones.

$> gfortran -freal-4-real-10 -freal-8-real-10 example_09.f90
$> ./a.out
 real32  :            4
 real64  :            8
 real128 :           16
 SQRT(2.0) using kind=real32  :   1.41421356237309504876      
 SQRT(2.0) using kind=real64  :   1.41421356237309504876      
 SQRT(2.0) using kind=real128 :   1.41421356237309504880168872420969818      
$> gfortran -freal-4-real-16 -freal-8-real-16 example_09.f90
$> ./a.out
 real32  :            4
 real64  :            8
 real128 :           16
 SQRT(2.0) using kind=real32  :   1.41421356237309504880168872420969818      
 SQRT(2.0) using kind=real64  :   1.41421356237309504880168872420969818      
 SQRT(2.0) using kind=real128 :   1.41421356237309504880168872420969818      

Changing kinds during compile time could have unintended consequences, for example using external libraries as could be the case with MPI.

Allocatable arrays

There are two types of memory for a program: The stack and the heap. Scalars and static arrays live in the stack but the size of that space is very limited and some sysadmins and queue systems limit its value even more. Most other variables including allocatable arrays live on the heap.

Before allocatable arrays were part of Fortran 90, Arrays were created with a fixed size. Programs used arrays with sizes that overestimated the actual needs for storage or require being recompiled every time the size of those arrays changed. Still, some scientific codes work with fixed arrays and need recompilation before any simulation. Modern written codes (Since Fortran 90) used allocatable arrays. Declarations and allocation happen in two steps instead of a single step with fixed arrays. As allocation takes time, it is not a good idea to allocate and deallocate very often. Allocate once and use the space as much as possible. Fortran 90 introduces ALLOCATABLE attributes and allocate and deallocate functions. Fortran 95 added DIMENSION attribute as an alternative to specifying the dimension of arrays. Otherwise, the array shape must be specified after the array-variable name. For example (example_10.f90):

program alloc_array

   implicit none

   integer, parameter :: n = 10, m = 20
   integer :: i, j, ierror

   real :: a(10)
   real, dimension(10:100, -50:50) :: b
   real, allocatable :: c(:, :)

   real, dimension(:), allocatable :: x_1d
   real, dimension(:, :), allocatable :: x_2d

   allocate (c(-9:10, -4:5))

   allocate (x_1d(n), x_2d(n, m), stat=ierror)
   if (ierror /= 0) stop 'error in allocation'

   do i = 1, n
      x_1d(i) = i
      do j = 1, m
         c(j - 10, i - 5) = j-10 + 0.01*(i-5) ! contiguous operation
         x_2d(i, j) = i + 0.01*j              ! non-contiguous operation
      end do
   end do

   print *,''
   print '(A, 10(F6.2))', 'x_1d:', x_1d(:)

   print *,''
   do j = -9, 10
      print '(A, 10(F6.2))', 'c   :', c(j, :)
   end do

   print *,''
   do i = 1, n
      print '(A, 20(F6.2))', 'x_2d:', x_2d(i, :)
   end do

   deallocate (c, x_1d, x_2d)

end program

Array Allocations: heap vs stack

Static arrays could be allocated on the stack instead of using the heap. The stack has a limited size and under some circumstances, static arrays could exhaust the stack space allowed for a process. Consider this example (example_12.f90):

program arraymem

   use iso_fortran_env

   implicit none

   integer, parameter :: n = 2*1024*1024 - 4096

   print *, 'Numeric Storage (bits): ', numeric_storage_size
   print *, 'Creating array N ', n

   call meanArray(n)

contains

   subroutine meanArray(n)
      integer, intent(in) :: n
      integer :: dumb
      integer, dimension(n) :: a
      a = 1
      !print *, sum(a)
      read *, dumb
   end subroutine meanArray

end program arraymem

You can check the limit for stack memory on the machine:

$> ulimit -S  -s
8192
$> ulimit -H  -s
unlimited

We have a soft limit of 8MB for stack and the user can raise its value to an unlimited value.

As the limit is 8MB we will create an integer array that takes a bit under 8MB. The array will be of integers and each integer takes by default 4 Bytes. This is not a safe value but will work for the purpose of demonstrating the effect:

n = 2*1024*1024 - 4096

Now we will compile the code above with gfortran, forcing the static arrays to be on stack. Some compilers move large arrays to heap automatically so we are bypassing this protection.

$> gfortran -fstack-arrays example_12.f90
$> ./a.out

Sometimes you will get an output like this:

 Numeric Storage (bits):           32
 Creating array N      2093056
Segmentation fault (core dumped)

We are so close to filling the stack that small variations in loading libraries could cross the limit. Try a few times until the code stops when asking for input from the keyboard.

Once the code is waiting for any input, execute this command on a separate terminal:

$> pmap -x `ps ax | grep a.ou[t] | awk '{print $1}' | head -1`

The command pmap will print a map of the memory and the stack is marked there.

Address           Kbytes     RSS   Dirty Mode  Mapping
0000000000400000       4       4       0 r---- a.out
0000000000401000       4       4       0 r-x-- a.out
0000000000402000       4       4       0 r---- a.out
0000000000403000       4       4       4 r---- a.out
0000000000404000       4       4       4 rw--- a.out
0000000001a3e000     132      16      16 rw---   [ anon ]
00007f8fb9948000    1808     284       0 r-x-- libc-2.17.so
00007f8fb9b0c000    2044       0       0 ----- libc-2.17.so
00007f8fb9d0b000      16      16      16 r---- libc-2.17.so
00007f8fb9d0f000       8       8       8 rw--- libc-2.17.so
00007f8fb9d11000      20      12      12 rw---   [ anon ]
00007f8fb9d16000     276      16       0 r-x-- libquadmath.so.0.0.0
00007f8fb9d5b000    2048       0       0 ----- libquadmath.so.0.0.0
00007f8fb9f5b000       4       4       4 r---- libquadmath.so.0.0.0
00007f8fb9f5c000       4       4       4 rw--- libquadmath.so.0.0.0
00007f8fb9f5d000      92      24       0 r-x-- libgcc_s.so.1
00007f8fb9f74000    2044       0       0 ----- libgcc_s.so.1
00007f8fba173000       4       4       4 r---- libgcc_s.so.1
00007f8fba174000       4       4       4 rw--- libgcc_s.so.1
00007f8fba175000    1028      64       0 r-x-- libm-2.17.so
00007f8fba276000    2044       0       0 ----- libm-2.17.so
00007f8fba475000       4       4       4 r---- libm-2.17.so
00007f8fba476000       4       4       4 rw--- libm-2.17.so
00007f8fba477000    2704     220       0 r-x-- libgfortran.so.5.0.0
00007f8fba71b000    2048       0       0 ----- libgfortran.so.5.0.0
00007f8fba91b000       4       4       4 r---- libgfortran.so.5.0.0
00007f8fba91c000       8       8       8 rw--- libgfortran.so.5.0.0
00007f8fba91e000     136     108       0 r-x-- ld-2.17.so
00007f8fbab29000      16      16      16 rw---   [ anon ]
00007f8fbab3d000       8       8       8 rw---   [ anon ]
00007f8fbab3f000       4       4       4 r---- ld-2.17.so
00007f8fbab40000       4       4       4 rw--- ld-2.17.so
00007f8fbab41000       4       4       4 rw---   [ anon ]
00007ffd65b94000    8192    8192    8192 rw---   [ stack ]
00007ffd663d3000       8       4       0 r-x--   [ anon ]
ffffffffff600000       4       0       0 r-x--   [ anon ]
---------------- ------- ------- -------
total kB           24744    9056    8324

Notice that in this case, we have fully consumed the stack. Compilers could take decisions of moving static arrays to the heap, but even with these provisions, is very easy that multiple arrays combined could cross the limit.

Consider arrays to be always allocatable, so they are allocated on the heap always.

Derived Types and Structures

Beyond the variables of a simple type (real, integer, character, logical, complex) new data types can be created by grouping them into a derived type. Derived types can also include other derived types Arrays can also be included in both static and allocatable. Structures can be made allocatable.

Consider this example that shows the use of derived types and its instances called structures (example_13.f90).

program use_structs

   implicit none

   integer :: i

   ! Electron Configuration
   ! Example: [Xe] 6s1 4f14 5d10
   type electron_configuration

      character(len=3) :: base_configuration
      integer, allocatable, dimension(:) :: levels
      integer, allocatable, dimension(:) :: orbitals
      integer, allocatable, dimension(:) :: n_electrons

   end type electron_configuration

   ! Information about one atom
   type atom

      integer :: Z
      character(len=3) symbol
      character(len=20) name
      integer, allocatable, dimension(:) :: oxidation_states
      real :: electron_affinity, ionization_energy
      type(electron_configuration) :: elec_conf ! structure

   end type atom

   ! Structures (Variables) of the the derived type my_struct
   type(atom) :: gold
   type(atom), dimension(15) :: lanthanide

   gold%Z = 79
   gold%symbol = 'Au'
   gold%name = 'Gold'
   allocate (gold%oxidation_states(2))
   gold%oxidation_states = [3, 1]
   gold%electron_affinity = 2.309
   gold%ionization_energy = 9.226
   allocate (gold%elec_conf%levels(3))
   allocate (gold%elec_conf%orbitals(3))
   allocate (gold%elec_conf%n_electrons(3))
   gold%elec_conf%base_configuration = 'Xe'
   gold%elec_conf%levels = [6, 4, 5]
   gold%elec_conf%orbitals = [1, 4, 3]
   gold%elec_conf%n_electrons = [1, 14, 10]

   print *, 'Atom name', gold%name
   print *, 'Configuration:', gold%elec_conf%base_configuration
   do i = 1, size(gold%elec_conf%levels)
      print '(3(I4))', &
         gold%elec_conf%levels(i), &
         gold%elec_conf%orbitals(i), &
         gold%elec_conf%n_electrons(i)
   end do
end program

Functions and Subroutines

A simple program will have only variable declarations and statements. The next level of organization is to encapsulate variable declarations and statements as an entity (function or subroutine) that can be reused inside the main program, other programs or other function or subroutine.

Another advantage of moving code inside functions or subroutines is that you hide variables inside the scope of the function, allowing the reuse of the names in other routines without collisions.

Fortran makes the distinction between function and subroutine. This is different from other programming languages such as C/C++, Python, Java. In purely functional programming languages (e.g. Haskell) only functions are allowed. Subroutines have arguments that make no distinction between input and output and input variables can be modified as side-effects.

Fortran Functions are simpler compared to subroutines. A function return a single value that is predefined. Functions can be invoked from within expressions, like a write statement, inside an if declaration and other statements.

A subroutine does not return a value, but can return many values via its changing one or more arguments. Subroutines can only be used using the keyword call.

This is one example of a code with one function and one subroutine. (example_14.f90)

subroutine sub_one(ix, oy, ioz)
   real, intent(in) :: ix ! input
   real, intent(out)  :: oy ! output
   real, intent(inout) :: ioz ! input and output

   oy = sqrt(real(ix)) + ioz
   ioz = max(ix, oy)

end subroutine

function func(i) result(j)
   integer, intent(in) :: i ! input
   integer              :: j ! output

   j = sqrt(real(i)) + log(real(i))
end function

program main

   implicit none

   integer :: i
   integer :: func
   real :: ix, oy, ioz

   ix = 10.0
   ioz = 20.0

   i = huge(1)
   print *, "i=", i, char(10), " sqrt(i) + log(i) =", func(i)

   print *, 'Before:', ix, oy, ioz
   call sub_one(ix, oy, ioz)
   print *, 'After :', ix, oy, ioz

end program

It is a good practice to declare the intent of variable with ìn, out or inout. Using them could help during debugging in case variables became modified unintentionally.

Modules

Modules is the next natural level of abstraction. Modules can contain various kinds of things like

This small example uses a module to contain constants. (example_15.f90)

module mod_constants

   use iso_fortran_env

   implicit none

   real, parameter :: pi = 3.1415926536
   real, parameter :: e = 2.7182818285

   real(kind=real64), parameter :: elementary_charge = 1.602176634D-19
   real(kind=real64), parameter :: G_grav = 6.67430D-11     ! Gravitational constant
   real(kind=real64), parameter :: h_plank = 6.62607015D-34  ! Plank constant
   real(kind=real64), parameter :: c_light = 299792458       ! Light Speed
   real(kind=real64), parameter :: vacuum_electric_permittivity = 8.8541878128D-12
   real(kind=real64), parameter :: vacuum_magnetic_permeability = 1.25663706212D-6
   real(kind=real64), parameter :: electron_mass = 9.1093837015D-31
   real(kind=real64), parameter :: fine_structure = 7.2973525693D-3
   real(kind=real64), parameter :: Josephson = 483597.8484
   real(kind=real64), parameter :: Rydberg = 10973731.568160
   real(kind=real64), parameter :: von_Klitzing = 25812.80745

contains

   subroutine show_consts()
      print *, "G = ", G_grav
      print *, "h = ", h_plank
      print *, "c = ", c_light
   end subroutine show_consts

end module mod_constants

program physical_constants

   use mod_constants

   implicit none

   print *, sqrt(2.0_real64)
   print *, sqrt(2.0_real128)
   print *, 'Inverse of Fine Structure constant = ', 1.0_real64 / fine_structure

   call show_consts()

end program

The content of a module is accessible after the statement use <module name> By default all variables, subroutines and functions inside a module are visible, but restrictions can be made using the private statement before the routine and it can only be used by other subroutines on the same module but not by routines that use the module. The variable attribute public can also be used to make exceptions after the private statement

Example of public and private variables and functions. It will be very similar for subroutines and data types. (example_16.f90):

module mod_public_private

   implicit none

   public

   real, parameter :: pi = 3.141592653, &
                      c = 299792458, &
                      e = 2.7182818285

   real, private :: rad_2_deg = 180.0/pi
   real, private :: deg_2_rad = pi/180.0

   private :: sin_deg, cos_deg

   public :: tan_deg

contains

   function sin_deg(x) result(y)
      real, intent(in) :: x ! input
      real             :: y ! output
      y = sin(x*deg_2_rad)
   end function

   function cos_deg(x) result(y)
      real, intent(in) :: x ! input
      real             :: y ! output
      y = cos(x*deg_2_rad)
   end function

   function tan_deg(x) result(y)
      real, intent(in) :: x ! input
      real             :: y ! output
      y = sin_deg(x)/cos_deg(x)
   end function

end module mod_public_private

program priv_pub_module

   use mod_public_private

   implicit none

   real :: r = 2.0

   print *, 'Area = ', pi*r**2

   ! This print will not work
   ! The variables rad_2_deg and deg_2_rad are private
   !print *, rad_2_deg, deg_2_rad

   print *, 'Tan(45) ', tan_deg(45.0)

   ! These lines will not work as functions are private
   !print *, 'Sin(45) ', sin_rad(45.0)
   !print *, 'Cos(45) ', cos_rad(45.0)

end program

Modules: attribute protected and renaming of variables

A public variable is visible by routines that use the module. Private variables will not. There are cases were the value needs to be visible but not changed outside. That is the purpose of private attribute.

Another option is for variables be renamed when the module is loaded allowing codes outside to use the name of the variable for other purposes.

This example combine both features (example_17.f90):

module module_privs

   implicit none

   integer, parameter, private :: sp = selected_real_kind(6, 37)
   integer, parameter, private :: dp = selected_real_kind(15, 307)
   integer, parameter, private :: qp = selected_real_kind(33, 4931)

   real(kind=sp), protected :: pi_sigl = &
   3.141592653589793238462643383279502884197169399375105820974944592307&
   &8164062862089986280348253421170679821480865132823066470938446_sp
   real(kind=dp), protected :: pi_dble = &
   3.141592653589793238462643383279502884197169399375105820974944592307&
   &8164062862089986280348253421170679821480865132823066470938446_dp
   real(kind=qp), protected :: pi_quad = &
   3.141592653589793238462643383279502884197169399375105820974944592307&
   &8164062862089986280348253421170679821480865132823066470938446_qp
   real :: x = 50.25
   real, protected :: x_prot = 512.125
   real :: y = 3.0

contains

   subroutine show_pi_3()
      print *, 'PI with  6 digits', kind(pi_sigl), pi_sigl
      print *, 'PI with 15 digits', kind(pi_dble), pi_dble
      print *, 'PI with 33 digits', kind(pi_quad), pi_quad
   end subroutine show_pi_3

end module module_privs

program main

   use module_privs, my_y => y

   implicit none

   call show_pi_3()

   print *, 'x from inside the module : ', x
   x = 25.50
   print *, 'x changed outside module : ', x

   print *, 'x_prot:', x_prot
   ! This variable is protected and cannot be changed
   ! Uncommenting the line below will not compile
   !x_prot = 125.512
   print *, 'x_prot:', x_prot

   ! The variable 'y' is not visible as it was renamed
   !print *, y
   print *, 'my_y:', my_y

end program main

Optional arguments

In Modern Fortran (since Fortran 90), optional arguments can be part of a subroutine. They must be declared as optional in the calling function (either through a module or an explicit interface). In several implementations of old Fortran 77 it was possible to simply leave out the last argument, if it was a scalar number This behavior, was not part of the Fortran standard but used by some programmers. Now the optional attribute must be declared explicitly.

This example explores several options for optional variables. (example_19.f90):

module my_module

   implicit none

   real :: d_default = -1.125

contains

   subroutine calc(a, b, c, d, e)
      real :: a, b, c, e
      real, optional :: d
      real :: dd
      if (present(d)) then
         dd = d
      else
         print *, 'd argument not present, using default'
         dd = d_default
      end if

      print '(5(F7.3))', a,b,c,dd,e

   end subroutine

end module

program main

   use my_module

   implicit none

   call calc(1., 2., 3., 4., 5.)
   call calc(1., 2., 3., e=5.)
   call calc(a=1., b=2., c=3., d=4., e=5.)
   call calc(b=2., d=4., a=1., c=3., e=5.)
   call calc(1., 2., 3., d=4., e=5.)
   call calc(1., 2., d=4., c=3., e=5.)

end program

Array syntax

One of the strengths of Fortran is scientific computing and High-Performance Computing. Central to HPC programming are arrays and Fortran is particularly good in expressing operations with arrays.

Many operations that require loops in C are done in one operation in Fortran, those are implicit loops hidden in the language expression.

Conditions for operations with arrays are:

Example (example_20.f90):

Key Points

  • Avoid old Fortran 77 style even if correct the code looks better if you use Modern Fortran