Modern Fortran Development
This skill covers writing modern, maintainable Fortran (2003/2008 and later) for scientific and numerical computing, including module organization, kind parameters, procedure design, memory safety, and testing.
Workflow for Writing a Modern Fortran Module
- Define shared kinds first — Create a
kinds_mod(or similarly named) module withiso_fortran_envorselected_real_kind/selected_int_kindparameters used across the whole project. - Design the module — Group related derived types and procedures into one focused module per file; declare
implicit noneat the top. - Write procedure interfaces — Give every dummy argument an explicit
intent(in),intent(out), orintent(inout); useuse, only:to import exactly what's needed. - Implement with early validation — Check preconditions (array bounds, allocation state, valid ranges) at the top of each procedure and return/stop early rather than nesting deeply.
- Manage arrays explicitly — Use allocatable arrays, check
allocated()before use, and deallocate when the lifetime isn't naturally scoped. - Build with warnings on — Compile with
-Wall -Wextra -std=f2008(gfortran) or the equivalent, and fail CI on new warnings. - Test — Write unit tests for individual procedures and integration tests for full numerical workflows, checking tolerances rather than exact floating-point equality.
Basic Principles
- Target modern Fortran standards — Fortran 2003, 2008, or newer — and avoid writing in a legacy FORTRAN 77 style just because the compiler still accepts it.
- Put
implicit nonein every program unit (module, program, and — via inheritance from a module or explicit statement — every procedure) so undeclared-variable typos are caught at compile time instead of producing silent wrong answers. - Put procedures in modules rather than external subprograms; module procedures get automatically-generated explicit interfaces, which lets the compiler catch argument mismatches that external procedures cannot.
- Keep modules focused on one concern and place each major module in its own file, named to match the module (e.g.,
module linear_solverinlinear_solver.f90). - Prefer clear, structured code over clever language tricks — Fortran numerical code is read far more often than it's written, usually by someone other than the original author.
- Avoid obsolete features:
COMMONblocks (replace with modules),GOTO-heavy control flow (replace with structuredif/do/select case, plusblockwhere useful), and numeric statement labels used as jump targets.
Kinds and Types
- Define numeric kind parameters in one shared module (e.g.,
kind_modorprecision_mod) so the whole codebase can change precision in one place. - Use
real(kind=dp)(or the project's approved real kind) for floating-point values — never barerealordouble precision, whose actual precision is compiler- and flag-dependent. - Use
integer(kind=i4)(or the project's approved integer kind) for integers where the width matters, especially in interfaces to C or binary I/O. - Define constants such as
piexplicitly and precisely (e.g.,real(dp), parameter :: pi = 4.0_dp * atan(1.0_dp)) rather than truncated literals. - Include the physical units in a comment for any variable representing a physical quantity (
real(dp) :: velocity ! m/s). - Use derived types to group related data (e.g., a
particle_ttype with position, velocity, and mass fields) instead of passing many loose primitive arguments through procedure calls.
Example: Kinds Module and a Numerical Procedure
module kinds_mod
use iso_fortran_env, only: real64, int32
implicit none
private
public :: dp, i4
integer, parameter :: dp = real64
integer, parameter :: i4 = int32
end module kinds_mod
module stats_mod
use kinds_mod, only: dp
implicit none
private
public :: mean, standard_deviation
contains
pure function mean(x) result(m)
real(dp), intent(in) :: x(:)
real(dp) :: m
m = sum(x) / real(size(x), dp)
end function mean
pure function standard_deviation(x) result(s)
real(dp), intent(in) :: x(:)
real(dp) :: s
real(dp) :: m
integer :: n
n = size(x)
if (n < 2) then
s = 0.0_dp
return
end if
m = mean(x)
s = sqrt(sum((x - m)**2) / real(n - 1, dp))
end function standard_deviation
end module stats_mod
program demo
use kinds_mod, only: dp
use stats_mod, only: mean, standard_deviation
implicit none
real(dp) :: samples(5)
samples = [1.0_dp, 2.0_dp, 3.0_dp, 4.0_dp, 5.0_dp]
print '(A, F0.4)', 'mean = ', mean(samples)
print '(A, F0.4)', 'stddev = ', standard_deviation(samples)
end program demo
Naming and Style
- Use lowercase for language keywords and most identifiers; Fortran is case-insensitive, so consistent lowercase avoids visual noise from mixed-case keywords.
- Use underscores for multi-word names (
particle_velocity, notparticleVelocityorParticleVelocity). - Avoid names that differ only by case (
Countvscount) since Fortran treats them as identical, inviting confusion. - Use descriptive names for procedures and state (
compute_residual, notcrorcalc2). - Repeat the procedure or module name after
endstatements (end module kinds_mod,end subroutine compute_residual) so long units are easy to verify by eye. - Keep indentation consistent (2 or 4 spaces, matching the project) inside
do,if,select case, and module blocks.
Procedures
- Keep subroutines and functions short and single-purpose — a procedure that computes a residual should not also write output files.
- Use
intent(in),intent(out), orintent(inout)for every dummy argument; an argument with nointentis a red flag that the interface hasn't been thought through. - Keep functions free of side effects whenever possible (mark them
pureorelementalwhen they qualify) and reservesubroutines for procedures that mutate state or perform I/O. - Prefer early validation and clear returns (
if (n <= 0) then; ...; return; end if) over deeply nestedifblocks. - Use
use, only: name1, name2when importing from a module instead of a bareuse module_name, so it's clear at the call site exactly what's being pulled in and name collisions are avoided.
Memory and Arrays
- Prefer allocatable arrays over pointers unless pointer semantics (aliasing, linked structures) are specifically required — allocatables are automatically deallocated and safer by default.
- Check
allocated()state and array sizes (size(),lbound(),ubound()) before using an array that might not have been allocated yet. - Deallocate allocatable arrays explicitly when their lifetime isn't naturally scoped (e.g., held in a derived type that outlives a single procedure call); arrays local to a procedure are deallocated automatically on exit.
- Specify array bounds clearly when they matter, especially non-default lower bounds (
real(dp) :: a(0:n)). - Avoid unnecessary dynamic allocation inside hot loops — allocate once outside the loop and reuse the buffer, or use automatic/stack arrays for small, fixed-size temporaries.
Testing and Build
- Use CMake, fpm (the Fortran Package Manager), Make, or whatever build system the project has standardized on — consistently, not a mix.
- Compile with warnings enabled (
gfortran -Wall -Wextra -std=f2008 -fcheck=allfor development builds) and treat important warnings as CI failures. - Add unit tests for public procedures (a pure function like
standard_deviationshould have a small, fast test with known input/output) and integration tests for full numerical workflows. - Test boundary conditions (empty arrays, single-element arrays, zero/negative inputs), invalid inputs, and representative scientific cases drawn from the actual problem domain.
- Verify numerical results against a tolerance (e.g.,
abs(actual - expected) < 1.0e-10_dp) rather than relying on exact floating-point equality, which is almost never guaranteed across compilers or optimization levels.
Common Mistakes
- Declaring variables after executable statements without wrapping them in a
blockconstruct — Fortran requires all declarations before executable code in a given scoping unit. - Assuming
random_numberis a function; it is a subroutine (call random_number(x), notx = random_number()). - Writing to stdout (
print,write(*,*)) from a procedure declaredpure— this is not allowed and will fail to compile. - Declaring the same variable twice in the same scope, which is easy to miss when a module has grown large.
- Assuming
pi,dp, or other project-standard kind/constant names already exist without importing or defining them explicitly viause.