Content from Connecting to ARCHER2 and transferring data
Last updated on 2026-02-21 | Edit this page
Estimated time: 40 minutes
Overview
Questions
- What can I expect from this course?
- How will the course work and how will I get help?
- How can I give feedback to improve the course?
- How can I access ARCHER2 interactively?
Objectives
- Understand how this course works, how I can get help and how I can give feedback.
- Understand how to connect to ARCHER2.
Purpose
Attendees of this course will get access to the ARCHER2 HPC facility. You will have the ability to request an account and to log in to ARCHER2 before the course begins. If you are not able to log in, you can come to this pre-session where the instructors will help make sure you can login to ARCHER2.
Connecting using SSH
The ARCHER2 login address is
Access to ARCHER2 is via SSH using both a time-based one time password (TOTP) and a passphrase-protected SSH key pair.
Passwords and password policy
When you first get an ARCHER2 account, you will get a single-use password from the SAFE which you will be asked to change to a password of your choice. Your chosen password must have the required complexity as specified in the ARCHER2 Password Policy.
The password policy has been chosen to allow users to use both
complex, shorter passwords and long, but comparatively simple passwords.
For example, passwords in the style of both LA10!£lsty and
horsebatterystaple would be supported.
SSH keys
As well as password access, users are required to add the public part of an SSH key pair to access ARCHER2. The public part of the key pair is associated with your account using the SAFE web interface. See the ARCHER2 User and Best Practice Guide for information on how to create SSH key pairs and associate them with your account:
TOTP/MFA
ARCHER2 accounts are now required to use timed one-time passwords (TOTP), as part of a multi-factor authorisation (MFA) system. Instructions on how to add MFA authentication to a machine account on SAFE can be found [here][safe-machine-mfa].
Data transfer services: scp, rsync, Globus Online
ARCHER2 supports a number of different data transfer mechanisms. The one you choose depends on the amount and structure of the data you want to transfer and where you want to transfer the data to. The two main options are:
-
scp: The standard way to transfer small to medium amounts of data off ARCHER2 to any other location. -
rsync: Used if you need to keep small to medium datasets synchronised between two different locations
More information on data transfer mechanisms can be found in the ARCHER2 User and Best Practice Guide:
- [Data management and transfer][archer2-data].
Installation
For details of how to log into an ARCHER2 account, see https://docs.archer2.ac.uk/quick-start/quickstart-users/
Check out the git repository to your laptop or ARCHER2 account.
BASH
$ git clone https://github.com/EPCCed/2026-02-18-MO-Fortran-intro.git
$ cd 2026-02-18-MO-Fortran-intro
Within the repository, the code we will use is located in the
episodes/files/exercises directory. The default Fortran
compiler on ARCHER2 is the Cray Fortran compiler invoked using
ftn. For example,
should generate an executable with the default name
a.out. If you are working on another machine, you should
invoke the correct compiler e.g. gfortran.
Each section of the course is associated with a different directory,
each of which contains a number of example programs and exercise
templates. Answers to exercises generally re-appear as templates to
later exercises. Miscellaneous solutions to some of the larger problems
also appear in solutions subdirectories.
Not all the examples compile. Some have deliberate errors which will be discussed as part of the course.
Course structure and method
Rather than having separate lectures and practical sessions, this course is taught following The Carpentries methodology, where we all work together through material learning key skills and information throughout the course. Typically, this follows the method of the instructor demonstrating and then the attendees doing along with the instructor. There are some larger exercises we will start working on at the end of day one.
There are many helpers available to assist you and to answer any questions you may have as we work through the material together. You should also feel free to ask questions of the instructor whenever you like. The instructor will also provide many opportunities to pause and ask questions.
- We should all understand and follow the ARCHER2 Code of Conduct to ensure this course is conducted in the best teaching environment.
- ARCHER2’s login address is
login.archer2.ac.uk. - You have to change the default text password the first time you log in
- MFA is mandatory in ARCHER2
Content from Hello World
Last updated on 2026-02-21 | Edit this page
Estimated time: 20 minutes
Overview
Questions
- What is the structure of a Fortran program?
- How do I print output from the program to the terminal?
Objectives
- Write, compile and run a simple Fortran program.
- Understand how to use the
printandwritestatements.` - Load the
iso_fortran_envmodule with ausestatement.
First example
A very simple program might be:
FORTRAN
program example1
! An example program prints "Hello World" to the screen
print * , "Hello World"
end program example1
Formally, a Fortran program consists of one or more lines made up of
Fortran statements. Line breaks are significant (e.g., there
are no semi-colons ; required here).
Comments are introduced with an exclamation mark !, and
may trail other statements.
The program statement is roughly doing the equivalent
job of main() in C/C++. However, note there is not (and
must not be) a return statement.
Exercise (1 minute)
Compile your first program
Check now you can compile and run the first example program example1.f90. What output do you get when you run it?
Formal description
FORTRAN
[ program [program-name] ]
[ specification-part ]
[ executable-part ]
[ contains
internal-subprogram-part ]
end [program-name]
Optional components are represented with square brackets
[...]. It follows that the shortest standard-conforming
program will be (see example2.f90):
If the program-name is present, it must be at both the
beginning and the end, and must be the same in both places.
We will return to the contains statement in the context
of modules.
print statement
In general
where the format is a format specifier (discussed later)
and the output-item-list is a comma-separated list of
values/variables to be printed to the standard output.
If the format is a * (a so-called free-format) the
implementation is allowed to apply a default format for a given type of
item.
Alternative
Consider the following program (available as example3.f90):
FORTRAN
program example3
use iso_fortran_env, only : output_unit
write (output_unit, *) "Hello ", "world"
end program example3
This example shows a more general way to provide some output. Here we
are also going to employ the use statement to import a
symbol from the (intrinsic) module iso_fortran_env. The
symbol is output_unit which identifies the default standard
output (cf. stdout).
use statement
Formally,
If module-nature is present, it must be either
intrinsic or non_intrinsic. The implementation
must provide certain intrinsic modules such
iso_fortran_env. You can write modules of your own, as we
will see later on.
There is no formal namespace mechanism in Fortran (cf. C++), so
restrictions on which symbols are visible can be made via an optional
only-list. If there is no only-list then all
the public symbols from module-name will be visible.
write statement
Formally,
where the output-item-list is a comma separated list of
items to be output. The io-control-spec-list has a large
number of potential arguments (again comma separated). For formatted
output, these must include at least a unit number and a format:
where the io-unit is a valid integer unit number, and
the format is a format-specifier (as for
print).
Examples are
C programmers looking for a new-line like symbol will notice that
none has appeared so far. The default situation is that both
print and write generate a new-line
automatically. The * symbol in the context of
io-unit is a default output unit (usually the screen).
We will return to the write statement and
format-specifiers in more detail in the context of i/o to external
files.
Some comments on style
Modern Fortran is not case sensitive. Older versions required capitals, a style which has persisted to the present day in some places. So you may see things such as
As modern etiquette tends to regard capitals as shouting, this can cause some tension.
The compiler will accept mixed case. An additional tool would be required to enforce a particular style.
This course will prefer an all lower-case style.
Exercise (2 minutes)
Check symbol values
Write a program which prints out the actual values of the symbols
output_unit, error_unit, and
input_unit (all from iso_fortran_env) to the
screen.
If you haven’t used the only clause in your
use iso_fortran_env, add it now. What happens to the
results if you miss out one of the symbols referenced from the
only clause? This behaviour will be explained in the
following section.
Bonus: check the values using both the Cray and GCC compilers. On
ARCHER2 both are invoked with the ftn compiler wrapper.
Which compiler is actually used depends on the PrgEnv-
module loaded.
Make sure to use the module, then use
print * statements to write the values of the three symbols
to the screen. Sample solution code is available in exercise1.f90.
Running with the default Cray compiler gives the output:
OUTPUT
output_unit is: 101
error_unit is: 102
input_unit is: 100
while using gfortran from GCC gives:
OUTPUT
output_unit is: 6
error_unit is: 0
input_unit is: 5
- A Fortran program begins with a
programstatement and ends with anend programstatement. - A
printstatement is a simple way to write output to the terminal. - A
writestatement provides more control including ways to write to file. - Modules can be loaded with the
usestatement. - The
iso_fortran_envmodule provides symbols which can be used to help read from and write to the terminal.
Content from Break
Last updated on 2026-02-21 | Edit this page
Comfort break.
Content from Variables
Last updated on 2026-02-21 | Edit this page
Estimated time: 30 minutes
Overview
Questions
- What are some basic types of numeric variables in Fortran?
- How are variables declared and defined?
- How do I create constants?
Objectives
- Understand how to create variables and assign values to them.
- Specify the representation in memory of variables independently of the implementation.
- Store constant values.
Numeric variables of intrinsic type
The following program declares a variable with each of the three intrinsic numeric types, and provides an initial value in each case.
FORTRAN
program example1
implicit none
integer :: i = 1 ! A default integer kind
real :: a = 2.0 ! A default floating point kind
complex :: z = (0.0, 1.0) ! A complex kind with (real-part, imag-part)
end program example1
Initial values are optional. If a declaration does not specify an initial value, the variable is said to be undefined.
Variable names
The valid Fortran character set for names is a-z,
A-Z, 0-9 and the underscore _.
Valid names must begin with a character. The maximum length of a name is
63 characters (F2003) with no spaces allowed. This includes names for
programs, and names for variables.
Other special characters recognised by Fortran are:
| Character | Name | Character | Name |
|---|---|---|---|
| Blank | ; |
Semi-colon | |
= |
Equals | ! |
Exclamation mark |
+ |
Plus | " |
Double quote |
- |
Minus | % |
Percent |
* |
Asterick | & |
Ampersand |
/ |
Slash | ~ |
Tilde |
\ |
Backslash | < |
Less than |
( |
Left parenthesis | > |
Greater than |
) |
Right parenthesis | ? |
Question mark |
[ |
Left square bracket | ' |
Apostrophe |
] |
Right square bracket | ` |
Grave accent |
{ |
Left curly brace | ^ |
Circumflex |
} |
Right curly brace | | |
Pipe |
, |
Comma | $ |
Dollar sign |
. |
Decimal point | # |
Hash |
: |
Colon | @ |
At |
Other characters may appear in comments.
implicit statement
The implicit statement defines a type for variable names
not explicitly declared. So, the default situation can be represented
by
that is, variables with names beginning with letters i-n
are implicitly of type integer, while anything else is of
type real (unless explicitly declared otherwise).
By modern standards this is tantamount to recklessness. The general solution to prevent errors involving undeclared variables (usually arising from typos) is to declare that no names have implicit type via
In this case, all variable names must be declared explicitly before they are referenced.
However, it is still common to see the idiom that variables beginning
with i-n are integers and so on, albeit declared
explicitly.
Using implicit none
To reiterate – all modern Fortran code really should use
implicit none. Failing to do so will almost certainly lead
to bugs and hence time spent debugging!
Exercise (1 minute)
The importance of being explicit
Compile and run the accompanying program exercise1.f90. What’s the problem and how should we avoid it? Check the compiler can trap the problem.
The issue is that the variable we declare is initialise is
ll, but the variable we print is l1. Look
closely at these names: the latter mistakenly uses a digit one
1 where it should be the letter l. When we
print the hitherto unmentioned l1, the compiler implicitly
creates it as a new integer.
Add implicit none to the top of the code
FORTRAN
program exercise1
implicit none
integer :: ll = 1
print *, "The value of ll is: ", l1
end program exercise1
which then produces an error at compile time, e.g. with the Cray compiler:
OUTPUT
print *, "The value of ll is: ", l1
^
ftn-113 ftn: ERROR EXERCISE1, File = exercise1.f90, Line = 7, Column = 36
IMPLICIT NONE is specified in the local scope, therefore an explicit type must be specified for data object "L1".
Cray Fortran : Version 15.0.0 (20221026200610_324a8e7de6a18594c06a0ee5d8c0eda2109c6ac6)
Cray Fortran : Compile time: 0.0024 seconds
Cray Fortran : 9 source lines
Cray Fortran : 1 errors, 0 warnings, 0 other messages, 0 ansi
Cray Fortran : "explain ftn-message number" gives more information about each message.
kind of type
The declarations above give us variables of some
(implementation-defined) default type (typically 4-byte integers, 4-byte
reals). A mechanism to control the exact kind, or representation, is
provided. For example (see example2.f90),
FORTRAN
use iso_fortran_env, only : int64, real64
implicit none
integer (kind = int64) :: i = 100
real (kind = real64) :: a = 1.0
complex (kind = real64) :: z = (0.0, 1.0)
Here we use kind type parameters int64 and
real64 to specify that we require 64-bit integers and
64-bit floating point storage, respectively.
A standard conforming Fortran implementation must provide at least
one integer precision with a decimal exponent range of at
least 18 (experience suggests all provide at least two kinds). Note that
Fortran does not have any equivalent of C/C++ unsigned integer
types.
An implementation must provide at least two real
precisions, one default precision, and one extended precision (sometimes
referenced as double precision).
Formally, the numeric types are introduced
where the numeric-type-spec is one of
integer, real, or complex. The
optional kind selector is
The upshot of this is that the syntax of declarations is quite elastic, and you may see a number of different styles. A reasonable form of concise declaration with an explicit kind type parameter is:
Example (2 minutes)
Take a moment to compare the output of the programs example1.f90 and example2.f90, which declare and initialise a variable of each type and print out their values to the screen.
Numeric literal constants
One may (optionally) specify the kind of an integer literal by
appending the kind type parameter with an underscore _,
e.g.:
Floating point literal constants can take a number of forms. Examples are:
FORTRAN
-3.14
.314
1.0e0 ! default precision
1.0d0 ! extended (double) precision
3.14_real128 ! may be available
3.14e-1 ! Scientific notation
3.14d+1 ! Scientific notation extended precision
Complex literals are constructed with real and imaginary parts, with each part real.
Parameters
Suppose we did not want to hardwire our kind type parameters throughout the code. Consider:
FORTRAN
program example3
implicit none
integer, parameter :: my_e_k = kind(1.e0)
integer, parameter :: my_d_k = kind(1.d0)
real (my_e_k) :: a
real (my_d_k) :: b
! ...
end program example3
Here we have introduced an integer with the
parameter attribute. This is an instruction to the
compiler that the associated name should be a constant (similar to a
const declaration in C). Any subsequent attempt to assign a
value to a parameter will result in a compile time error.
The value specified in the parameter declaration must be a constant
expression (which the compiler may be able to evaluate at compile time).
Here we have made use of the intrinsic function
kind(). The kind() function returns an integer
which is the kind type parameter of the argument. In this context a
constant expression is one in which all parts are intrinsic.
Note we have not included anything or use’d anything to
make this kind() function available. It is an intrinsic
function and part of the language itself.
Using a parameter provides a degree of abstraction for the real data type. Other examples might include:
FORTRAN
integer, parameter :: nmax = 32 ! A constant
real, parameter :: pi = 4.0*atan(1.e0) ! A well-known constant
complex, parameter :: zi = (0.0, 1.0) ! square root of -1
The intrinsic function atan() returns the inverse
tangent (as a value in radians) of the argument.
Exercise (2 minutes)
Challenge
Consider further the accompanying example3.f90, where
we have introduced another intrinsic function
storage_size() (roughly equivalent to C’s
sizeof() although it returns a size in bits rather than
bytes). Run the program and check the actual values of the kind type
parameters and associated storage sizes. Speculate on the portability of
a program declaring, e.g.:
Exercise (2 minutes)
Parameters
Consider the accompanying exercise2.f90. Check the compiler error emitted and remove the offending line.
Arithmetic operations on numeric types
For all intrinsic numeric types, standard arithmetic operations,
addition (+), subtraction (-), multplication
(*) and division (/) are defined, along with
exponents (**) and negation (-).
In order of increasing precedence these are -,
+, /, *, and **
(otherwise left-to-right). In particular
Use parentheses to avoid ambiguity if necessary.
A large number of intrinsic functions exist for basic mathematical
operations and are relevant for all numeric types. A simple example is
sqrt(). Some are specific to particular types, e.g.,
conjg() for complex conjugate.
Broadly, assignments featuring different data types will cause promotion to a “wider” type or cause truncation to a “narrower” type. If one wants to be explicit, the equivalent of the cast mechanism in C is via intrinsic functions, e.g.,
FORTRAN
integer :: i = 1
complex (real64) :: z = (1.0, 1.0)
real (real64) :: a, b
a = real(i, real64) ! a should be 1.d0
a = real(z, real64) ! imaginary part is ignored
b = real(i) ! return default real kind
z = cmplx(a, b) ! (sic) Form complex number from two reals
The second argument of the real() function is optional,
and specifies the kind type parameter of the desired result. If the
optional argument is not present, then a real value of the default kind
is returned.
Note here the token real has been used in two
different contexts: as a statement in the declaration of variables
a and b, and as a function. There is not quite
the same concept of “reserved words” (cf C/C++); lines are split into
tokens based on spaces, and tokens are parsed in context.
Complex real and imaginary parts
The real and imaginary parts of a complex variable may be accessed
where the % symbol is referred to as the component
selector. The real and imaginary parts are also available via the
real() and aimag() intrinsic functions,
respectively.
Exercise (2 minutes)
Using sqrt()
By using variables of complex type, check that you can use the
intrinsic function srqt() to confirm that the square root
of -1 is i. What happens if you try to try to take the
square root of a negative value stored as a real variable?
If you call sqrt() on a complex variable
z%re = -1.0 and z%im = 0.0 then a result will
be returned with a real part of 0 and an imaginary part of
1.
If you call sqrt() on a real number -1 then
the compiler will exit with an error and a message explaining why this
is not valid. If sqrt() is called on a variable with the
value -1 it will return NaN.
Exercise (5 minutes)
Calculating pi
The accompanying template exercise3.f90 provides instructions for an exercise which involves the approximation to the constant pi computed via a Gauss-Legendre algorithm. Some background can be found at https://en.wikipedia.org/wiki/Gauss-Legendre_algorithm.
Use the instructions in the template to calculate an estimate of pi
by creating an appropriate parameter to store a kind,
declaring the appropriate variables, then performing the calculation and
printing the values of pi.
A solution to this problem appears as the template for the first exercise in the episode on loops.
Exercise (5 minutes)
Calculating the conductance in a narrow channel
A second exercise in a similar vein looks at an approximation to the conductance of a rectangular channel subject to a constant flow. Instructions are in exercise4.f90.
As with the previous exercise, create a kind, the
variables, then perform the necessary arithmetic to calculate
C_1.
A solution to this problem appears as the template for the second exercise in the episode on loops.
- Fortran provides the intrinsic numeric types of
integer,realandcomplex. - Without a
kind, these types are implementation-defined. Usekindto specify the representation of variables. - The
iso_fortran_envintrinsic module provides standard kinds such asreal32andreal64. - Always use
implicit noneto prevent the accidental implicit declaration of new variables. - Use the
parameterattribute to make the value associated with the name constant.
Content from Logical and conditionals, character variables
Last updated on 2026-02-21 | Edit this page
Estimated time: 20 minutes
Overview
Questions
- What other types of variable does Fortran provide?
- How can I direct a program’s flow down different paths?
- Is it possible to store characters in a variable?
Objectives
- Understand how to use
logicalvariables. - Be able to control the flow of a program with
ifstatements. - Write conditional expressions using logical and relational operators.
- Be able to use a
caseconstruct. - Learn how to declare
charactervariables.
Logical variables
Fortran has a logical type (cf. Boolean type in C);
there are two relevant literal values, illustrated here:
It is possible to specify a kind type parameter for logical kinds,
but you don’t see it very often. The default logical kind
has kind type parameter kind(.false.).
Logical operators and expressions
Standard logical operators .or., .and. and
.not. are available. The precedence is illustrated by,
e.g.,
Again, use parentheses to avoid ambiguity, or to add clarity.
Remember that .or. evaluates to .true. if
either or both its operands are .true., while
.and. requires both to be .true. in order to
evaluate to .true.. The .not. operator inverts
the value of its operand.
Relational operators
To form logical expressions from numeric or other expressions, we require relational operators. The are two forms in Fortran:
| Relation | Operator | Older form | For |
|---|---|---|---|
| Less than | < |
.lt. |
integer real
|
| Less than or equal to | <= |
.le. |
integer real
|
| Greater than | > |
.gt. |
integer real
|
| Greater than or equal to | >= |
.ge. |
integer real
|
| Equal to | == |
.eq. |
integer real complex
|
| Not equal to | /= |
.neq. |
integer real complex
|
If you are coming from C/C++ don’t be tempted by != for
inequality.
Logic and flow of control
if construct
Conditional statements are provided by the if construct,
formally:
FORTRAN
[if-name:] if (logical-expression) then
block
[ else if (logical-expression) then
block ] ...
[ else
block ]
end if [if-name]
There may be zero or more else if blocks, but at most
one else block. At most one block is executed. For
example:
FORTRAN
if (i < j) then
print *, "The smaller is: i ", i
else if (i > j) then
print *, "The larger is: i ", i
else
print *, "i,j are the same: ", i
end if
A single clause if statement is also available, for
example:
Example 1 (3 minutes)
Using relational operators
The file example1.f90 contains a version of the code above. Check it works as expected. Can you replace the relational operators to use the older form given in the table above?
Construct names
A number of control constructs in Fortran include the option to specify names. This can be useful when highly nested structures are present and one needs to refer unambiguously to one or other. A construct name follows the same rules as for variable names.
A if construct with a name must have the matching name
with the end if.
For example
FORTRAN
highly_nested_if_construct: if (a < b) then
! ... structured block ...
end if highly_nested_if_construct
As a matter of style, a leading name can be obtrusive, so one can put
it on a separate line using the continuation character
&, e.g.,
The standard maximum line length in Fortran is 132 characters. Longer
lines can be broken with the continuation character &
to a maximum of 255 continuation lines (F2003).
Finally, note the use of endif without a space is not a
typo; both forms with and without a space are acceptable (this is true
of a number of Fortran statements). It’s probably preferable to stick to
“end if” (at least be consistent).
case construct
This is an analogue of the C switch facility, and can be useful for actions conditional on a range or set of distinct values. Formally,
FORTRAN
[case-name:] select case (case-expression)
[ case (case-value-range-list)
block ] ...
[ case default
block ]
end select [case-name]
The case-expression must be a scalar integer, logical, or character expression. The case-value-range-list is a comma-separated list of either individual values, or ranges.
For example:
FORTRAN
integer :: mycard = 1 ! Playing cards 1--13
select case (mycard)
case (1)
! Action for ace ...
case (2:10)
! Action for other card ...
case (11, 12, 13)
! Court card ...
case default
! error...?
end select
A range may be open-ended (e.g., 2: or
:10). Note there is no break-like statement as in the C
switch; only the relevant case block is executed.
Character variables
Character variables hold zero or more characters. Some examples are:
FORTRAN
program example2
implicit none
character (len = *), parameter :: string1 = "don't" ! 5 characters
character (len = 5) :: string2 = "Don""t" ! 5 characters
character (len = 6) :: string3 = 'don''t' ! 5 characters + blank
end program example2
The implementation must provide at least one type of character
storage, with the default kind being kind('A'). In
practice, kind type parameters are not often specified. However, there
should be a len specifier.
There is, again, a certain elasticity in the form of the declaration, so you may see slightly different things.
Strings may be concatenated with the string concatenation operator
//; a single character, or a subset of characters, can be
extracted via use of an array-index like notation e.g.,
string(1:2).
We will return to character variables in more detail when we consider strings in a later section.
Exercise (1 minute)
Character len
Compile and check the output of example2.f90 to
see the result of the examples above. What happens if you change one of
the len specifications to be too short?
Without modifications, the code should produce the following output:
OUTPUT
string1: don't
string2: Don"t
string3: don't
Catenated: don't Don"tdon't
Substring: Do
kind('A'): 1
If we make string3 too short, e.g.:
then the variable will truncate at that length:
OUTPUT
string1: don't
string2: Don"t
string3: do
Catenated: doDon"tdon't
Substring: Do
kind('A'): 1
Exercise (5 minutes)
Solving a quadratic equation
Write a program which uses real data types to compute the two solutions to the quadratic equation:
\(ax^2 + bx + c = 0\)
for given values of a, b, and
c. See Wikipedia’s
page for some background. A template exercise1.f90
provides some instructions.
A solution to this exercise appears as a template for the first exercise in the episode on array expressions
- Fortran provides two non-numeric intrinsic data types:
logicalandcharacter. - A program’s flow can be directed using the results of logical
operations used in conjunction with
ifandcaseconstructs.
Content from Lunch
Last updated on 2026-02-21 | Edit this page
Break for lunch.
Content from Loops and loop control
Last updated on 2026-02-21 | Edit this page
Estimated time: 20 minutes
Overview
Questions
- How can I make the program do work repeatedly?
- How can I have the program only do as much work as is required?
Objectives
- Learn how to use a
doconstruct to execute a single block of code many times. - See how to have a
doconstruct skip a loop withcycleor end early withexit. - Learn how to use loop control variables with
doconstructs.
Uncontrolled do construct
A simple iteration is provided by the do statement. For
example, …
A slightly more useful version requires some control (see example1.f90):
FORTRAN
integer :: i = 0
do
i = i + 1
if (mod(i, 2) == 0) cycle ! go to the next iteration
if (i >= 10) exit ! exit loop completely
! ... some computation ...
end do
! ... control continues here after exit ...
Loop constructs may be nested, and may also be named (see example2.f90):
FORTRAN
some_outer_loop: &
do
some_inner_loop: &
do
if (i >= 10) exit some_outer_loop ! exit belongs to outer loop
! ... continues ...
end do some_inner_loop
end do some_outer_loop
If the control statements cycle or exit do
not have a label, they belong to the innermost construct in which they
appear.
Loop control
More typically, one encounters controlled iterations with an
integer loop control variable. E.g.,
Formally, we have
with loop-control of the form:
and where the number of iterations will be
in the absence of any subsequent control. The number of iterations may be zero. Any of the expressions may be negative. If the stride is not present, it will be 1 (unity); if the stride is present, it may not be zero.
Exercise (2 minutes)
How many iterations make a do loop?
What is the number of iterations in the following cases?
FORTRAN
do i = 1, 10
print *, "i is ", i
end do
do i = 1, 10, -2
print *, "i is ", i
end do
do i = 10, 1, -2
print *, "i is ", i
end do
You can confirm your answers by running example3.f90.
Note there is no way (cf. C for) to limit the scope of the
loop variable to the loop construct itself; the variable will then have
a final value after exit from the loop.
The following output is produced:
OUTPUT
First loop: 1, 10
i is 1
i is 2
i is 3
i is 4
i is 5
i is 6
i is 7
i is 8
i is 9
i is 10
Second loop: 1, 10, -2
Third loop: 10, 1, -2
i is 10
i is 8
i is 6
i is 4
i is 2
The first loop has ten iterations with i running from 0
to 10 in steps of 1.
The second loop runs zero times as, with a stride of -2, there is no way to iterate from 1 to 10.
The third loop has five iterations, from 10 to 2 in steps of -2. The stated end value of 1 can’t be reached, and 0 would go too far, so it finishes at 2.
Exercise (5 minutes)
Calculating pi with loops
We return to the exercises discussed in the earlier episode on variables. You can use your own solutions, or the new template here.
For exercise1.f90
which computes an approximation to the constant pi using the
Gauss-Legendre algorithm, introduce an iteration to compute a fixed
number of successive improvements. How many iterations are required to
converge when using kind(1.d0)? Would you be able to adjust
the program to halt the iteration if the approximation is within a given
tolerance of the true answer?
Sample code implementing loops with this problem is used as a template to the exercise in the episode on arrays.
You should be able to observe that with kind(1.d0) the
value of pi converges after only three iterations. You also can have the
code exit the loop and halt iteration by storing the previous estimate
of pi if the difference between the old and new values is
less than the desired tolerance.
Exercise (5 minutes)
Conductance of a channel with loops
exercise2.f90
returns to the calculation of the conductance in the narrow channel and
will need similar work. Use a loop to compute a fixed number of terms in
the sum over index k (use real type real64 for the sum).
Here you should find convergence is much slower (you may need about 1000
terms); check by printing out the current value every so many terms (say
20).
Expert question: What happens if you accumulate the sum in the reverse order in this case? Can you think why this happens?
Answer to the expert question: floating point numbers of a given
format (such as real64) all have the same relative
error, no matter how big or small they are. The absolute
error on the other hand is the produce of the number’s value and
the relative error; thus, a small real has a small absolute
error, and a large real has a large absolute error. If the
large numbers are summed first, the sum’s absolute error quickly becomes
large, and the smaller numbers are subsumed into it. If the small
numbers are summed first, the sum’s absolute error remains small enough
that the small numbers are able to contribute. Consider further how in
floating point maths it is true that
(x + y) + z /= x + (y + z), if you are interested look up
Kahan’s
algorithm.
- Iteration in Fortran is based around the
doconstruct (somewhat analogous to Cforconstruct). There is no equivalent of the C++ iterator class. - Without any control, a
doloop will execute forever. - A loop iteration can be skipped with a
cyclestatement. - A loop can be ended if an
exitstatement is encountered. - It is very common to control the execution of a loop with an
integervariable.
Content from Array declarations
Last updated on 2026-02-21 | Edit this page
Estimated time: 20 minutes
Overview
Questions
- How do I create arrays of different sizes and numbers of dimensions?
- Can I create the arrays at runtime if I don’t yet know how big they need to be?
Objectives
- Understand how to create arrays of one, two and more dimensions.
- Recognise and use array terminology.
- Be able to create and use allocatable arrays.
- Get help from the compiler if things go wrong.
A one-dimensional array
Arrays may be declared with addition of the dimension
attribute, e.g.,
FORTRAN
program example1
implicit none
real, dimension(3) :: a ! declare a with elements a(1), a(2), a(3)
end program example1
The size of the array is the total number of elements (here 3). The default lower bound is 1 and the upper bound is 3.
In a slightly older style you may also see arrays declared by placing the size of the array directly after the name, e.g. and equivalently,
Fortran array indexing
Very importantly, Fortran arrays by default begin their indexing at 1. This is in direct contrast to other languages such as C, C++ and Python which begin at 0. Both styles have advantages and disadvantages; the Fortran standard has simply settled on a style that more closely resembles the indexing of mathematical matrices, while starting from 0 makes the offset in the memory more visible.
A two-dimensional array
This two-dimensional array (said to have rank 2) has two elements in the first dimension (or extent 2), and 3 elements in the second dimension (extent 3). It is said to have shape (2,3), which is the sequence of extents in each dimension. Its size is 6 elements.
There is an array element order which in which we expect the
implementation to store contiguously in memory. In Fortran this has to
be the left-most dimension counting fastest. For array a we
expect the order in memory to be
that is, the opposite the convention in C.
Looping through multi-dimensional arrays
In you write a loop which moves through an array, remember that you will typically move most quickly through the first index. That means that generally the innermost loop’s control variable should be used for the first index, the next outer loop should use the second control variable, and so on, e.g.,
The principles of rank-2 arrays can be extended to higher ranks. The standard requires support for a minimum of rank-15 arrays, but individual compilers may allow for even more.
reshape
A constructor for an array of rank 2 or above might be used, e.g.,
where we have used the intrinsic function reshape().
reshape() can be used wherever you might need to alter
an array’s shape.
Exercise (2 minutes)
Array sizes and shapes
Check the accompanying example1.f90 to see examples of intrinsic functions available to interrogate array size and shape at run time.
Allocatable arrays
If we wish to establish storage with shape determined at run time,
the allocatable attribute can be used. The rank must be
specified but the value of the extent in each dimension is deferred
using the : symbol:
FORTRAN
real, dimension(:, :), allocatable :: a
! ... establish shape required, say (imax, jmax) ...
allocate(a(imax, jmax))
! ... use and then release storage ...
deallocate(a)
Again, this array will take on the default lower bound of 1 in each dimension.
Formally,
The optional source argument may be used to provide a
template for the newly allocated object (values will be copied). We will
return to this in more detail in the context of dynamic type.
A successful allocation with the optional stat argument
will assign a value of zero to the argument.
Allocation status
An array declared with the allocatable attribute is
initially in an unallocated state. When allocated, this status will
change; this status can be interrogated via the intrinsic function
allocated().
FORTRAN
integer, dimension(:), allocatable :: m
...
if (allocated(m)) then
! ... we can do something ...
end if
An attempt to deallocate a variable which is not
allocated is an error.
Exercise (5 minutes)
Computing pi with arrays
Return again to the program to compute the approximation of pi via the Gauss-Legendre expansion (last seen in the previous episode on do loops). You may use your own version or the new template provided in this directory (see exercise1.f90).
Introduce array storage for the quantites a,
b and t. Use a fixed number of terms. Assign
appropriate values in a first loop. In a second loop, compute the
approximation of pi at each iteration.
What might you do if you wanted to store only the number of terms taken to reach a converged answer?
Help is available!
As arrays are self-describing in Fortran, it is relatively easy for
the compiler to analyse whether array accesses are valid, or within
bounds. This can help debugging. Most compilers will have an option that
instructs the compiler to inject additional code which checks bounds at
run time. For the Cray Fortran compiler, this is -hbounds;
for the GNU compiler, this is -fcheck=bounds.
E.g.,
Some compilers may also be able to check certain bounds at compile time, and issue a compile-time message. However, in general, errors may not appear until run time. If your program crashes, or produces unexpected results, this compiler option can help to track down problems with invalid array accesses.
- Unlike C, which often uses pointers to handle array data, Fortran has arrays which are an intrinsic feature of the language.
- The number of dimensions in an array is its rank.
- The number of elements in one of an array’s dimensions is its extent.
- The ordered sequence of extents in an array is its shape.
- The total number of elements in an array, equal to the product of its shape, is its size.
- In Fortran array indices begin at 1 by default, but this can be changed if required.
- New arrays can be created by using the intrinsic
reshape()function with an existing one. - An array with an unknown size can be allocated at runtime.
- Compilers are typically able to help you debug issues with arrays if you ask them to.
Content from Array expressions and assignments
Last updated on 2026-02-21 | Edit this page
Estimated time: 30 minutes
Overview
Questions
- How do I assign values to Fortran arrays?
- Can I work at once with subsets of an array?
- How can I reduce or extract information from an array?
Objectives
- Learn the syntax used to work with array sections.
- Learn about array reductions.
- Understand how to use arrays in logical expressions.
Array sections
A general subset of array elements, an array section, may be constructed from a triplet with a start and end subscript and a stride:
Given a rank 1 array a(:), some valid array sub-objects
are:
FORTRAN
a ! the whole array
a(:) ! the whole array again
a(1:4) ! array section [ a(1), a(2), a(3), a(4) ]
a(:4) ! all elements up to and including a(4)
a(2::2) ! elements a(2), a(4), a(6), ...
If an array subscript is present, it must be valid; if the stride is present it may not be zero.
Array expressions and assignments
Intrinsic operations can be used to construct expressions which are arrays. For example:
FORTRAN
integer, dimension(4, 8) :: a1, a2
integer, dimension(4) :: b1
a1 = 0 ! initialise all elements to zero
a2 = a1 + 1 ! all elements of a2(:,:)
a1 = 2*a1 ! multiplied element-wise
b1 = a1(:, 1) ! b1 set to first column of a1
Such expressions and assignments must take place between entities with the same shape (they are said to conform). A scalar conforms with an array of any shape.
Given the above declarations, the following would not make sense:
Exercise (2 minutes)
Array appearance
A caution. How should we interpret the following assignments?
Compile the accompanying program example1.f90, check the compilation errors and improve the program.
The first assignment given above is ambiguous. Is d a
scalar variable, or an entire array? The second assignment is clearly on
every element of the rank-1 array e.
The compiler errors you get when compiling example1.f90 may spell out exactly what is going wrong. At line 13,
b1 is a rank-1 array; the rank-2 array a1
cannot be used to assign values to it. Change it to
Then, the second issue is the assignment
as the section a2(1, 4:4) is an array of the incorrect
size (note that a2(1, 4) would work as it becomes a scalar,
but wouldn’t set b2 to the first half of the first row of
a2). To correct the error, fix the start index:
Array style
Following this exercise, you hopefullly agree it may be a good idea
to prefer use of a(:) over a when referencing
the whole array. The former has the appearance of an array, while the
latter may incorrectly appear to be a scalar to an unfamiliar reader of
the code (or you yourself if it’s been a while since you last read
it).
Elemental intrinsic functions
Many intrinsic functions are elemental: that is, a call with a scalar argument will return a scalar, while a call with an array argument will return an array with the result of the function for each individual element of the argument. For example,
will fill each element of b with the cosine of the
corresponding element in a.
Reductions
Other intrinsic functions can be used to perform reduction operations on arrays, and usually return a scalar. Common reductions are
Logical expressions and masks
There is an array equivalent of the if construct called
where, e.g.,:
which performs the appropriate operations element-wise. Formally,
in which all the array-logical-expr and array-assignments must have the same shape.
Logical functions any(), all(), and others
may be used to reduce logical arrays or array expressions. These return
a logical value so can be used in an if
statement:
FORTRAN
if (any(a(:) < 0.0)) then
! do something if at least one element of a(:) is negative
end if
if (all(a(:) < 0.0)) then
! do something if every element of a(:) is negative
end if
Some intrinsic functions have an optional mask argument which can be used to restrict the operations to certain elements, e.g.,
FORTRAN
b = min(array(:), mask = (array(:) > 0.0)) ! minimum of positive value
n = count(array(:), mask = (array(:) > 0.0)) ! count the number of positive values
These may be useful in certain situations.
Another caution
There may be a temptation to start to construct array expressions of baroque complexity, perhaps in the search for brevity. This temptation is probably best avoided:
- Such expressions can become very hard to read and interpret for correctness;
- Performance: compilers may struggle to generate the best code if expressions are very complex. Array expressions and constructs may not work in parallel: explicit loops may provide better opportunities.
If array expressions are used, simple ones are best.
Exercise (5 minutes)
Quadratic equation
The template exercise1.f90 re-visits the quadratic equation exercise. Check you can replace the scalars where appropriate. See the template for further instructions.
Sieve of Eratosthenes
Here’s an additional exercise: write a program to implement the Sieve of Eratosthenes algorithm which performs a search for prime numbers. See exercise2.f90 for a template with some further instructions. How much array syntax can you reasonably introduce?
A sample solution is provided in solution.f90.
- Fortran allows flexible operations on arrays or subsets of array elements and provides numerous intrinsic functions for such operations.
- Some of these, such as
all()andany()can be useful in directing the logical flow of a program.
Content from Break
Last updated on 2026-02-21 | Edit this page
Comfort break.
Content from Mini exercise: a choice of two
Last updated on 2026-02-21 | Edit this page
Estimated time: 60 minutes
Overview
Questions
- What are some real problems I can try out?
Objectives
- Work on one of two larger exercises, putting your new skills to use.
A choice…
We have two exercises you can choose to work on now. Both will allow you to make use of the Fortran that you have learnt today. The former exercise is the solution of a tri-diagonal system of equations, while the latter will allow you program an implementation of Conway’s Game of Life.
Whichever you choose (or if you choose both), we have time this afternoon and tomorrow for you to work on your solutions.
Solve a tri-diagonal system (30 minutes)
This exercise will solve a tri-diagonal system of equations. A linear system of equations can be represented by the problem
\(Ax = b\)
where \(A\) is a known matrix, \(b\) is a known vector, and \(x\) is an unknown vector we wish to solve for. \(A\) is tri-diagonal, meaning that it is 0 everywhere except for a stripe of non-zeroes three elements wide moving down the diagonal – except the very top and bottom, where it is two elements wide.
This means that we can represent an \(n\)-by-\(n\) tri-diagonal matrix using three rank-1
arrays, one of size n representing the diagonal and two of
size n-1 representing the upper and lower diagonals.
Through two operations across the arrays, the first moving forwards and
the second backwards, as described at Wikipedia,
the solution x can be determined.
The accompanying template tri-diagonal.f90 has some further instructions, and some suggestions on how to test the result.
A solution to the problem appears as a template to the exercise in the later episode on dummy arguments.
Game of Life (60 minutes)
In this exercise you will create a simple implementation of Conway’s Game of Life. The Game of Life is a cellular automata model where an array of cells is updated based on some simple rules which give rise to complex patterns.
This model uses a ‘board’ of cells, each cell having a value of 0 (“dead”) or 1 (“alive”). The board’s initial state is chosen and then stepped forwards. In a new step, each cell calculates the sum of the surrounding eight cells. Depending on the value of that sum, the cell’s state may change or stay the same. The rules governing the update are summarised in the following table:
| Cell state | Sum of neighbouring 8 cells | New Cell state |
|---|---|---|
| 0 | 0,1,2,4,5,6,7,8 | 0 |
| 0 | 3 | 1 |
| 1 | 0,1 | 0 |
| 1 | 4,5,6,7,8 | 0 |
| 1 | 2,3 | 1 |
A template life.f90 is provided with some hints to get you started.
Reference
Your program should produce the following output for the initial board and the first update:
$ ./a.out
Initial Board Set-up:
.....................
.....................
.....................
.....................
.....................
.....................
.....................
.....................
........#.#.#........
........#...#........
........#...#........
........#...#........
........#.#.#........
.....................
.....................
.....................
.....................
.....................
.....................
.....................
.....................
First iteration:
.....................
.....................
.....................
.....................
.....................
.....................
.....................
.....................
.........#.#.........
.......##...##.......
.......###.###.......
.......##...##.......
.........#.#.........
.....................
.....................
.....................
.....................
.....................
.....................
.....................
.....................
- With a fairly small amount of the Fortran language, you can already solve some real problems.
Content from Modules and compilation of modules
Last updated on 2026-02-21 | Edit this page
Estimated time: 20 minutes
Overview
Questions
- How can I collect parameters and sub-programs of my own in a module?
- Can I keep some parts of the module internal to it and hidden?
- Am I able to use scoping to introduce temporary variables in a larger program unit?
Objectives
- Learn how to write a non-intrinsic module of your own, and understand what is and isn’t appropriate to place within them..
- Understand how to use
publicandprivatewithin a module to control what components are visible externally. - Use the
blockconstruct to control the scope of names within larger program structures.
Module structure
We have already used one intrinsic module
(iso_fortran_env); we can also write our own, e.g.,
FORTRAN
module module1
implicit none
integer, parameter :: mykind = kind(1.d0)
contains
function pi_mykind() result(pi) ! Return the value of a well-known constant
real (kind = mykind) :: pi
pi = 4.0*atan(1.0_mykind)
end function pi_mykind
end module module1
We may now use the new module in other program
units (main program or other modules). For example:
FORTRAN
program example1
use module1
implicit none
real (kind = mykind) :: value
value = pi_mykind()
end program example1
Here both the parameter mykind and the function
pi_mykind() are said to be available by use
association. Note that any use statements must come
before the implicit statement.
Formally, the structure of a module is:
FORTRAN
module module-name
[specification-statements]
[ contains
module-subprograms ]
end [ module [ module-name ]]
The contains statement separates the specification
statements from module sub-programs. Sub-programs, or
procedures, consist of functions and/or subroutines which will
cover in the next episode
.
Digression: compilation of modules, and programs
One would typically expect modules and a main program to be in separate files, e.g.,:
It is often convenient to use the same name for the both module and
the corresponding file (with extension .f90). You can do
differently, but it can become confusing. Likewise for the main
program.
We can compile the module, e.g.,
where the -c option to the Fortran compiler
ftn requests compilation only (no link). This should give
us two new files:
The first is a compiler-specific module file (usually with a
.mod extension). This plays the role roughly analogous to a
header (.h) file in C, in that it contains the relevant
public information about what the module provides. The other file is the
object file (.o extension) which can be linked with the run
time to form an executable.
We can now compile both the main program and the module to give an executable.
Again, by analogy with C header files, we do not include the
.mod file in the compilation command; there is a search
path which the compiler uses to look for module files (which includes
the current working directory).
.mod files
As stated, the .mod files are compiler specific. That
means that you shouldn’t expect that a file created by one compiler will
be usable by a different compiler. This can be can cause compile time
errors if you try to use a .mod from one compiler with
another; perhaps the most common scenario for this is when testing
builds with different compilers without first cleaning up the
.mod files. Sometimes compilers also use different naming
schemes for the .mod, such as capitalising them. This
particular behaviour can usually be changed with compiler flags.
Exercise (2 minutes)
Compiling modules
If you haven’t already done so, compile the accompanying module1.f90 and program1.f90. Check the errors which occur if you: (1) try to compile the program without the module file via, e.g.,
and (2), if you try to compile and link the module file alone:
Compiling the program alone produces (with the Cray compiler) the following error:
OUTPUT
use module1
^
ftn-292 ftn: ERROR PROGRAM1, File = program1.f90, Line = 3, Column = 7
"MODULE1" is specified as the module name on a USE statement, but the compiler cannot find it.
real (kind = mykind) :: a
^
ftn-113 ftn: ERROR PROGRAM1, File = program1.f90, Line = 6, Column = 16
IMPLICIT NONE is specified in the local scope, therefore an explicit type must be specified for data object "MYKIND".
^
ftn-868 ftn: ERROR PROGRAM1, File = program1.f90, Line = 6, Column = 16
"MYKIND" is used in a constant expression, therefore it must be a constant.
Cray Fortran : Version 15.0.0 (20221026200610_324a8e7de6a18594c06a0ee5d8c0eda2109c6ac6)
Cray Fortran : Compile time: 0.0472 seconds
Cray Fortran : 13 source lines
Cray Fortran : 3 errors, 0 warnings, 0 other messages, 0 ansi
Cray Fortran : "explain ftn-message number" gives more information about each message.
This tells us precisely that the compiler doesn’t know what module
we’re talking about when we use module1, and then that
mykind (which we have been expecting to get from the
module) doesn’t exist either.
Compiling module1 alone gives the following output from
the Cray compiler:
OUTPUT
/opt/cray/pe/cce/15.0.0/binutils/x86_64/x86_64-pc-linux-gnu/bin/ld: /usr/lib64//crt1.o: in function `_start':
/home/abuild/rpmbuild/BUILD/glibc-2.31/csu/../sysdeps/x86_64/start.S:104: undefined reference to `main'
This is less immediately meaningful. The second line tells us that
there’s no function called main available. This is because
what we’ve compiled has no program ... end program. There’s
no program in there to start, were we able to invoke it on the command
line. In other words, a module alone doesn’t make a program we can
run.
Scope
Entities declared in a module are, by default, available by use
association, that is, they are visible in program units which
use the module. One can make this scope explicit via the
public and private statements.
FORTRAN
module module1
implicit none
public
integer, parameter :: mykind = kind(1.d0)
contains
function pi_mykind() result(pi)
real (kind = mykind) :: pi
...
end function pi_mykind()
end module module1
Note that the parameter mykind is available throughout
the module via host association (always).
An alternative would be to switch the default to
private, and explicitly add public
attributes:
FORTRAN
module module1
implicit none
private
integer, parameter, public :: mykind = kind(1.d0) ! visible via `use`
integer, parameter :: mypriv = 2 ! not visible via `use`
public :: pi_mykind
contains
function pi_mykind() result(pi) ! public
! ... may call my_private() ...
end function pi_mykind
subroutine my_private() ! private
...
end subroutine
end module module1
Note that scope of the implicit statement also covers
the whole module, including sub-programs.
Exercise (1 minute)
Private modules
Edit the accompanying module1.f90 to add a
private statement and check the error if you try to compile
program1.f90.
Making the module private hides the mykind
parameter it provide from the program. On compilation,
mykind can’t be found in the scope of the program, and the
compiler will complain that no such name exists.
Module data and other horrors
It is possible to establish non-parameter data in the specification section of a module. E.g.,
This course will argue that you should not do so. There are a number of reasons.
- Any such data takes on the character of a global mutable object. Global objects are generally frowned upon in modern software development.
- Operations in module procedures on such data run the risk of being neither thread safe nor re-entrant.
Even worse, variables declared with an initialisation in a module sub-program, e.g.,
implicitly take on the Fortran save attribute. This
means the variable is placed in heap memory and retains its value
between calls. (This is analogous to a static declaration
in C.) This is certainly neither thread-safe nor re-entrant.
Uninitialised variables appear on the stack (and disappear) as
expected.
For this reason it is the rule, rather than the exception, that variables are not initialised at the point of declaration in Fortran.
We will look at alternative ways of establishing and moving data as we go along.
Scope again: block
It is not possible in Fortran to intermingle declarations and executable statements. Specification statements must appear at the start of scope before any executable statements. This can lead to rather lengthy list of declarations at the start of large routines.
It is possible to introduce a local scope which follows executable
statements using the block construct. Schematically:
FORTRAN
... some computation ...
block
integer :: itmp ! in scope within the block only
... some more computation ...
end block
... some more computation ...
This can be useful for introducing temporary variables which are only
required for the duration of a short part of a longer procedure. In this
way, it acts like { .. } in C.
Exercise (5 minutes)
Return to Gauss-Legendre
Return to your code for the approximation to pi via the Gauss-Legendre iteration (or use the template exercise1.f90 from the earlier episode on arrays). Using the examples above as a template, write a module to contain a function which returns the value so computed. Check you can use the new function from a main program.
An example solution is provided in solution_program.f90 and solution_module.f90.
This is a circular dependency: a depends on
b depends on a. There is no solvable
dependency tree, and this is not allowed.
- Modules in Fortran provide the way to structure collections of related definitions and operations, and make them available elsewhere.
Content from Functions and subroutines
Last updated on 2026-02-21 | Edit this page
Estimated time: 20 minutes
Overview
Questions
- How can I factor code out into different program procedures?
- How do I control the flow of information into and out of procedures?
Objectives
- See how to create and call both functions and subroutines.
- Use the
intentattribute with dummy arguments to change their read and write permissions within the procedure. - Understand the meanings of
pureandrecursiveprocedures.
What’s the difference?
The difference between functions and subroutines is to a degree one of context. A function returns a result and is generally used where it is best invoked as part of an expression or assignment, schematically:
Unlike C, it is not possible simply to discard a function result. A
subroutine, by contrast, does not return a result (it may be thought of
as a void function in C terms), but it is also invoked
differently:
Subroutines are generally used to express more lengthy algorithms.
Note that in the calling context, the arguments arg1,
arg2, etc are referred to as the actual
arguments.
Dummy arguments and intent attribute
Procedures may have zero or more arguments (referred to as the dummy arguments at the point at which the procedure is defined). Three different cases can be identified:
- read-only arguments whose values are not updated by the procedure;
- read-write arguments whose values are expected to be defined on entry, and may also be updated by the procedure;
- write-only arguments whose values are only defined on exit.
These three cases may be encoded in the declarations of the dummy
arguments of a procedure via the intent attribute . For
example:
FORTRAN
subroutine print_x(x)
real, intent(in) :: x
print *, "The value of x is: ", x
end subroutine print_x
Here, dummy argument x has intent(in) which
tells the compiler that any attempt to modify the value is erroneous (a
compiler error will result). This is different from C, where a change to
an argument passed by value is merely not reflected in the caller.
Passing by reference
If you are used to C/C++, you should remain aware here that the
Fortran standard requires actual arguments to be passed to functions and
subroutines in such a way that they ‘appear’ to have been passed by
reference. How to do so is left to the compiler (which may use copies or
actually pass by reference) but any changes made to a dummy argument
inside a procedure will be reflected in the actual arguments
passed to it, unless prevented by having intent(in).
If one wants to alter the existing value of the argument,
intent(inout) is appropriate:
If the dummy argument is undefined on entry, or has a value which is
simply to be overwritten, use intent(out), e.g.:
The intent attribute, as well as allowing the compiler
to detect inadvertent errors, provides some useful documentation for the
reader.
Local variables do not have intent and are declared as usual.
Exercise (2 minutes)
Exercise name
Attempt to compile the accompanying module1.f90 and associated main program program1.f90. Check the error message emitted, and sort out the intent of the dummy arguments in module1.f90.
assign_x() should use intent(out) for
x as it doesn’t matter what it was previously; we want to
set it to 1 and return it.
print_x() is correct to use intent(in) for
x as it needs to know its value in order to print it, and
it shouldn’t change it.
increment_x() should have intent(inout) for
x: the subroutine needs to know its previous value in order
to add to it (hence in) and it needs to be able to pass the
new value back out (hence out).
Functions
A function may be defined as:
FORTRAN
function my_mapping(value) result(a)
real, intent(in) :: value
real :: a
a = ...
end function my_mapping
Formally,
FORTRAN
[prefix] function function-name (dummy-arg-list) [suffix]
[ specification-part ]
[ executable-part ]
end [ function [function-name] ]
As ever, there is some elasticity in the exact form of the
declarations you may see. In particular, older versions did not have the
result() suffix, and the function-name was used as
the variable to which the return value was assigned. E.g.,
The modern form should be preferred; the result() part
allows the two names to be decoupled.
pure procedures
Procedures which have no side effects may be declared with the
pure prefix; this may provide useful information to the
compiler in some circumstances. E.g.,
pure function special_function(x) result(y)
real, intent(in) :: x
! ...
end function special_function
There are a number of conditions which must be met to qualify for
pure status:
- For a function, any dummy arguments must be intent(in);
- No variables accessed by host association can be updated (and no
variables with
saveattribute); - there must be no operations on external files;
- there must be no
stopstatement.
recursive procedures
If recursion is required, a procedure must be declared with the
recursive prefix. E.g.,
FORTRAN
recursive function fibonacci(n) result(nf)
! ... implementation...
nf = fibonacci(n-1) + fibonacci(n-2)
end function fibonacci
Such a declaration must be included for both types of recursion: direct (a procedure calling itself) and indirect (a procedure calling another which in turn calls the first).
Subroutines
Subroutines follow the same rules as functions, except that there is
no result() suffix specification:
Placing procedures in modules
It is very often a good idea to include your procedures within
modules beneath the contains statement. Aside from leading
to good organisation of code, this has an extra advantage in that the
compiler will then automatically generate the contract block (forward
declaration or prototype) for the procedure as part of the module. This
can make it easier when using certain types of procedures (such as those
with allocatable dummy arguments) or in certain contexts
(such as passing the procedure as an argument).
Exercise (5 minutes)
Gauss and Legendre met Fibonacci
- Can your function for the evaluation of pi from the previous
episodes safely be declared
pure? You can also use the accompanying template exercise_module1.f90 and exercise_program1.f90 to check. - Add a new version of this calculation: a subroutine which takes the number of terms as an argument, and also returns the computed value in the argument list.
- Add to the module a recursive function to compute the nth Fibonacci number, and test it in the main code. See, for example the page at Wikipedia.
Solutions are available in solution_program1.f90 and solution_module1.f90.
- Functions and subroutines are referred to collectively as
_procedures_. - Using
intentfor dummy variables allows control over whether updates to their values are permitted. - Procedures can be modified with prefixes such as
pureandrecursivewhich ensure respectively that the procedure has no side-effects and that it is able to directly or indirectly call itself. - Procedures may be defined as module sub-programs for which the compiler will automatically generate the contract block as part of the module file.
Content from More on array dummy arguments
Last updated on 2026-02-21 | Edit this page
Estimated time: 20 minutes
Overview
Questions
- What do I need to take into consideration when passing an array as an argument?
- Can I pass arrays which haven’t yet been allocated?
- How do I implement optional dummy arguments?
Objectives
- Understand the meaning of an assumed shape array and where care needs to be taken with its bounds.
- Be able to use the intrinsic
lbound()andubound()functions. - Understand the conditions around the usage of allocatable arrays as dummy arguments.
- Make some arguments optional and be able to provide them using keyword arguments.
Array dummy arguments
When a dummy argument is an array, we need to think about how we want the procedure to know about its shape.
Explicit shape
One is entitled to make explicit the shape of an array in a procedure definition, e.g.,
FORTRAN
subroutine array_action1(nmax, a)
integer, intent(in) :: nmax
real, dimension(1:nmax), intent(inout) :: a
...
end subroutine array_action1
and similarly for arrays of higher rank. As before, if the lower
bound of the array is unspecified, it takes on the default value of
1.
Assumed shape
However, it may be more desirable to leave the exact shape implicit in the array itself, e.g.,
FORTRAN
subroutine array_action2(a, b)
real, dimension(:,:), intent(in) :: a
real, dimension(:,:), intent(inout) :: b
...
end subroutine array_action2
Note that the rank is always explicit.
There are pros and cons to this: it is slightly more concise and general, but some care may need to be exercised with the distinction between extents and bounds. It is the shape that is passed along with the actual arguments, and not the bounds.
Bounds, extents and shapes
Remember from the earlier lesson on arrays that the number of elements an array has in a given dimension is that dimension’s extent. The ordered set of extents is the array’s shape. The bounds are the minimum and maximum indices used in each dimension.
lbound() and ubound() again
You will have seen the lbound() and
ubound() functions being used during the example in the earlier
episode on arrays. These functions return a
rank one array which is the relevant bound in each dimension. The
optional argument dim can be used to obtain the bound in
the corresponding rank or dimension e.g.,
Exercise (4 minutes)
Checking array bounds
Consider the accompanying example in program1.f90
and module1.f90.
Is the code correct? Add calls to lbound() and
ubound() in the subroutine to check.
What do you think was the intention of the programmer? What remedies are available?
Automatic arrays
One is allowed to bring into existence ‘automatic’ arrays on the stack. These are usually related to temporary workspace, e.g.,
FORTRAN
subroutine array_swap1(a, b)
integer, dimension(:), intent(inout) :: a, b
integer, dimension(size(a)) :: tmp
tmp(:) = a(:)
a(:) = b(:)
b(:) = tmp(:)
end subroutine array_swap1
In this example, the same effect could have been achieved using a loop and a temporary scalar.
Allocatable dummy arguments
It may occasionally be appropriate to have a dummy argument with both
an intent and allocatable attribute.
FORTRAN
subroutine my_storage(lb, ub, a)
integer, intent(in) :: lb, ub
integer, dimension(:), allocatable, intent(out) :: a
allocate(a(lb:ub))
end subroutine my_storage
There are a number of conditions to such usage.
- The corresponding actual argument must also be allocatable (and have the same type and rank);
- If the intent is
intent(in)the allocation status cannot be changed; - If the intent is
intent(out)and the array is allocated on entry, the array is automatically deallocated.
The intent applies to both the allocation status and to the array itself. Some care may be required.
A function may have an allocatable result which is an
array; this might be thought of as returning a temporary array which is
automatically deallocated when the relevant calling expression has been
evaluated.
Optional arguments
We have encountered a number of intrinsic procedures with optional
dummy arguments. Such procedures may be constructed with the
optional attribute for a dummy argument, e.g.:
FORTRAN
subroutine do_something(a, flag, ierr)
integer, intent(in) :: a
logical, intent(in), optional :: flag
integer, intent(out), optional :: ierr
end subroutine do_something
Any operations on such optional arguments should guard against the
possibility that the corresponding actual argument was not present using
the intrinsic function present(). E.g.,
Any attempt to reference a missing optional argument will generate an error.
The one exception is that an optional argument may be passed directly as an actual argument to another procedure where the corresponding dummy argument is also optional.
Positional and keyword arguments
Procedures will often have a combination of a number of mandatory (non-optional) dummy arguments, and optional arguments. These may be mixed via the use of keywords, which are the dummy argument name. E.g., using the subroutine defined above:,
Here, a is appearing as a conventional positional
argument, while a keyword argument is used to select the appropriate
optional argument ierr. The rules are:
- no further positional arguments can appear after the first keyword argument;
- positional arguments must appear exactly once, keyword arguments at most once.
Exercise (10 minutes)
Tri-diagonal modules
Consider again the problem of the tri-diagonal matrix.
Refactor your existing stand-alone program (or use the template exercise.f90) to provide a module subroutine such as
where b, a, and c are arrays
of the relevant extent to represent the diagonal elements, the lower
diagonal elements and the upper diagonal elements respectively. Use the
size() intrinsic to determine the current size of the
matrix in the subroutine. The extent of the off-diagonal arrays should
be one less element. The rhs is the vector of right-hand
side elements, and x should hold the solution on exit.
Assume, in the first instance, that all the arrays are of the correct
extent on entry.
Check your result by calling the subroutine from a main program with
some test values. (You may wish to take two copies of the template
exercise.f90 and use one as the basis of a module, and the
other as the basis of the main program.)
What do you need to do if the diagonal b and the
right-hand side d arrays should be declared
intent(in)?
What checks would be required in a robust implementation of such a routine?
In the original implementation, the diagonal and right-hand side are
destroyed during the calculation. To use intent(in) with
the dummy arguments, we have to leave them untouched. That means making
copies in local automatic arrays and using those instead.
A robust implementation would need to include checks on the bounds of the dummy arrays to make sure that they correctly conform to one another.
Sample solutions are available in the later episode on derived types in exercise_program.f90 and exercise_module.f90.
- There are some additional considerations to think about when dummy arguments are arrays.
- You may wish to pass the shape of the array explicitly, at the cost of providing more dummy arguments.
- Arrays may have an assumed shape, but remember that they only receive the shape and not the original bounds of its dimensions.
- Allocatable arrays can also be used as dummy arguments.
- Dummy arguments can have the
optionalattribute. The corresponding actual arguments can be positional or provided as a keyword argument.
Content from Break
Last updated on 2026-02-21 | Edit this page
Comfort break.
Content from More on characters and strings
Last updated on 2026-02-21 | Edit this page
Estimated time: 20 minutes
Overview
Questions
- How are character variables compared in Fortran?
- How can I create and use flexible strings?
Objectives
- Understand relational operators and some of the intrinsic functions that can be used with character variables.
- Be able to create and use deferred length character variables.
- Work with arrays of character variables.
- Be able to use characters as procedure dummy arguments and results.
Useful character operations
Relational operators
The meaning of relational operators == and so on in the
context of characters is largely what one would expect from the standard
ASCII sequence. E.g.,
Note the ordinal position of a single character in the ASCII sequence can be found via an intrinsic function, e.g.:
FORTRAN
integer :: i
character (len = 1) :: c
i = iachar("A") ! i = 65
c = achar(i) ! c = "A"; requires 1 <= i <= 127
Remember that in ASCII the upper case characters A to
Z have integer values 65 to 90, while the lower case
characters a to z have integer values 97 to
122.
If character variables are compared, then each letter is
compared left-to-right until a difference is found (or not). If the
variables have different lengths, then the shorter is padded with
blanks.
Some other intrinsic functions
The length of a character variable is provided by the
len() intrinsic function.
The length with trailing blanks removed is len_trim().
To actually remove the trailing blanks, use trim(). This is
often seen when concatenating fixed-length character strings using the
// operator:
It’s also useful if you want to perform an operation on each individual character, e.g.,
Note that the colon is mandatory in a sub-string reference, so a single character must be referenced, e.g.,
Various other intrinsic functions exist for lexical comparisons, justification, sub-string searches, and so on.
Deferred length strings
The easiest way to provide a string which can be manipulated on a
flexible basis is the deferred length character:
FORTRAN
character (len = :), allocatable :: string
string = "ABCD" ! Allocate and assign string
string = "ABCDEFG" ! Reallocate and assign again
string(:) = '' ! Keep same allocation, but set all blanks
This may be initialised and updated in a general way, and the run time will keep track of book-keeping. This also (usually) reduces occurrences of trailing blanks.
If an allocation is required for which only the length is known (e.g., there is no literal string involved), the following form of allocation is required:
FORTRAN
integer :: mylen
character (len = :), allocatable :: string
! ... establish value of mylen ...
allocate(character(len = mylen) :: string)
Allocatable strings will automatically be deallocated when they go out of scope and are no longer required. One can also be explicit:
if wanted.
Arrays of strings
We have seen that we can define a fixed length parameter, e.g.,:
Suppose we wanted an array of strings for “Sunday”, “Monday”, “Tuesday”, and so on. One might be tempted to try something of the form:
Exercise (2 minutes)
Check the result of the compilation of example3.f90.
The are a number of solutions to this issue. One could try to pad the lengths of each array element to be the same length. A better way is to use a constructor with a type specification:
Here the type specification is used to avoid ambiguity in how the list is to be interpreted.
Check you can make this adjustment to example3.f90.
Strings as dummy arguments, or results
If a character variable has intent in in the context of
a procedure, it typically may be declared:
This is also appropriate for character variables of intent
inout where the length does not change.
For all other cases, use of deferred length allocatable characters is recommended. E.g.,
FORTRAN
function build_filename(stub, extension) result(filename)
character (len = *), intent(in) :: stub
character (len = *), intent(in) :: extension
character (len = :), allocatable :: filenane
...
A matching declaration in the caller is required.
Exercise (10 minutes)
Exercise name
Write a subroutine which takes an existing string, and adjusts it to make sure it is all lower case. That is, any character between “A” and “Z” is replaced by the corresponding character between “a” and “z”.
Write an additional function to return a new string which is all lower case, leaving the original unchanged.
You can use the accompanying templates exercise_module1.f90 and exercise_program1.f90.
A solution to the exercise can be found in solution_program1.f90 and solution_module1.f90.
- String-handling can be problematic if there is no mechanism to keep track of the length of the string.
- Remember that Fortran provides intrinsic functions such as
trim()and operators such as concatenation//which can help with string manipulation. - Pay attention when using strings as dummy arguments or procedure results.
Content from Formats and edit descriptors
Last updated on 2026-02-21 | Edit this page
Estimated time: 20 minutes
Overview
Questions
- How do I read input from the keyboard to my program?
- How can I control the format of input and output of various types?
Objectives
- Use the
readstatement analogously towriteto obtain input from the keyboard. - Learn how to combine edit descriptors to create a format specifier.
- Use format specifiers to control the appearance of output of the various types we have encountered.
Input and output
So far we have seen only output (via either print or
write). Input is via the read statement.
In very many respects, read looks much like
write, in that it takes the same arguments, including unit
number and format specifier. However, whereas write may
have an I/O list involving expressions, read must have only
the variables which will store the incoming values.
The standard input unit (input_unit from
iso_fortran_env in the above) is typically the keyboard (or
“screen”) for interactive use.
Format specifier
In addition to the list-directed or free-format
* specifier, a format may be a string literal, a fixed
character parameter, or a string constructed at run time. Some examples
are:
FORTRAN
read (*, *) var ! read using list-directed, or free format
print '(a)', "A string" ! character literal '(a)'
write (*, myformat) var ! character variable (or parameter)
write(unit = myunit) var ! unformatted (no format)
Output with a format specifier, either string or *, is
referred to as formatted I/O. This is to distinguish it from
unformatted I/O; this is essentially a direct dump of the
binary internal representation. For unformatted I/O the format specifier
is simply omitted.
Binary output can have its uses, but more portable and flexible methods of “fast” output are probably to be preferred (e.g., via a library such as HDF5). We will not consider unformatted output any further in this course.
Edit descriptors
A format specifier consists of a comma-separated list of edit descriptors enclosed in parentheses. The edit descriptors determine how the internal representation of data is translated or converted to characters which can be displayed.
The formal and exhaustive statement of exactly what is allowed in an edit descriptor is rather, well, exhausting. It is easier to consider some common examples of the three different types of descriptor.
Data edit descriptors
Data edit descriptors feature a single character describing the type data format wanted, and a literal integer part indicating the total field width, and number of decimals. These include
FORTRAN
iw ! integer width w characters
fw.d ! floating point number total width w and d decimal characters
ew.d ! scientific notation with total width w and d decimal characters
aw ! character string total width w
The width is the total width of the field, and must be wide enough to accommodate any leading signs, decimal points, or exponents. Where there is a w.d the d indicates the number of digits to appear after the decimal point.
Data edit descriptors should correspond to the relevant item in the io-list, based on position. Some simple examples are:
FORTRAN
integer :: ivar = 40
real :: avar = 40.0
print "( i10)", ivar ! produces "40" with 8 leading spaces
print "(f10.3)", avar ! produces "40.000" with 4 leading spaces
print "(e10.3)", avar ! produces "0.400e+02" with 1 leading space
For scientific notation, the exponent will appear either with two digits (“E+dd” or “E-dd”), or three digits for exponents greater than 99 (“+ddd” or “-ddd”).
If a first significant digit is required before the decimal point in
scientific notation (instead of a zero) the es edit
descriptor can be used instead of e; otherwise it’s the
same.
An engineering edit descriptor en is available. It also
acts like a standard e descriptor, but exponents are
limited to a subset …,-3,0,+3,+6,…, corresponding to standard SI
prefixes milli-, kilo-, mega-, and so on. There will be an appropriate
movement in the decimal point.
If an exponent greater than 999 is required, an optional
e descriptor for the exponent itself is available:
Note that an integer descriptor is allowed to be of the form iw.d, in which case leading zeros are added to pad to the width w, if required.
Character string edit descriptors
One can use a string literal as part of the edit descriptor itself, e.g.,
Control edit descriptors
There are a number of these. They do not correspond to an item in the
io-list, but alter the appearance of the output. The most
common is x which introduces a space (or blank). E.g.,
This will ensure at least one space between the two items in the list.
If a leading plus sign is wanted, use the sp control,
e.g.:
Exercise (2 minutes)
Exercise name
Compile and run the accompanying example1.f90 program, which provides some specimen formats. Some of the format specifiers have not been allowed a large enough width. What’s the result? What’s the solution?
Using gfortran produces the following output:
OUTPUT
Format i10: 40
Format i10.10: 0000000040
Format l10: F
Format f10.3: 40.000
Format e10.3: 0.400E+02
Format en10.3: 40.000E+00
Format a10: Hello
Format i1: *
Format f10.3: **********
Format e10.3: 0.180+309
Others:
Format sp,e12.3: +0.180+309
Format e12.3e4: 0.180E+0309
Where the format specifiers are too narrow, the output is reduced to
asterisks * in the space that has been provided. To fix
this, the specifiers need to be widened. For example, the integer
specifier on line 25 is i1, clearly not enough to print
-40. Making it i3 allows it to output.
The next failed output uses f10.3. It’s trying to print
a huge() number, where it was used to store the largest
possible number of the same type as the original avar. As a
real64, that’s approximately 1.80E+308. The
format specifier provides a width of 10 characters to print a number
with 309 digits (although we only have precision in the first 15 or so).
The f format specifier is very definitely unsuitable; one
of the e types such as es24.17 is much more
appropriate.
Repeat counts
For a larger number of items of the same type, it can be useful to specify a repeat count. Simply place the items to be repeated within parentheses and prefix a literal integer count to the format, e.g.:
Multiple subformats are comma-separated, much like the individual items, and subformats can also be nested. In this way, more complex formats can be constructed.
New lines
If no new line at all is wanted, one can use the
advance = no argument to write(),
e.g.:
Non-advancing output must not use list-directed I/O.
Statement labels
Formally, a format specifier may also be a statement label. For example,
Here, the statement at the line with label 100 is the
format keyword, which may be used to define a format
specifier. This is very common in older forms of Fortran. However, as
the reference and the statement can become widely separated, this can be
troublesome. However, statement labels are useful in other contexts, as
we will see when we come to error handling.
Exercise (5 minutes)
Experimenting with formats
Using the example1.f90 program, check what happens if there is a mismatch between the format specifier and the data type in the I/O list. Choose an example, and run the program.
What happens if the number of items in the io-list is larger than the number of edit descriptors?
Additional exercise. The b edit descriptor can be used
to convert integer values to their binary representation. For a 32-bit
integer type, the recommended descriptor would be b32.32 to
ensure the leading zeros are visible. You may wish to try this out.
Using the wrong type variable with a format specifier should still allow the code to compile, but you will receive a runtime error when it is encountered.
Any extra items in the io-list above what the format provides will not be printed.
- Control over the appearance of output is via the format specifier, which is a string.
- This similar in spirit to the format string provided to
printf()in C. - The use of a format allows predictable tabulation of output for human readers.
Content from Lunch
Last updated on 2026-02-21 | Edit this page
Break for lunch.
Content from Operations on external files
Last updated on 2026-02-21 | Edit this page
Estimated time: 40 minutes
Overview
Questions
- How do I open and close files for input and output?
- How do I handle any errors that might occur during I/O?
Objectives
- Learn how to open and close files for input and output using a unit number.
- Be able to read and write to an open file and handle errors that might arise.
- Move around an open file using
readto move forwards andbackspace()andrewind()to move back.
Operations on external files
Writing data to, or reading data from, an external file is an essential part of a useful application.
Writing a file
File handles, or unit numbers are obtained using
open(), and are used to direct the output of
write() to the relevant file.
FORTRAN
integer :: myunit
open(newunit = myunit, file = 'filename.dat', form = 'formatted', &
action = 'write', status = 'new')
write(unit = myunit, *) data1 ! write data first record
write(unit = myunit, *) data2 ! second record ... and so on
close(unit = myunit, status = 'keep')
To read back the same data, we might use:
FORTRAN
open(newunit = myunit, file = 'filename.dat', form = 'formatted', &
action = "read", status = "old")
read(unit = myunit, *) data1
read(unit = myunit, *) data2
close (unit = myunit, status = 'keep')
Unit numbers
A valid unit number can be assigned by using the newunit
option to open() – this has been available since Fortran
2008. There is no need to choose your own (and it can be error-prone to
do so).
Note that a given file can only be connected to one unit number at any given time.
open()
The arguments seen above include:
-
file: literal string or character variable or expression with the name of the file in the file system; -
form:formattedorunformatted; -
action:read,writeorreadwrite. An error may occur if inappropriate actions are performed; -
status: one ofold,new,replace,scratch, orunknown.
A file with status old is expected to exist, while an
attempt to create a new file when one already exists with
the same name will result in an error. If replace is
specified, any existing file will be overwritten by a new file. If
scratch is specified, a temporary file is created which
will be deleted when close() is executed (or at the end of
the program). The system will automatically choose a name for a scratch
file if no file argument is present. The default status is
unknown, which means the status is system dependent.
Formally, only the unit number is mandatory (and must appear first), but there is no reason not to provide as much information as possible.
inquire()
The inquire statement offers a way to obtain information
on the current state of either unit numbers or files. There are a large
number of optional arguments. One common usage is to check whether a
file exists:
Internal files
In some situations, it may be convenient to use a formatted read to
generate a new string in memory (in the same way as sprintf
in C). Fortran uses a so-called internal file, which is usually
a character string. E.g.,
Here buffer takes the place of the input unit. This can
be used, e.g., to create format strings at run time.
Recovery from errors
Operations on external files can be error-prone. While there is no formal exception mechanism in Fortran, some ability to recover is available.
Consider the following schematic example:
FORTRAN
subroutine read_my_file_format(myunit, ..., ierr)
integer, intent(in) :: myunit
integer, intent(out) :: ierr ! error code
character (len = 256) :: msg
read (myunit, ..., err = 999, iomsg = msg) ...
! Everything was ok
ierr = 0
return
999 continue
ierr = -1
print *, "Error reading file: ", trim(msg)
return
end subroutine real_my_file_format
We assume the relevant file has been opened successfully, and is
connected to unit number myunit. If an error occurs at the
point of the read statement, the err argument directs
control to be transferred to the statement will label 999.
In this case the system should provide a meaningful message describing
the error.
If the read is successful, the routine completes at the first return
statement with intent out ierr = 0.
Error handling for open() and close()
Both open and close statements provide
optional arguments for error handling, illustrated schematically here
with open():
FORTRAN
integer :: ierr
character (len = 128) :: msg
open( newunit = myunit, ..., err = 900, iostat = ierr, iomsg = msg)
-
err: is a label in the same scope to which control is transferred on error; -
iostat: an integer variable which is zero on success but positive if an error has occurred; -
iomsg: if an error occurs, a system-dependent message will be assigned tomsg.
If neither err nor iostat arguments are
present, then an error may result in immediate termination of the
program.
The variable msg should be a scalar character variable;
the message will be truncated or padded appropriately.
Error handling for read() and write()
Error handling facilities for read() and
write() are similar, and are illustrated here:
-
end: label in same scope to which control is transferred on end-of-file; -
err: label in same scope to which control is transferred on any other error; the label may be the same asend; -
iostat: the integer error code is negative if end-of-record or end-of-file, or positive if another error (e.g., bad format conversion) or zero on success; -
iomsg: should return a useful message on error.
The two negative iostat cases can be distinguished via
calls to the intrinsic functions
stop statement
If one really can’t continue, then execution can be terminated
immediately via the stop statement. This has an optional
message string argument.
In general one should try to recover by returning control to the
caller, so stop is a last resort.
Moving around an open file
It is sometimes useful to be able to reposition oneself in an open
file so that a given record can be read (or written) more than once.
This can be done using backspace() with the relevant unit
number. Formally
This steps back one record.
It is also possible to reposition to the beginning of the file, again
with the relevant connected unit number using rewind().
Exercise (20 minutes)
Writing a simple image
Portable bit map (PBM, PGM, PPM) is a very simple image format which can be expressed as an ASCII file. See the description at Wikipedia.
In the template program1.f90, we establish
a two-dimensional logical array of a fixed size, and we want to write
out a file that can be viewed in “P1” format (file extension
.pbm ) by an image viewer.
The data is initialised with a suitable pattern to check that the image is correct (it is the right way up and is not a mirror). A little care is required in the rows and columns (look closely at the example).
Provide a module with the subroutine write_pbm() with
three arguments as in the template program. Note the template program
expects a module solution_module.
Additional exercises: repeat for integer data (to “P2” format
.pgm) and floating point data (to “P3” format
.ppm).
You can check any image you produce by either downloading it to your
machine with scp or rsync, or you can view it
directly on ARCHER2 by logging in with X11 forwarding enabled, running
module load imagemagick and then
display test49.pbm.
A solution to this problem appears in the later episode on interfaces.
You should be able to produce an output PBM that looks like the example shown below.

- Writing data to, or reading data from, an external file is an essential part of a useful application.
- Unit numbers are integers which are used as handles to open files.
- Errors from I/O can be handled with labels and by examining the returned message.
Content from Using namelists
Last updated on 2026-02-21 | Edit this page
Estimated time: 40 minutes
Overview
Questions
- What is a namelist?
- How can I use a namelist to read and write grouped data?
Objectives
- Understand what a namelist is and how to create declare one.
- Be able to read and write namelists from file.
- Understand how namelists store structured data such as arrays and derived types.
Fortran namelists
Fortran supports a special form of file I/O called namelists which enable the grouping of variables for reading and writing.
The namelist specification
The namelist associates a name with a list of variables. The namelist
is declared using a namelist construct which takes the
form
where the namelist-group-name is surrounded by forward
slashes / and the variable-name-list is a
comma separated list of variables, e.g.:
Namelists in a file
A namelist file is plain text and may contain one or more namelists.
Each of these is begun by &namelist-group-name and
terminated by /. Variable-value assignments occur in the
body of the namelist, e.g. the file may contain
The ordering of variables in the namelist does not matter, so the previous example could also have been written as
Reading a namelist
A common usecase for namelists is to specify parameters for a program
at runtime. For example a simulation code might read its configuration
from a namelist called run as follows:
As you can see, an extra nice feature of namelists is their support
for comments using the same ! character as in Fortran
code.
To read the namelist from a file, the
namelist-group-name is passed as the argument to
read(), so in order to read this run namelist
we might do the following:
FORTRAN
integer :: myunit
character(len=:) :: name
integer :: nsteps
real :: dt
namelist /run/ name, nsteps, dt
open(newunit = myunit, file = 'config', action = 'read', status = 'old')
read(myunit, run)
close(myunit)
Note that any variables not set in the namelist file
will retain their initial values.
Exercise (5 minutes)
Reading a namelist from file
Using namelist-read.f90,
confirm that the runtime values can be specified by changing the
contents of the namelist in config.nml. Modify
the program to specify a default value for dt and confirm
that dt retains its value unless otherwise set in the
namelist.
What happens if the value set is not of the same type as the variable specification in the program?
What happens if you add an unexpected value to the namelist?
If the value in the namelist is not of the same type as the program’s variable, you will receive an error stating that it is mismatched.
Any unexpected values will also trigger an error stating that the namelist object cannot be matched to a variable.
Reading multiple namelists
As stated earlier, we can have multiple namelists within a single
file. Continuing with our simulation example we might additionally want
to specify some numerical schemes for the simulation, e.g. adding the
schemes namelist:
FORTRAN
&run
name = "TGV" ! Case name
nsteps = 100 ! Number of timesteps
dt = 0.1 ! Timestep
/
&schemes
advection = "upwind" ! Advection scheme
diffusion = "central" ! Diffusion scheme
transient = "RK3" ! Timestepping scheme
/
We can read the values for each of these namelists as outlined
previously, but note, however that the order of the namelists themselves
is important. After reading a namelist the file reader will be
positioned at its end, and trying to read a namelist that occurred
previously in the file will fail. Either the reading subroutine must
match the order of the namelists which occur in the file or file motion
calls such as rewind() should be used to ensure that
reading is independent of order.
Exercise (5 mins)
Reading multiple namelists
Extend the example program namelist-read.f90
to read the numerical schemes namelist from config-full.nml.
(Optional) Ensure that your program can read the run
configuration and the numerical schemes regardless of the
order in which they appear.
Add some character variables to store the items from the
schemes namelist, declare the namelist itself with those
variables, then, after reading the run namelist, read
schemes. To make sure it can read the two schemes in any
order, do a rewind(nmlunit) in between them. This way, you
read through to the first namelist, reset position to the start, then
read through to the second.
If they are in the wrong order and you don’t do this, you will encounter an end of file error.
Writing a namelist
Writing a namelist works similarly to reading one. With an open file,
or other output device, passing the namelist as the argument to
write() will write the namelist to the file.
FORTRAN
integer :: myunit
integer :: a, b, c
namelist /ints/ a, b, c
a = 1
b = 2
c = 3
open(newunit = myunit, file = 'output.nml', action = 'write', status = 'new')
write(myunit, ints)
close(myunit)
will output the following to the new file
output.nml:
Exercise (10 minutes)
Writing namelists to file
Write a program to write the simulation parameters as a namelist so that it can be read by the previous example program.
(Optional) If you completed the extension to read the namelists in any order, confirm that this works with your new program when writing in any order.
A solution writing the run namelist can be found in namelist-write.f90.
Handling complex data
So far we have only considered simple scalars and strings, but namelists also support I/O of more complex data structures such as arrays or user-defined types. These are handled similarly to the code we have seen so far.
Exercise (10 minutes)
Writing complex data to namelists
An example program namelist-complex.f90 is provided to write more complex data structures to a namelist. Try running this program and inspecting the output to see how these data structures are represented.
Modify the program to read the namelist back into data and confirm that the data is correct, for example by testing array elements match their expected values.
- Namelists are declared using the
namelist /namelist-group-name/ variable-name-listconstruct. - Read and write namelists by passing them directly to the
readandwritestatements. - Remember that namelists cannot be read out of order from a file without first moving backwards through the file.
Content from Break
Last updated on 2026-02-21 | Edit this page
Comfort break.
Content from Exercises
Last updated on 2026-02-21 | Edit this page
Estimated time: 60 minutes
Overview
Questions
- What are some problems I can work on?
Objectives
- Continue to work on exercises from taught material and the larger two problems from yesterday.
We will use the rest of today to work on any of the taught exercises you would like to spend more time on, as well as the tri-diagonal and Game of Life exercises from yesterday afternoon’s session .
- Gain more experience putting your new knowledge to use.
Content from Structures: derived types
Last updated on 2026-02-21 | Edit this page
Estimated time: 25 minutes
Overview
Questions
- How can I group together variables to make more complex structures?
- How are these derived types defined and then initialised?
Objectives
- Understand what a derived type is.
- Learn how to define a derived type.
- Learn the different ways of initialising a derived type variable.
- Get a glimpse of more advanced object oriented methodology.
Derived types
We have seen intrinsic types, such as integer and
character. However, in many cases it can be useful to
create structured types combining multiple types together in some way of
our choosing. We call these derived types.
Type definitions
A derived type with two components would be declared, e.g.,
Components may be intrinsic data types (all declared in the usual way), or derived types.
A variable of this type is declared
and individual components are referenced with the component selector
%, e.g.,
The component selector is the same as we have seen earlier for the
complex intrinsic type – recall that the real and imaginary components
of a complex variable z are accessed with z%re
and z%im respectively.
An array of types is defined in the usual way, and the component selector is applied to individual elements, e.g.,
Dummy arguments to procedures are declared in the same way as for intrinsic types with the appropriate attribute list, including intent.
Put type definitions in a module
If a type definition is placed in the specification part of a module, it can be made available consistently elsewhere via use association.
Some derived type features require that the definition be in a module.
Scope of components
Formally, we have
FORTRAN
type [ [, attribute-list] :: ] type-name
[private]
component-part
[ contains
procedure-part ]
end type [ type-name ]
The default situation is for both the type and its components to be public. This may be made explicit by
If one wants a public type with private components (an opaque type), use
Externally, other program units will be able to reference this opaque type, but will not be allowed to access the components (a compiler error).
If a type is only for use within the module in which it is defined,
then it can be declared private in the attribute list.
Type constructors
For types with public components, it is possible to use a structure constructor to provide initialisation, e.g.:
FORTRAN
type, public :: my_type
integer :: ia
real :: b
complex :: z
end type my_type
...
type (my_type) :: a
a = my_type(3, 2.0, (0.0, 1.0))
Values or expressions can be used, but must appear in the order
specified in the definition of the components. An allocatable or pointer
(next episode) component must appear as null() in a
constructor expression list.
Default initialisation
A type may be defined with default initial values. One notable exception is that allocatable components do not have an initialisation. E.g.:
FORTRAN
type :: my_type
integer :: nmax = 10
real :: a0 = 1.0
integer, dimension(:), allocatable :: ndata
end type
A default initialisation can be applied by using an empty constructor:
For an allocatable component, the result is a component with a not allocated status.
Warning: some compilers can’t manage an empty constructor for
allocatable components. The appropriate expression in the constructor is
null().
Exercise (5 minutes)
A type to store a random number generator
The accompanying example (module1.f90 and program1.f90) provides an implementation of a very simple pseudo-random number generator. This is a so-called linear congruential generator.
The module provides a derived type to aggregate the multiplier
a, the seed or state s, the increment
c and the modulus m. These have some default
values. Practical implementations often choose c = 0.
Compile the program, and check the first few numbers returned in the
sequence. The key to obtaining acceptable statistics is to identify some
appropriate values of a and m (e.g., those
given in the default).
Check you can introduce some new values of a and
m using the structure constructor (a spectacularly bad
choice is suggested in the code).
What happens if you make the components of the type
private? What would you then have to provide to allow
initialisation?
Running the code as provided gives the following output:
OUTPUT
Step 1, 45991
Step 2, 2115172081
Step 3, 17451818
Step 4, 1615161307
Step 5, 1424320507
Step 6, 1230752996
Changing to use the suggested bad RNG values means doing
and this produces the following output (note Cray Fortran being helpful with the first line)
OUTPUT
Step 2*1
Step 2, 1
Step 3, 1
Step 4, 1
Step 5, 1
Step 6, 1
Making the RNG type’s components private means doing:
FORTRAN
type, public :: my_rng
private
integer (int64) :: a = 45991
integer (int32) :: s = 1
integer (int64) :: c = 0
integer (int64) :: m = 2147483647
end type my_rng
Trying to compile the program while setting the a etc.
values will cause a compiler error; those components are no longer
public, and the compiler knows it shouldn’t touch them. If we wanted to
change those values while my_rng is opaque, we’d need to
write another module procedure to do so.
Default input/output for derived types
List-directed output for derived types can be used to provide a default output in which each component appears in order, schematically:
FORTRAN
type (my_type) :: a
...
write (*, fmt = *) a
write (*, fmt = *) a%component1, a%component2, ...
or one can apply a specific format to correspond to the known type components.
Exercise (15 minutes)
A tri-diagonal structure
In the earlier material on using arrays as dummy arguments we implemented the tri-diagonal solver as a module procedure. Implement a derived type to hold the relevant data for the tri-diagonal matrix, ie., at least the three diagonals.
Define a function which returns a fully initialised matrix type based on arrays holding the three diagonals. Refactor the solver routine to use the new matrix type.
Additional exercise: A very simple tridiagonal matrix may have all diagonal elements the same, and all off-diagonal elements the same. Write an additional function to initialise such a matrix from two scalar values.
A template for the exercise can be found in exercise_program.f90 and exercise_module.f90; or you can use your own version that you have been developing to this point.
Your new tri-diagonal matrix type should look something like this:
FORTRAN
type, public :: tri_matrix
integer :: nmax
real (mykind), dimension(:), allocatable :: a ! lower (2:nmax)
real (mykind), dimension(:), allocatable :: b ! diag (1:nmax)
real (mykind), dimension(:), allocatable :: c ! upper (1:nmax-1)
end type tri_matrix
Implementations of the solution are available in solution_program.f90 and solution_module.f90.
- The ability to aggregate related data in a structure is important.
- Fortran offers the
_derived type_in addition to intrinsic types. - In its simplest form, one may think of this as the analogue to a C
struct. - Derived types also form the basis of aggregation of data and related operations or procedures (viz. object-oriented programming); however, this introductory course will only touch on this feature.
Content from Pointers and targets
Last updated on 2026-02-21 | Edit this page
Estimated time: 20 minutes
Overview
Questions
- How do I use a pointer to reference another variable?
- Can I alias variable names?
- Can I change the sizes of allocatable arrays?
Objectives
- Understand how to create pointers, targets, and how to associate the two to create references.
- Learn how to use an
associateblock to provide aliases to variables. - Learn how to efficiently reallocate storage for an allocatable array if its size needs to be changed.
Pointer attribute
A pointer may be declared by adding the pointer
attribute to the relevant data type, e.g.,
In this case we declare a pointer to integer p, which is
initialised to the special value null(). The pointer is
said to be unassociated. This is a different state to
undefined (i.e., without initialisation).
Note that pointer assignment uses => and not
=. A common cause of errors is to forget the
> if pointer assignment is intended.
It is important to be able to check that a given pointer is not
null(). This is done with the associated()
intrinsic; schematically,
If a pointer is not initialised as pointing to a target variable, it
should be initialised with null() to avoid undefined
behaviour when testing with associated(). Pointers may
otherwise be used in expressions and assignments in the usual way.
If one wishes to have a pointer to an array, the rank of the pointer should be the same as the target:
and so on.
Targets
A pointer may be associated with another variable of the appropriate type (which is not itself a pointer) by using the target attribute:
The pointer is now said to be associated with the target. We can now
perform operations on datum vicariously through
p. E.g., a standard assignment would be
FORTRAN
integer, target :: datum = 1
integer, pointer :: p => null()
...
p => datum ! pointer assignment
p = 2 ! normal assignment
leaves us with datum = 2. There is no dereferencing in
the C fashion; the data is moved as the result of the normal
assignment.
The target attribute is there to provide information to the compiler
about which variables may be, and which variables may not be, associated
with a pointer. This somewhat in the same spirit as the C
restrict qualifier.
Note that there is an optional target argument to the
associated() intrinsic, which allows the programmer to
inquire whether a pointer is associated with a specific target,
e.g.,
Exercise (2 minutes)
Associating a pointer
Try to compile the accompanying example1.f90, which is an erroneous version of the code above. See what compiler message you get (if any). Fix the problem.
Check you can use the associated() function to print out
the status of p before and after the pointer
assignment.
The p pointer can’t be associated with
datum as the latter doesn’t have the target
attribute; the compilation should fail with an error. If you add the
attribute, it should compile and run:
Check the associated() status of p along
these lines:
Pointers as aliases
One common use of pointers is to provide a temporary alias to another
variable (where no copying takes place). As a convenience, one can use
the associate construct, e.g.:
FORTRAN
real :: improbably_or_tediously_long_variable_name
...
associate(p => improbably_or_tediously_long_variable_name)
! ... lots of operations involving p ...
end associate
Note that there is no requirement here to have the
target attribute in the original declaration (and there’s
no explicit declaration of p). Any update to p
in the associate block will be reflected in the target on exit.
Multiple associations can be made in the same block; simply provide a comma separated list in the parentheses. Also of note is that the selector on the right hand side of the association can be either a variable or an expression.
Exercise (2 minutes)
Using an associate construct
Compile, and check the output of the accompanying code in example2.f90.
Pointers to establish storage
One common use of pointers is for linked data structures. For example, an entry in a linked list might be represented by the type
This sort of dynamic data structure requires that we establish or destroy storage as entries are added to the list, or removed from the list.
FORTRAN
subroutine my_list_add_node(head, datum)
! Insert new datum at head of list
type (my_node), pointer, intent(inout) :: head
integer, intent(in) :: datum
type (my_node), pointer :: pnode
allocate(pnode) ! assume no error
pnode%datum = datum
pnode%next => head
head => pnode
end subroutine my_list_add_node
In the subroutine, we can see the pointer pnode is
allocated. This dynamically creates a my_node variable to
contain the new datum. At the end of the procedure, the
pnode pointer is itself used as a target for the
head pointer.
Pointer or allocatable array?
The question may now arise: should you use pointers or allocatable
arrays? If you just want to establish storage for arrays, the answer is
almost certainly that you should use allocatable. An
allocatable array will almost certainly be held contiguously in memory,
whereas pointers are a more general data structure which may have to
accommodate a stride.
If an allocatable array is not appropriate, then a pointer may be required.
If you just require a temporary alias, the associate
construct is recommended.
Reallocating array storage
If one needs to increase (or decrease) the size of an existing
allocatable array, the move_alloc() intrinsic is useful.
E.g., if we have an integer rank one array
and establish storage of a given size, and some relevant initialisations, we may then wish to increase the size of it.
FORTRAN
integer :: nold
integer, dimension(:), allocatable :: itmp
nold = size(iorig)
allocate(itmp(2*nold)) ! double size; assume no error
itmp(1:nold) = iorig(1:nold) ! copy existing contents explicitly
call move_alloc(itmp, iorig)
! itmp deallocated, iorig now refers to the memory that had been itmp
This minimises the number of copies involved in re-assigning the original storage.
move_alloc() efficiency
A thought exercise. How many copies would be required if
move_alloc() was not available when enlarging the size of
an existing allocatable array?
If move_alloc() were not available, we would need to
make two copies rather than one. Analogously to the above example, we
would have to perform the following to double the storage in
iorig:
FORTRAN
integer :: nold
integer, dimension(:), allocatable :: itmp
nold = size(iorig)
allocate(itmp(nold)) ! allocate temporary storage for original data
itmp(:) = iorig(:) ! first copy from original storage into the temporary
deallocate(iorig) ! deallocate the original array
allocate(iorig(2*ndold)) ! and then reallocate at twice the size
iorig(1:nold) = itmp(:) ! second copy from the temporary into the new storage
deallocate(itmp) ! deallocate the temporary
Arrays of pointers
A small trick is required to arrange an array of pointers. Recall that
is a pointer to a rank one array, and not a rank one array of pointers. If one did want an array of such objects, it can be achieved by wrapping it in a type:
FORTRAN
type :: pointer_rr1
real, dimension(:), pointer :: p => null()
end type pointer_rr1
type (pointer_rr1), dimension(10) :: a
a(1)%p => null()
So a is a rank one array of the new type, each element
having the component p which is a pointer to a real rank
one type.
- Pointers are extremely useful for certain types of operations: they
provide a way to refer indirectly to other variables or names (they act
as an
_alias_), or can be used for establishing dynamic data structures. - Remember to always initialise a pointer, to
null()if necessary. - If a pointer is allocated, memory for its type is allocated and the pointer becomes associated.
- The
move_alloc()intrinsic function moves a memory allocation from one allocatable variable to another.
Content from Procedures again: interfaces
Last updated on 2026-02-21 | Edit this page
Estimated time: 40 minutes
Overview
Questions
- What is an interface and why might I need to write one?
- Can I use a function or a subroutine as an argument to another procedure?
- How can I write a procedure that will accept arguments of different types?
- Can I overload arithmetic and relational operators to work with my derived types?
- Can I write a function that will accept both scalar and array arguments?
Objectives
- Understand the need for interfaces and recognise when they are necessary.
- Be able to write an interface to allow the passing of a procedure as an actual argument.
- Understand and be able to implement overloading and polymorphism.
- Understand what an
elementalfunction is and be able to write one.
So far, we have demanded that all procedures be defined as part of a
module. This has the important advantage that it makes the
interface to the function or subroutine explicit to
the compiler, and to other program units via the use
statement. The compiler can then check the order and type of the actual
arguments.
Other situations arise where we may need to give the compiler additional information about the interface.
An external function
Consider a program unit (which may or may not be a separate file) which contains a function declaration outside a module scope. E.g.,
FORTRAN
function my_mapping(i) result(imap)
integer, intent(in) :: i
integer :: imap
...
end function my_mapping
If we compile a program which includes a reference to this function, an error may result. We have not given the compiler any information about how the function is meant to be called.
It is possible to provide the compiler with some limited information
about the return value of the function with the external
attribute (also available as a statement). E.g.,
However, we have still not given the compiler full information about
the interface. The interface is said to remain implicit. To
make it explicit, the interface construct is available:
FORTRAN
program example
implicit none
interface
function my_mapping(i) result(imap)
integer, intent(in) :: i
integer :: imap
end function my_mapping
end interface
...
This has provided a full, explicit, statement about the interface of
the function my_mappping(). (Note that the dummy argument
names are not significant, but the function name and the argument types
and intents are.)
The compiler should now be able to check the arguments are used correctly in the calling program (as if we had declared the function in a module).
Interface blocks are necessary in other contexts.
Exercise (5 minutes)
Externals and interfaces
To illustrate the points made above, a very simple external function is defined in the file external.f90, and an accompanying program example1.f90 calls the function therein.
Try to compile the two files, e.g.:
What happens?
Try adding the appropriate external declaration
What happens if you try to compile the program now? (Note at this
point that there are no modules involved, so no .mod files
will appear).
Then try running the program. Is the output what you expect?
Finally, remove the external declaration and try to
introduce the correct interface block. What happens
now?
Without either the external declaration of the function
or an interface, the compilation will simply fail. gfortran
produces the following:
OUTPUT
example1.f90:24:33:
24 | print *, "The array size is: ", array_size(a), size(a)
| 1
Error: Function 'array_size' at (1) has no IMPLICIT type
Adding the external declaration allows compilation to
succeed, but the following output is produced on running the
program:
OUTPUT
The array size is: 0 6
The function call returns an incorrect value of 0 where the (3,2) array has an actual size of 6.
Removing the external declaration and adding an interface matching
the function in external.f90 as follows will help (and you
may have already spotted the issue):
FORTRAN
interface
function array_size(a) result(isize)
real, dimension(:), intent(in) :: a
integer :: isize
end function array_size
end interface
Compiling now produces an error again, as reported here by
gfortran:
OUTPUT
example1.f90:23:45:
23 | print *, "The array size is: ", array_size(a), size(a)
| 1
Error: Rank mismatch in argument 'a' at (1) (rank-1 and rank-2)
In the main program we were attempting call array_size()
on a rank two array, while in external.f90 we implemented
it for rank one arrays. Now that we have an interface, the compiler can
see that the way we’re calling the function is incorrect, so it produces
the error and fails compilation.
This is preferable to the external declaration by which
we essentially asked the compiler to trust that it can call
array_size() however we tell it to do so, even though it
was in this case incorrect.
Passing functions or subroutines as arguments
Sometimes it is convenient to provide a procedure as an argument to another function or subroutine. Good examples of this are for numerical optimisation or numerical integration methods where a user-defined function needs to be evaluated for a series of different arguments which cannot be prescribed in advance.
This can be done if an interface block is provided which describes to the calling procedure the function that is the dummy argument. E.g.,
FORTRAN
subroutine my_integral(a, b, afunc, result)
real, intent(in) :: a
real, intent(in) :: b
interface
function afunc(x) result(y)
real, intent(in) :: x
real :: y
end function afunc
end interface
...
The function dummy argument has no intent. One cannot in general use intrinsic procedures as actual arguments.
Limited polymorphism
We have seen a number of intrinsic functions which take arguments of
different types, such as mod(). Such a situation where the
arguments can take a different form is sometimes referred to as limited
polymorphism (or overloading).
However, it is not possible in Fortran to define two procedures of the same name, but different arguments (at least in the same scope). We need different names; suppose we have two module sub-programs, schematically:
FORTRAN
subroutine my_specific_int(ia)
integer, intent(inout) :: ia
... integer implementation ...
end subroutine my_specific_int
subroutine my_specfic_real(ra)
real, intent(inout) :: ra
... real implementation ...
end subroutine my_specific_real
A mechanism exists to allow the compiler to identify the correct routine based on the actual argument when used with a generic name. This is:
FORTRAN
interface my_generic_name
module procedure my_specific_int
module procedure my_specific_real
...
end interface my_generic_name
This should appear in the specification (upper) part of the relevant module. The two specific implementations must be distinguishable by the compiler, that is, at least one non-optional dummy argument must be different.
Exercise
An interface for polymorphic PBMs
In the earlier episode on I/O, we wrote a
module to produce a .pbm image file. The accompanying
module pbm_image.f90
provides two implementations of such a routine: one for a logical array,
and another for an integer array.
Check you can add the appropriate interface block with
the generic name write_pbm to allow the program example3.f90
to be compiled correctly.
We need to add an interface to the module specification for
write_pbm which allows use of both the
write_logical_pbm and write_integer_pbm
subroutines. You should be able to follow the description above to do
so. The only minor complication is that the module uses
private by default; unless we specify public
for the new generic interface, it will remain hidden from the main
program.
Putting this together, the following should allow you to compile and run the program with no errors:
FORTRAN
public :: write_pbm
interface write_pbm
module procedure write_logical_pbm
module procedure write_integer_pbm
end interface
With this, the internal workings of the module are hidden away and
its use is entirely via write_pbm.
Operator overloading
For simple derived types it may be meaningful to define relational and arithmetic operators. For example, if we had a date type such as
it may be meaningful to ask whether two dates are equal and so on (it would not really be meaningful to add one date to another).
One can write a function to do this:
FORTRAN
function my_dates_equal(date1, date2) result(equal)
type (my_date), intent(in) :: date1
type (my_date), intent(in) :: date2
logical equal
! ...
end function my_dates_equal
As a syntactic convenience, it might be useful to use ==
in a logical expression using dates. This can be arranged via
Again this should appear in the relevant specification part of the
relevant module. Such overloading is possible for relational operators
==, /=, >=,
<=, > and <. If
appropriate, overloading is also available for arithmetic operators
+, -, *, and /.
It is also possible to overload assignment =.
Elemental functions
Again, we have seen that some intrinsic functions allow either scalar or array actual arguments. The same effect can be achieved for a user-defined function by declaring it to be elemental. The procedure is declared in terms of a scalar dummy argument, but then may be applied to an array actual argument element by element.
Such a procedure should be declared:
FORTRAN
elemental function my_function(a) result(b)
integer, intent(in) :: a
integer :: b
! ...
end function my_function
An invocation should be, e.g.:
All arguments (and function results) must conform. An elemental
routine usually must also be pure.
Exercise (5 minutes)
Elemental conversion from logical to PBM
In the pbm_image.f90
module there is a utility function logical_to_pbm() which
is used in write_logical_pbm() to translate the logical
array to an integer array. Refactor this part of the code to use an
elemental function.
The function logical_to_pbm() is currently
pure and takes in a scalar logical lvar to
return the scalar integer ivar. The function is currently
pure. Everything is already in place to convert the
function to elemental:
FORTRAN
elemental function logical_to_pbm(lvar) result (ivar)
! Utility to return 0 or 1 for .false. and .true.
logical, intent(in) :: lvar
integer :: ivar
ivar = 0
if (lvar) ivar = 1
end function logical_to_pb
All that remains is to have write_logical_pbm() call
this new elemental version. Further down in that function
you will see the old nested loop to move through the map
array and from it use logical_to_pbm() to fill the
imap array:
FORTRAN
do j = 1, size(map, dim = 2)
do i = 1, size(map, dim = 1)
imap(i,j) = logical_to_pbm(map(i,j))
end do
end do
With the new elemental version of
logical_to_pbm(), we can replace this entire structure with
the single line:
Exercise (20 minutes)
Integrating a function
Write a module/program to perform a very simple numerical integration of a simple one-dimensional function f(x). We can use a trapezoidal rule: for lower and upper limits a and b, the integral can be approximated by
(b - a)*(f(a) + f(b))/2.0
We can go further and split the interval between a and
b into small sections of size h = (b - a)/n.
In a similar manner, approximating the integral of each small section
with a trapezium allows us to estimate the total integral to be
h*(f(a) + sum + f(b))/2.0
with the sum
sum = 0.0
do k = 1, n-1
sum = sum + 2.0*f(a+k*h)
end do
Write a procedure that will take the limits a and
b, the integer number of steps n, and the
function, and returns a result.
To check, you can evaluate the function cos(x) sin(x)
between a = 0 and b = pi/2 (the answer should
be 1/2). Check your answer gets better for value of
n = 10, 100, 1000.
A sample solution is provided in integral_program.f90 and integral_module.f90.
- An interface provides important information about a procedure to the compiler. Without this information, the compiler may not be able to use it.
- When using modules, interfaces are generated automatically and provided to the compiler.
- If not in a module, a function can be made visible to the compiler
by declaring it with the
externalattribute, but the compiler can go further by checking the argument types if you write a full, explicitinterfaceblock. - Interfaces can also be used to implement limited polymorphism and operator overloading for derived types.
- An elemental function is one which can be applied to a scalar actual argument or element-by-element to an array actual argument.
Content from Break
Last updated on 2026-02-21 | Edit this page
Comfort break.
Content from Miscellaneous
Last updated on 2026-02-21 | Edit this page
Estimated time: 20 minutes
Overview
Questions
- How can I retrieve any command line arguments I want to give my program?
- How can I examine environment variables?
- Can I have my Fortran code execute OS commands on the terminal?
- Can I get the time and date?
Objectives
- Learn several useful procedures that allow your programs to interact with the outside shell and environment.
- Learn how to obtain the time and date and be able to time sections of code.
There a a number of features which have not been covered so far, but are useful in certain contexts.
Interacting with the environment
Command line arguments
If you wish to write a program which makes use of command line arguments, or simply wish to know the executable name at run time, the command line can be retrieved.
The number of command line arguments is returned by the function
This returns an integer: zero if the information is not
available, or the number of arguments not including the
executable name itself.
The entire command line can be retrieved as a string via
This returns the command line as a string (truncated or padded with spaces as appropriate), the integer length of the command line string, and an integer status which will be non-zero if the information is not available. All the arguments are optional.
Individual command line arguments based on their position can be retrieved using the subroutine
FORTRAN
subroutine get_command_argument(position, value, length, status)
integer, intent(in) :: position
character (len = *), optional, intent(out) :: value
integer, optional, intent(out) :: length
integer, optional, intent(out) :: status
Here, position is zero for the (executable) command
itself and positive for others. The remaining arguments are:
value is the command; length is the length of the command,
and istat returns 0 on success, -ve if the arg
string is too short, or +ve if the information is not available.
Environment variables
A similar routine exists for inquiry about environment variables:
FORTRAN
subroutine get_environment_variable(name, value, length, status, trim_name)
character (len = *), intent(in) :: name
character (len = *), optional, intent(out) :: value
integer, optional, intent(out) :: length
integer, optional, intent(out) :: status
logical, optional, intent(in) :: trim_name
This will return the value associated with the given name if it
exists. Various non-zero status error conditions can occur,
including a value of 1 if the variable does not exist, or
-1 if the value is present, but is too long to fit in the
string provided.
System commands
It is sometimes useful to pass control of execution back to the operating system so that some other command can be used.
FORTRAN
subroutine execute_command_line(command, wait, iexit, icmd, cmdmsg)
character (len = *), intent(in) :: command
logical, optional, intent(in) :: wait
integer, optional, intent(inout) :: iexit
integer, optional, intent(out) :: icmd
character (len = *), optional, intent(inout) :: cmdmsg
The command should be a string. The wait argument tells
the routine to return only when the command has finishing executing. The
iexit argument gives the return value of the command. The
icmd argument returns a positive value if the command fails
to execute (e.g., the command was not found), or negative if execution
is not supported, or zero on successful execution. An informative
message should be returned in cmdmsg if icmd
is positive.
It is recommended to use wait = .true. always. Portable
programs should use system commands with extreme caution, or not at
all.
Time and date from date_and_time()
Use, e.g.,
FORTRAN
character (len = 8) :: date ! "yyyymmdd"
character (len = 10) :: time ! "hhmmss.sss"
character (len = 5) :: zone ! "shhmm"
integer, dimension(8) :: ivalues ! see below
call date_and_time(date = date, time = time, zone = zone, values = ivalues)
All the arguments are optional. On return the date holds
the year, month, and day in a string of the form “yyyymmdd”;
time holds the time in hours, minutes, seconds, and
milliseconds; the time zone is encoded with a sign (+ or
-) and the time difference from Coordinated Universal Time
(UTC) in hours and minutes.
ivalues returns the same information as the other three
arguments in the form of an array of integers. These are, in order:
year, month (1-12), day (1-31), time difference in minutes between local
and UTC, hour (0-23), minute (0-59), seconds (0-59), and milliseconds
(0-999).
Timing pieces of code
If you want to record the time taken to execute a particular section
of code, the cpu_time() function can be used. This returns
a real positive value which is some system-dependent time
in seconds. Subtracting two consecutive values will give an elapsed
time:
FORTRAN
real :: t0, t1
call cpu_time(t0)
! ... code to be timed here ...
call cpu_time(t1)
print *, "That piece of code took ", t1-t0, " seconds"
In the unlikely event that there is no clock available, a negative
value may be returned from cpu_time().
- Fortran provides other utility procedures that can help with your program’s functionality.
- These allow you to retrieve information from the calling command line and environment.
- You can pass commands back to the operating system, but you should be very careful that your code remains portable.
- Timing your code with
cpu_time()can help you profile your code.
Content from Lunch
Last updated on 2026-02-21 | Edit this page
Break for lunch.
Content from Other things you may see
Last updated on 2026-02-21 | Edit this page
Estimated time: 15 minutes
Overview
Questions
- What have we not covered that I should be aware of?
Objectives
- Learn about some other Fortran features which may be of interest.
Not covered in this course
The Fortran standard covers a large and increasing number of features. Some of these we have only touched on, and others not mentioned at all.
- Object-oriented features: type extension, abstract classes/interfaces, …
- Interoperability with C
- Support for IEEE floating point arithmetic
- Coarrays
Things you may see
Old stuff
Don’t panic! Consult a reference to see what is happening.
May include:
- old-style declarations
real*4,real*8,character*10etc. -
GO TOstatements -
DATAblocks
Some features are formally obsolescent, in which case the compiler should give a warning. There should be a more modern (“better”) way doing the same thing. Obsolescent features will be deleted at some point in the future.
Preprocessor
The processing of Fortran is not standardised in the same way as the C-preprocessor is part of the C standard.
However, it is very common to see preprocessor directives for conditional compilation, and other preprocessor features. E.g.,
As the Fortran preprocessor is not standardised, some care may be required to ensure portability. For example, stringification of macro arguments can be problematic.
An additional compiler flag may be required to switch on the
preprocessor explicitly. Alternatively, it is common that compilers will
automatically run the preprocessor if the file extension has a capital
letter e.g., .F90, .F03, and so on.
Parallelism
where, forall
Fortran has introduced a number of constructs which are intended to allow the expression of parallelism.
While the standard allows these, and all compilers should inplement them correctly, few compilers actually introduce any parallelism.
Loops are a perfectly good solution.
Message passing interface
The message passing interface provides a standard interface for distributed memory computing.
MPI makes use of a number of data types, macro definitions, and library subroutines. A modern program might introduce the information required via
which uses derived types for data types (which are often opaque).
Earlier versions might use
where the opaque types are integer handles.
Older codes may even use
to make the necessary handles and macros available.
MPI library calls
MPI is at base a C interface which accommodates Fortran. The C routines often have prototypes of the form:
FORTRAN
int MPI_Send(const void * buf, int ount, MPI_Datatype dt, int dest, int tag,
MPI_Comm comm);
The data to be sent is identified here using void * buf.
A return code provides an error status.
The lack of void * in Fortran means that the Fortran
API, formally, has been on a rather shaky foundation for a long time.
This is being addressed in the most recent standards.
In particular, the use of array sections as the buf
argument might prove problematic and should probably be avoided. It is
also preferable for the application to marshal data into a contiguous
buffer for performance reasons.
In modern versions, the error return code in Fortran are optional integer arguments.
OpenMP
OpenMP is a standard way to introduce thread-level parallelism (typically at the level of loops). A program should
to provide OpenMP functions and kind type parameters.
OpenMP is largely based around compiler directives, which are
switched on via a compiler option, usually -fopenmp.
In Fortran, the directives are introduced by the sentinels:
which is ignored as a comment if no OpenMP is required
(cf. #pragma omp ... { } in C).
The OpenMP standard documents provide a useful stub
implementation which can be used in place of the real implementation
when compiling without OpenMP. This prevents a profileration of
conditional compilation directives.
Testing
Testing is an important consideration in modern software engineering. There are a number of unit test frameworks such as pFUnit.
Some others are mentioned at the Fortran wiki.
Resources
The standard reference is “Modern Fortran Explained” by Metcalf, Reid, Cohen and Bader (Oxford University Press). The latest version is “Modern Fortran Explained: Incorporating Fortran 2023” (2023).
The Fortran wiki also has a lot of useful material.
- You may come across Fortran following an older standard; some will be understandable on sight, but you may need to consult a reference.
- MPI and OpenMP allow you to write Fortran code that will execute in parallel.
- NVIDIA supports CUDA in Fortran for execution on GPUs.
- Writing tests is an important step in safe code development.
- Several books and the Fortran Wiki are invaluable resources.
Content from Exercises: conjugate gradient and matrices
Last updated on 2026-02-21 | Edit this page
Estimated time: 60 minutes
Overview
Questions
- How can I really push what I’ve learnt so far?
Objectives
- Write a conjugate gradient solver.
- Read a Matrix Market Exchange file and plot a downloaded matrix.
Exercises
To finish things off, here are two fairly substantial exercises you can tackle which should put everything you’ve learnt to the test.
Conjugate gradient solver
Contrasting with the very specific tri-diagonal solver we’ve been working on, the conjugate gradient method provides a slightly more general method for the solution of linear systems
\(Ax = b\)
where the matrix \(A\) is symmetric positive definite. The algorithm is explained in detail on Wikipedia, and if you are very interested you might like to read An Introduction to the Conjugate Gradient Method Without the Agonizing Pain.
However, it is not necessary to understand the details, as here we are just interested in implementing the algorithm.
There are two main steps involved. The first is to perform a
matrix-vector multiplication. This can be done using the Fortran
intrinsic function matmul(). (Or you can have a go at
implementing your own version - it’s not too difficult.)
The second step is to compute a scalar residual from a vector
residual. If we have an array (vector) r(1:n) this can be
done with:
All the operations in the algorithm can be composed of these, as well as vector additions and multiplications by a scalar.
A template program cg_test.f90
is provided with a small matrix to use as a test. You need to implement
a module cgradient which supplies a function
cg_test() taking the arguments set out in the template.
Remember that you can always check your answer by multiplying out \(Ax\) to recover the original right-hand side \(b\).
You can check a suggested solution with cg_test.f90 and cgradient_solution.f90.
Storage of large sparse matrices
The Matrix Market provides a number of archived matrices of different types. It also defines a simple ASCII format for the storage of sparse matrices.
The Matrix Market Exchange format .mtx files are
structured as follows.
OUTPUT
%% Exactly one header line starting %%
% Zero or more comment lines starting %
nrows ncols nnonzero
i1 j1 a(i1,j1)
i2 j2 a(i2,j2)
...
where nrows and ncols are the number of
rows and columns in the matrix, respectively. nnonzero is
the number of non-zero entries in the matrix. For each non-zero entry
there then follows a single line which contains the row index, the
column index, and the value of the matrix element itself.
Download an example, e.g.,
BASH
$ wget https://math.nist.gov/pub/MatrixMarket2/Harwell-Boeing/laplace/gr_30_30.mtx.gz
$ gunzip gr_30_30.mtx
If you look at the first few line of this example, you should see
OUTPUT
%%MatrixMarket matrix coordinate real symmetric
900 900 4322
1 1 8.0000000000000e+00
2 1 -1.0000000000000e+00
31 1 -1.0000000000000e+00
32 1 -1.0000000000000e+00
2 2 8.0000000000000e+00
Define a type that can hold the sparse representation and provide a procedure which initialises such a type from a file.
Provide a procedure which brings into existence a dense matrix (just a two-dimensional array) initialised with the correct non-zero elements.
Use the
.pbmfile generator to produce an image of the non-zero elements to provide a check the file has been read correctly.Try some other matrices from the Matrix Market.
A suggested solution in provided in mmtest.f90 and mmarket.f90.
- Fortran performance and array handling make it ideal for the solution of intense problems.
Content from Break
Last updated on 2026-02-21 | Edit this page
Comfort break.
Content from Exercises
Last updated on 2026-02-21 | Edit this page
Overview
Questions
Objectives
We will use the rest of today to work on any exercises from the previous lessons you still wish to go over.