Fortran Modules

Modules are Fortran's answer to three problems at once: sharing constants and types, grouping procedures into libraries, and giving the compiler a checked interface for every call. A use statement replaces entire heaps of hand-written interfaces. This lesson covers the module lifecycle, visibility control, and the compile-order discipline that makes multi-file projects reliable.

Why modules

Procedure arguments have types, and those types must be known at both call sites. Modules make the compiler know them: when a program uses a module, the compiler reads the module's interface file and checks every call for argument count, type, kind and intent — the same safety net other languages get from headers or signatures, but generated automatically and guaranteed in sync.

Module dependency tree and USE relationships

Fig. 1 — Consumers compile after providers; each USE edge is verified by the compiler.

The module skeleton

A module has three zones in order: use statements importing its dependencies, declarations of public data (parameters, types), and a contains block of procedures. Everything not explicitly private is public by default — which is a leak; the recommended pattern declares the module private and re-exports only the intended API with public :::

module geometry
  implicit none
  private                          ! nothing leaks by default
  public :: circle_area, g
                

Compile order and .mod files

Compiling a module produces the object file plus a module file (*.mod on most compilers, e.g. geometry.mod) containing the interface. Every unit that uses it must find that file at compile time — hence the classic two-pass recipe and its flags:

gfortran -c -J build/ geometry.f90     # first: produce .mod into build/
gfortran -c -I build/ app.f90           # second: consume the .mod
gfortran -o app build/geometry.o app.o  # link everything

-J names the directory where this compilation writes module files; -I lists directories where it looks for imported ones. fpm and CMake generate this ordering automatically from the dependency graph — one of the strongest arguments for adopting fpm early (the ecosystem lesson installs it properly).

Renaming and ONLY

use module, only: local = imported renames on import, which resolves the two classic collisions: two modules exporting the same name, and a module name clashing with a local variable. only: also documents intent — every reader of the use line sees exactly which symbols the unit needs. Prefer it over bare use module, which drags the entire public surface into scope.

module constants
  real, parameter :: pi = 3.14159265358979
  real, parameter :: e  = 2.71828182845905
end module constants

module math_utils
  use constants, only: pi      ! math_utils depends on constants
  implicit none
contains
  pure real function torus_volume(R, r)
    real, intent(in) :: R, r
    torus_volume = 2.0 * pi**2 * R * r * r
  end function torus_volume
end module math_utils

program ring
  use math_utils, only: torus_volume
  use constants, only: pi => pi_val   ! renamed to dodge the name 'pi'
  implicit none
  print '(a,f8.3)', trim('torus: '), torus_volume(3.0, 1.0)
  print '(a,f8.5)', 'pi as pi_val = ', pi_val
end program ring

Generic interfaces and operators

A generic interface lets one call name dispatch to several specific procedures by argument type — the foundation of Fortran's compile-time polymorphism. Combined with interface operator(+) it also teaches the compiler how to add your derived types:

module vector2d
  implicit none
  private
  public :: vector, magnitude, operator(+)
  type :: vector
    real :: x, y
  end type vector
  interface magnitude
    module procedure magnitude_v2        ! real vectors
    module procedure magnitude_v3        ! integer vectors
  end interface magnitude
contains
  pure real function magnitude_v2(v)
    type(vector), intent(in) :: v
    magnitude_v2 = sqrt(v%x**2 + v%y**2)
  end function magnitude_v2
  pure real function magnitude_v3(v)
    integer, intent(in) :: v(3)
    magnitude_v3 = sqrt(real(v(1)**2 + v(2)**2 + v(3)**2))
  end function magnitude_v3
  type(vector) function operator_plus(a, b)
    type(vector), intent(in) :: a, b
    operator_plus = vector(a%x + b%x, a%y + b%y)
  end function operator_plus
end module vector2d

program generic_call
  use vector2d
  implicit none
  type(vector) :: p, q
  p = vector(1.0, 2.0); q = vector(3.0, 4.0)
  print *, magnitude(p)           ! dispatches to magnitude_v2
  print *, magnitude([1, 2, 3])   ! dispatches to magnitude_v3
  print *, p + q                  ! our own operator(+)
end program generic_call

Submodules: compile-time decoupling

Large modules recompile fully when any detail changes. Submodules split declaration from implementation: the parent module declares an interface, the submodule provides the body. Consumers depend only on the parent, so editing a submodule body never recompiles the whole project. Every serious codebase above a few tens of procedures should keep this tool in mind, though fpm's incremental builds already soften the pain.

Module rules for this track: one module per file, file named after the module; private by default with explicit public ::; only on every use; contains for every procedure — and let fpm own the build order.
real, parameter :: g = 9.80665 ! private constant, invisible outside contains pure real function circle_area(r) real, intent(in) :: r circle_area = 3.14159265358979 * r * r end function circle_area end module geometry program use_geometry use geometry, only: circle_area ! import exactly one symbol implicit none print '(a,f8.3)', "area = ", circle_area(2.0) end program use_geometry