Beginner's Guide to Fortran

  ·   19 min read

Introduction to Fortran #

What is Fortran? #

Fortran (originally FORTRAN, from FORmula TRANslation) is a general-purpose, compiled imperative programming language that is especially popular for numerical and scientific computing. It’s known for its efficiency and its extensive libraries for mathematical operations, making it a powerful tool for tasks ranging from complex simulations to data analysis. Unlike many modern languages, Fortran emphasizes readability through a structured and declarative style, prioritizing clear expression of algorithms. While it has evolved significantly since its inception, modern Fortran (Fortran 90 and beyond) incorporates many features found in contemporary languages, such as modularity and dynamic memory allocation.

Why Learn Fortran? #

Despite its age, Fortran remains highly relevant due to several compelling reasons:

  • High Performance: Fortran’s compiler optimizations and its strong legacy in scientific computing translate to highly efficient code, crucial for computationally intensive applications.
  • Extensive Libraries: A rich ecosystem of mature libraries exists for various scientific and engineering domains, including linear algebra (LAPACK, BLAS), parallel computing (MPI), and more. Leveraging these libraries can drastically reduce development time and improve performance.
  • Large Existing Codebase: A significant portion of legacy scientific and engineering software is written in Fortran. Maintaining, updating, and extending this code requires Fortran expertise.
  • Community Support: Although smaller than some modern language communities, the Fortran community is active and supportive, providing resources and assistance to developers.
  • Strong Typing: Fortran’s strong typing system helps catch errors early in the development process, leading to more robust and reliable code.

Fortran’s History and Applications #

Fortran’s history dates back to the mid-1950s, making it one of the oldest high-level programming languages still in widespread use. Its initial development focused on scientific computation, and it quickly became the dominant language for this purpose. Over the decades, it has undergone several revisions, incorporating features to improve its capabilities and address modern programming paradigms.

Today, Fortran continues to be vital in various fields:

  • High-performance computing (HPC): Simulations, modeling, and data analysis in areas like weather forecasting, climate modeling, astrophysics, and materials science.
  • Engineering: Finite element analysis, computational fluid dynamics (CFD), and other simulation tasks.
  • Financial modeling: Quantitative analysis and risk management.
  • Scientific data analysis: Processing and analyzing large datasets from scientific experiments and simulations.

Setting up your Fortran Environment #

To start programming in Fortran, you’ll need a Fortran compiler and a suitable Integrated Development Environment (IDE) or text editor. Several excellent options are freely available:

  • Compilers: Popular choices include gfortran (part of the GNU Compiler Collection – GCC), Intel Fortran Compiler, and LLVM’s Flang compiler. These compilers translate your Fortran code into machine-executable instructions.

  • IDEs: Many IDEs offer support for Fortran, including:

    • Eclipse with the PDT plugin (or similar Fortran plugins): Provides code completion, debugging tools, and project management.
    • Visual Studio Code: With appropriate extensions, VS Code can provide syntax highlighting, debugging, and other useful features.
    • Dedicated Fortran IDEs: There may be more specialized IDEs available, though their availability and support might vary.
  • Installation: The specific steps for installing a compiler and IDE will depend on your operating system (Windows, macOS, Linux). Consult the documentation for your chosen compiler and IDE for detailed instructions. Typically, you’ll download the installer, run it, and configure your system to use the compiler and IDE. After installation, you might need to configure your environment variables (path) to correctly point to the compiler’s executable. This will allow you to invoke the compiler from the command line or terminal.

Remember to check the documentation for your chosen compiler and IDE for the most up-to-date instructions and troubleshooting information.

Basic Syntax and Structure #

Program Structure #

A Fortran program consists of one or more program units. The simplest program unit is a main program, which is where execution begins. A main program generally follows this basic structure:

program program_name
  implicit none  ! Always use implicit none!

  ! Variable declarations
  integer :: i, j
  real :: x, y
  character(len=20) :: name

  !Executable statements
  i = 10
  x = 3.14159
  print *, "Hello, world!"
  print *, i, x

end program program_name
  • program program_name: This line declares the beginning of the main program and gives it a name. The name should be descriptive of the program’s purpose.

  • implicit none: This crucial statement disables implicit type declarations. It forces you to explicitly declare the type of every variable, which helps prevent errors and improves code readability. Always use implicit none.

  • Variable Declarations: Variables must be declared before they are used. The declaration specifies the variable’s name and its data type.

  • Executable Statements: These are the lines of code that perform calculations, input/output operations, and other actions.

  • end program program_name: This line marks the end of the main program.

Variables and Data Types #

Variables are named storage locations that hold data. Fortran has several built-in data types:

  • integer: Stores whole numbers (e.g., 10, -5, 0).
  • real: Stores floating-point numbers (e.g., 3.14, -2.5, 0.0). real(kind=8) can be used for double-precision floating-point numbers (more accurate).
  • complex: Stores complex numbers (e.g., (1.0, 2.0)).
  • logical: Stores Boolean values (.true. or .false.).
  • character: Stores strings of characters (e.g., “Hello”). character(len=n) specifies the maximum length of the string.

Example:

integer :: count
real :: temperature
logical :: is_active
character(len=50) :: message

Constants #

Constants are values that cannot be changed during program execution. They are declared using the parameter attribute:

integer, parameter :: max_value = 100
real, parameter :: pi = 3.14159

Input and Output (Read and Print) #

The simplest way to perform input and output is using the read and print statements:

  • print *, ...: Prints values to the console. The asterisk (*) indicates default formatting.

  • read *, ...: Reads values from the console. The asterisk indicates free-format input.

Example:

program io_example
  implicit none
  integer :: age
  print *, "Enter your age:"
  read *, age
  print *, "You entered:", age
end program io_example

More sophisticated I/O operations are possible using formatted I/O, which provides greater control over the appearance of the output.

Arithmetic Operators #

Fortran supports standard arithmetic operators:

  • + (addition)
  • - (subtraction)
  • * (multiplication)
  • / (division)
  • ** (exponentiation)

Example:

real :: a, b, result
a = 10.0
b = 5.0
result = a + b * 2.0  ! result will be 20.0

Comments and Formatting #

Comments are used to explain the code. In Fortran, comments begin with an exclamation mark (!) and extend to the end of the line.

Good formatting improves readability:

  • Use consistent indentation (e.g., 2 or 4 spaces).
  • Add blank lines to separate logical sections of code.
  • Use meaningful variable names.

Example:

! This program calculates the area of a rectangle
program rectangle_area
  implicit none
  real :: length, width, area

  ! Get the length and width from the user
  print *, "Enter the length:"
  read *, length
  print *, "Enter the width:"
  read *, width

  ! Calculate the area
  area = length * width

  ! Print the result
  print *, "The area is:", area

end program rectangle_area

Control Flow #

Conditional Statements (IF-THEN-ELSE) #

Conditional statements allow you to execute different blocks of code based on whether a condition is true or false. Fortran uses the IF-THEN-ELSE construct:

Simple IF:

if (condition) then
  ! Code to execute if the condition is true
end if

IF-THEN-ELSE:

if (condition) then
  ! Code to execute if the condition is true
else
  ! Code to execute if the condition is false
end if

IF-THEN-ELSE IF-ELSE:

if (condition1) then
  ! Code to execute if condition1 is true
else if (condition2) then
  ! Code to execute if condition2 is true
else if (condition3) then
  ! Code to execute if condition3 is true
...
else
  ! Code to execute if none of the conditions are true
end if

Example:

program conditional_example
  implicit none
  integer :: number

  print *, "Enter a number:"
  read *, number

  if (number > 0) then
    print *, "The number is positive."
  else if (number < 0) then
    print *, "The number is negative."
  else
    print *, "The number is zero."
  end if
end program conditional_example

Loops (DO loops) #

DO loops are used to repeat a block of code a specific number of times. The basic syntax is:

do i = start, stop [, step]
  ! Code to be repeated
end do
  • i: The loop counter variable (must be an integer).
  • start: The initial value of the counter.
  • stop: The final value of the counter. The loop continues as long as i <= stop.
  • step (optional): The increment for the counter (defaults to 1).

Example:

program do_loop_example
  implicit none
  integer :: i, sum

  sum = 0
  do i = 1, 10
    sum = sum + i
  end do
  print *, "The sum of numbers from 1 to 10 is:", sum
end program do_loop_example

You can also use exit to prematurely terminate a do loop and cycle to skip to the next iteration.

Logical Operators #

Logical operators are used to combine or modify logical expressions:

  • .not. (negation): Reverses the truth value of a logical expression.
  • .and. (conjunction): True only if both operands are true.
  • .or. (disjunction): True if at least one operand is true.
  • .eqv. (equivalence): True if both operands have the same truth value.
  • .neqv. (non-equivalence): True if both operands have different truth values.

Example:

program logical_operators
  implicit none
  logical :: a, b, result

  a = .true.
  b = .false.

  result = a .and. b  ! result will be .false.
  result = a .or. b   ! result will be .true.
  result = .not. a    ! result will be .false.

end program logical_operators

Remember that in Fortran, comparisons use double equals signs (==) for equality and /=, <, >, <=, >= for inequality and other comparisons.

Arrays and Data Structures #

Declaring and Initializing Arrays #

Arrays are used to store collections of elements of the same data type. In Fortran, arrays are declared specifying their type, name, and size (number of elements).

Declaration:

type(type_name), dimension(size) :: array_name
  • type_name: The data type of the array elements (e.g., integer, real, character).
  • size: The number of elements in the array (e.g., 10, (10) which is equivalent, (1:10) for an array with indices from 1 to 10).

Initialization:

Arrays can be initialized at the time of declaration using several methods:

  • Explicit initialization: Listing the values directly:
integer, dimension(5) :: numbers = [1, 2, 3, 4, 5]
  • Implicit initialization: Using the =0 syntax to initialize elements to zero:
real, dimension(10) :: values = 0.0  ! Initializes all elements to 0.0
  • Using DO loops: For more complex initialization patterns, DO loops are useful:
integer, dimension(10) :: squares
do i = 1, 10
  squares(i) = i**2
end do

Array Operations #

Fortran provides concise ways to perform operations on arrays:

  • Element-wise operations: Arithmetic and logical operators can be applied to entire arrays:
real, dimension(10) :: a, b, c
a = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0]
b = [10.0, 9.0, 8.0, 7.0, 6.0, 5.0, 4.0, 3.0, 2.0, 1.0]
c = a + b  ! Element-wise addition
  • Intrinsic functions: Many intrinsic functions work on arrays, performing the operation on each element. Examples include SUM, MAX, MIN, AVG, DOT_PRODUCT and others.
real, dimension(5) :: x = [1.0, 2.0, 3.0, 4.0, 5.0]
real :: sum_x
sum_x = SUM(x) ! sum_x will be 15.0
  • Array sections: You can access parts of an array using array sections:
integer, dimension(10) :: array
array = [(i,i=1,10)] ! fills array with 1 through 10
print *, array(1:5)   ! Prints elements 1 through 5
print *, array(2:2:10) ! Prints elements at even indices

Multi-dimensional Arrays #

Multi-dimensional arrays can be declared by specifying multiple dimensions in the dimension attribute:

real, dimension(3, 4) :: matrix

This declares a 3x4 matrix (3 rows, 4 columns). Elements are accessed using multiple indices:

matrix(1, 1) = 1.0
matrix(2, 3) = 7.0

print *, matrix(1,2)

Initialization for multi-dimensional arrays can be done similarly to one-dimensional arrays but often requires nested loops or array constructors:

integer, dimension(2,3) :: my_array = reshape([1, 2, 3, 4, 5, 6], shape(my_array))

or

integer, dimension(2,3) :: another_array
do i=1,2
  do j=1,3
    another_array(i,j) = i*10+j
  end do
end do

Remember that the order of the indices corresponds to the order in the dimension attribute. Fortran uses column-major order (elements within the same column are stored contiguously in memory). This is different from row-major order used in some other languages (C, C++, etc.).

Functions and Subroutines #

Fortran uses functions and subroutines to modularize code, promoting reusability and readability. Functions return a single value, while subroutines perform actions without necessarily returning a value.

Defining Functions and Subroutines #

Function:

function function_name(argument1, argument2, ...) result(result_variable)
  implicit none
  ! Declare variables
  type(type_name) :: argument1, argument2, ...
  type(result_type) :: result_variable

  ! Function body (calculations)
  result_variable = ...  ! Assign a value to the result variable

end function function_name
  • function_name: The name of the function.
  • argument1, argument2, …: The input arguments.
  • result(result_variable): Specifies the name of the variable that holds the function’s return value. The type of result_variable determines the return type of the function.

Subroutine:

subroutine subroutine_name(argument1, argument2, ...)
  implicit none
  ! Declare variables
  type(type_name) :: argument1, argument2, ...
  ! Declare local variables

  ! Subroutine body (actions)

end subroutine subroutine_name
  • subroutine_name: The name of the subroutine.
  • argument1, argument2, …: The input and/or output arguments. The subroutine can modify the values of the arguments.

Example:

function add(x, y) result(sum)
  implicit none
  real :: x, y
  real :: sum
  sum = x + y
end function add

subroutine print_message(message)
  implicit none
  character(len=*), intent(in) :: message
  print *, message
end subroutine print_message

Passing Arguments #

Arguments are passed to functions and subroutines using their names in the function/subroutine call. The intent attribute can specify the intended use of an argument:

  • intent(in): The argument is used only for input; its value will not be changed by the function/subroutine.
  • intent(out): The argument is used only for output; its initial value is ignored.
  • intent(inout): The argument is used for both input and output.

Example showing intent:

subroutine modify_value(x)
  implicit none
  integer, intent(inout) :: x
  x = x * 2
end subroutine modify_value

program test_modify
  implicit none
  integer :: a = 5
  call modify_value(a)
  print *, a  ! Output: 10
end program test_modify

Without the intent attribute, arguments are generally passed by reference (changes made inside the function/subroutine affect the original variable). Using intent makes the code more clear and can improve performance and prevent accidental modification of input data.

Function Return Values #

A function returns a value by assigning a value to the result variable declared in the result clause. The type of the result variable determines the return type of the function. The result variable’s name is also the name used to return the result.

Example:

function factorial(n) result(fact)
  implicit none
  integer, intent(in) :: n
  integer :: fact
  if (n == 0) then
    fact = 1
  else
    fact = n * factorial(n-1)  ! Recursive call
  end if
end function factorial

The value of fact is returned when the function completes. The calling program can then use the returned value:

program use_factorial
  implicit none
  integer :: num, result
  num = 5
  result = factorial(num)
  print *, "Factorial of", num, "is", result
end program use_factorial

Modules and Program Organization #

Modules are a crucial feature of modern Fortran, providing a way to organize code into reusable units and manage data effectively. They improve code maintainability, readability, and promote good programming practices.

Creating Modules #

A module is a program unit containing declarations and definitions that can be accessed by other program units. A module is created using the module statement followed by a contains statement (if the module contains subroutines or functions).

module my_module
  implicit none
  ! Declare variables, constants, types, and interfaces here

  integer, parameter :: max_value = 100
  type my_type
    integer :: id
    real :: value
  end type my_type

contains

  ! Define functions and subroutines here

  function calculate_something(x) result(result)
      ! ... function code ...
  end function calculate_something


end module my_module
  • The module is named my_module. Choose descriptive names.
  • implicit none should always be included.
  • Declarations of variables, constants, derived types, and interfaces go within the module. These items are then accessible to other program units that use the module.
  • Subroutines and functions are defined in the contains block.

Using Modules #

To use a module in another program unit, use the use statement:

program main_program
  use my_module  ! Accesses the contents of my_module

  implicit none
  integer :: i
  type(my_type) :: my_variable

  my_variable%id = 1
  my_variable%value = 3.14
  i = calculate_something(5)
  print *, i

end program main_program

This program uses the my_module, making its variables, constants, types, and procedures available. You can access module items using their names directly (e.g., max_value, calculate_something, my_type).

You can also rename module items when you use the module to avoid naming conflicts:

program main_program
  use my_module, only: max_val => max_value, calc_result => calculate_something
  implicit none
  ...
  i = calc_result(5) ! uses the renamed function
  print *, max_val   ! uses the renamed constant
end program main_program

Data Hiding and Encapsulation #

Modules support data hiding and encapsulation through the use of the private and public attributes. By default, all entities declared in a module are public (accessible from any program unit using the module). You can make specific entities private, restricting access to only within the module itself.

module my_module
  implicit none
  private  ! Default visibility is now private
  public :: max_value, calculate_something ! these are public

  integer, parameter :: max_value = 100
  integer :: private_variable = 10  ! this variable is not directly accessible outside the module

  contains
    function calculate_something(x) result(result)
        ! ... can use private_variable here ...
    end function calculate_something
end module my_module

In this example, private_variable is not directly accessible from program units using my_module; only calculate_something and max_value can be accessed. This promotes data protection and better code organization. This is particularly useful when building larger, more complex applications. It makes it easier to modify internal workings of the module without affecting external code that utilizes it.

Advanced Topics (Optional) #

This section covers more advanced Fortran features that are not strictly necessary for beginners but are important for building larger and more complex applications.

Pointers #

Pointers in Fortran allow you to indirectly access memory locations. A pointer variable doesn’t store the data itself, but rather the memory address where the data resides. This provides flexibility for dynamic memory allocation and manipulation of data structures.

Declaration:

type(type_name), pointer :: pointer_variable

Allocation:

Memory must be allocated for the pointer to point to using allocate:

allocate(pointer_variable)

Association:

A pointer is associated with a target using the => operator:

integer :: target_variable = 10
integer, pointer :: pointer_variable
pointer_variable => target_variable

Deallocation:

When you’re finished with the allocated memory, it’s crucial to deallocate it using deallocate:

deallocate(pointer_variable)

Pointers are powerful but require careful management to avoid memory leaks and dangling pointers.

Derived Data Types #

Derived data types allow you to create custom data structures by combining different data types. They are analogous to structs or classes in other languages.

Declaration:

type my_type
  integer :: id
  real :: value
  character(len=20) :: name
end type my_type

Usage:

type(my_type) :: my_variable
my_variable%id = 1
my_variable%value = 3.14
my_variable%name = "Example"

Derived types can improve code organization and readability by grouping related data together. They are fundamental to object-oriented programming in Fortran.

Object-Oriented Programming in Fortran #

Modern Fortran supports object-oriented programming (OOP) features, including:

  • Derived types: Serve as the basis for classes.
  • Type-bound procedures: Methods associated with a derived type.
  • Polymorphism: The ability of objects of different types to respond to the same method call in their own way (achieved through interfaces).
  • Inheritance: Creating new types that inherit properties from existing types (achieved through type extension).

OOP in Fortran is a powerful tool but requires a deeper understanding of OOP concepts. Many tutorials are available online for learning OOP in Fortran.

File Input/Output #

Fortran offers extensive features for file I/O, allowing you to read data from and write data to files. Basic I/O uses open, read, write, and close statements.

Opening a file:

integer :: file_unit
open(unit=file_unit, file='my_file.txt', status='old', action='read') ! 'old' for existing files. 'new' creates a file.

Reading from a file:

integer :: number
read(file_unit, *) number  ! Reads a number from the file

Writing to a file:

write(file_unit, *) number  ! Writes a number to the file

Closing a file:

close(file_unit)

More advanced features include formatted I/O (controlling the appearance of data in files), direct access (accessing specific records in a file), and error handling. Consult Fortran documentation for details on these features. Using formatted I/O significantly increases your control over how the data is read from and written to files. It also allows for more efficient storage in certain cases.

Debugging and Best Practices #

This section provides guidance on identifying and resolving common issues in Fortran programming and on writing clean, maintainable code.

Common Errors #

Beginners frequently encounter these types of errors in Fortran:

  • Type Mismatches: Attempting to perform operations on variables of incompatible types (e.g., adding an integer to a string). Fortran’s strong typing helps catch many of these at compile time, but some might only surface at runtime.

  • Array Index Errors: Accessing array elements outside the defined bounds (e.g., trying to access array(11) when the array only has 10 elements). This often leads to runtime crashes or unpredictable behavior.

  • Logical Errors: The program compiles and runs without errors, but produces incorrect results due to flaws in the program’s logic. Careful planning and testing are key to preventing these.

  • Uninitialized Variables: Using variables before they have been assigned a value. This can lead to unexpected results. implicit none helps mitigate this.

  • Incorrect Argument Passing: Passing arguments to functions or subroutines incorrectly (wrong data type, wrong number of arguments, etc.).

  • Memory Leaks: Failing to deallocate dynamically allocated memory using deallocate, leading to gradual memory exhaustion. This is particularly relevant when using pointers.

  • I/O Errors: Problems opening, reading from, or writing to files (e.g., incorrect file paths, file permissions).

Debugging Techniques #

Effective debugging strategies include:

  • Compiler Warnings: Pay close attention to compiler warnings. They often point to potential problems. Treat warnings as errors and correct them.

  • Print Statements: Insert print statements at strategic points in your code to display the values of variables and monitor program execution. This is a simple but effective way to track down the source of errors.

  • Debuggers: Use a debugger (many IDEs include debuggers) to step through your code line by line, inspect variables, and identify the point where errors occur. Debuggers greatly enhance debugging capabilities. They permit setting breakpoints, stepping through code, and evaluating expressions.

  • Static Analysis Tools: Some tools can analyze your code without executing it to identify potential problems like uninitialized variables or memory leaks.

  • Unit Testing: Write small test programs to verify the correctness of individual functions and subroutines. This makes it easier to isolate and fix problems.

Coding Style Guidelines #

Following consistent style guidelines improves code readability and maintainability:

  • Indentation: Use consistent indentation (e.g., 2 or 4 spaces) to clearly show the structure of your code.

  • Naming Conventions: Use descriptive names for variables, functions, and subroutines. Follow a consistent naming scheme (e.g., snake_case, camelCase).

  • Comments: Add comments to explain complex sections of code. Good commenting is crucial for maintaining and understanding code later. Document the purpose, behavior, and parameters of functions and subroutines.

  • Modularization: Break down large programs into smaller, manageable modules and subroutines. This improves code organization and reusability.

  • Error Handling: Include error checks and handling to gracefully deal with potential issues (e.g., file not found, invalid input).

  • Avoid implicit none only once: Always use implicit none at the beginning of every program unit to prevent accidental implicit type declarations.

  • Keep lines concise: Avoid excessively long lines of code. This improves readability.

By adhering to these practices, you’ll write higher quality, easier to debug, and better maintained Fortran code. Good coding style reduces debugging time and significantly improves collaborative efforts.