> "The connections between modules are the assumptions which the modules make about each other."
Prerequisites
- 3
- 5
- 6
- 7
Learning Objectives
- Explain what a module is, and use `use` (with `only:`) to access a module's variables, procedures, and constants from another program unit.
- Design a module's public interface with `public` and `private`, and explain how putting a procedure in a module gives every caller an explicit interface for free.
- Separate a module's interface from its implementation with a submodule, and say why that prevents recompilation cascades and breaks circular dependencies.
- Explain why modules replaced the FORTRAN 77 `COMMON` block and `INCLUDE` line, and translate a `COMMON` block into a module.
- Determine the correct compilation order for a set of modules from their dependency graph, and describe the role of the `.mod` and `.smod` files.
- Split the growing heat solver into a clean module hierarchy — `kinds`, `heat_solver`, `heat_io` — and name the compile order.
In This Chapter
- Overview
- Learning Paths
- 8.1 use and the Module Concept
- 8.2 public, private, and Explicit Interfaces "for Free"
- 8.3 Submodules: Separating Interface from Implementation
- 8.4 Why Modules Replaced COMMON and INCLUDE
- 8.5 Compilation Order and the .mod Files
- 8.6 Designing a Module Hierarchy for a Growing Program
- Project Checkpoint
- Summary
- Spaced Review
- What's Next
Chapter 8: Modules — The Foundation of Modern Fortran Program Organization
"The connections between modules are the assumptions which the modules make about each other." — David Parnas, "On the Criteria To Be Used in Decomposing Systems into Modules" (1972)
Overview
Everything you have written so far has lived in a single file. That was the right way to learn — a program you can see all of at once is a program you can reason about all at once. But no real scientific code fits in one file, and the moment you split a computation across many files you face a question that FORTRAN 77 answered badly and modern Fortran answers beautifully: how does the code in one file safely use the variables, constants, and procedures defined in another?
The modern answer is the module, and it is the single most important organizing idea in the language.
A module is a container — for constants, for shared variables, for your own procedures — with a wall
around it and a labeled door in the wall. Code outside the module reaches in through the door with a
use statement and gets exactly what the module chose to expose, with the compiler checking every access
on the way through. That checking is not a formality. When a procedure lives in a module, every place
that calls it is verified against its real signature — right number of arguments, right types, right
ranks, right intent — at compile time, before a single instruction runs. This is the "explicit
interface for free" that makes the intent, the optional arguments, the keyword arguments, and the
assumed-shape arrays you met in Chapter 6
genuinely safe to use across files. It is also, not coincidentally, what lets the compiler optimize
across a call boundary.
This chapter opens Part II, and it is load-bearing for everything after it. From here on, essentially every piece of Fortran we write lives in a module: the derived types of Chapter 9, the numerical kernels of Part V, the parallel routines of Part VIII. So we take our time and get the foundation exactly right.
In this chapter, you will learn to:
- Write a module that exports constants, shared variables, and procedures, and pull them into another
program unit with
use ... only:. - Control precisely what a module exposes with
publicandprivate, and understand why the compiler's explicit-interface checking is a safety feature you get simply by using modules. - Split a module's interface from its implementation with a submodule, and explain the two problems submodules were invented to solve.
- Say, with specifics, why modules retired the FORTRAN 77
COMMONblock andINCLUDEline — the global mess we will meet properly in Chapter 17. - Work out the compilation order of a multi-module program from its dependencies, and know what the
.modfiles the compiler leaves behind are actually for. - Design a clean module hierarchy for a program that is going to keep growing — starting with our own heat solver, which becomes a real modular codebase in this chapter's Project Checkpoint.
Learning Paths
How to read this chapter by track. - 🔬 Scientist — read §8.1, §8.2, and §8.6 closely; they are how you will organize your own code for the rest of your career. Skim §8.3 (submodules) on a first pass and return when a library you write gets large. - 📖 Standard — read straight through. Modules are the backbone of the language; §8.3 and §8.5 contain details the other tracks can defer but you should not. - 🔧 Legacy — §8.4 is written for you: it is the bridge from the
COMMON/INCLUDEworld you have inherited to the module world you are moving toward. Pair it with Chapter 17. - ⚡ HPC — §8.2's "explicit interface for free" and its ⚡ Performance Note explain why modular code optimizes better, and §8.5 is the compile-order knowledge you need before you build anything large.
8.1 use and the Module Concept
Start with the problem a module solves. Suppose you have written a small function that computes the mean
temperature of a plate, and you want to call it from three different programs. In
Chapter 6 you learned to write it as an
internal procedure — tucked inside a contains in the program that uses it. That is perfect for a
helper used in one place, but it cannot be shared: an internal procedure belongs to its host and to
nothing else. To share code, you need a home for it that is not inside any one program. That home is a
module.
Definition (module). A module is a program unit — introduced by
module nameand closed byend module name— that packages related definitions (named constants, variables, derived-type definitions, and procedures) so that other program units can access them. A module is not a program; it has noprogramline and does nothing on its own. It is a library of definitions that other code draws on.
Here is a first real module. It gathers two functions that report on a temperature field, and it makes them available to any program that wants them:
module field_stats
use, intrinsic :: iso_fortran_env, only: dp => real64
implicit none
private
public :: field_mean, field_max ! the module's public door
contains
pure function field_mean(u) result(m)
real(dp), intent(in) :: u(:,:)
real(dp) :: m
m = sum(u) / real(size(u), dp)
end function field_mean
pure function field_max(u) result(hi)
real(dp), intent(in) :: u(:,:)
real(dp) :: hi
hi = maxval(u)
end function field_max
end module field_stats
Three structural points, because this shape recurs in every module you will ever write. First,
implicit none appears once, at the top of the module, and it governs the whole module including every
procedure inside it — you write it once and never again for the procedures the module contains. Second,
the module's own procedures live after a contains statement, exactly as internal procedures live after
contains inside a program. These are module procedures.
Definition (module procedure). A module procedure is a subroutine or function defined after the
containsstatement of a module. Because it lives in a module, it has an explicit interface automatically visible to every unit thatuses the module, and it can bepublic(callable from outside) orprivate(usable only inside the module).
Third — and this is the whole point — the procedures are of no use sitting in the module. Some other
program has to reach in and take them. That is what use does:
program report
use, intrinsic :: iso_fortran_env, only: dp => real64
use field_stats, only: field_mean, field_max
implicit none
real(dp) :: plate(2,3) = reshape([ 10.0_dp, 20.0_dp, 30.0_dp, &
40.0_dp, 50.0_dp, 60.0_dp ], [2,3])
print '(a, f6.2)', 'mean = ', field_mean(plate)
print '(a, f6.2)', 'max = ', field_max(plate)
end program report
$ gfortran -std=f2018 -Wall -O2 field_stats.f90 report.f90 -o report && ./report
mean = 35.00
max = 60.00
Definition (
use). Theusestatement, writtenuse module_name, gives the current program unit access to the public entities of a module. Writtenuse module_name, only: a, b, it imports only the named entities — the disciplined form we prefer, because it documents exactly what this file depends on and prevents surprises when the module grows. You can also rename on import to avoid a clash:use field_stats, only: peak => field_max.
Let us verify that output by hand, because we never run code to find out what it does. The array
constructor [10, 20, 30, 40, 50, 60] is reshaped into a $2\times3$ array. Fortran fills arrays in
column-major order (the lesson of Chapter 5),
so the first column gets 10, 20, the second 30, 40, the third 50, 60. The six values sum to $210$;
divided by size = 6 that is a mean of $35.00$, and the maximum is $60.00$. The f6.2 descriptor prints
each in a field six characters wide with two decimals, right-justified — hence the leading spaces.
💡 Intuition: Think of a module as a labeled drawer in a filing cabinet. Everything related lives together in the drawer; the
publiclabel on the front tells you what you may take out;use ... only:is you reaching in and removing exactly the two folders you need, leaving the rest undisturbed. The alternative — every scrap of paper loose on one enormous desk — is what FORTRAN 77 programs actually looked like, and it is the mess §8.4 describes.
Modules can hold more than procedures. A module can export named constants, which is how our project
already ships the precision kind dp:
module kinds
implicit none
private
public :: dp
integer, parameter :: dp = selected_real_kind(15, 307)
end module kinds
You wrote this in Chapter 3
and have been use-ing it ever since. It exports a single parameter — a compile-time constant — and
that is a completely legitimate, even ideal, use of a module: one authoritative definition of dp, drawn
on by every other file, impossible to get subtly wrong in one place and right in another.
A module can also hold module variables — ordinary variables, declared in the module's specification part, that persist for the life of the program and are shared by every procedure that can see them.
Definition (module variable). A module variable is a variable declared directly in a module (not inside one of its procedures). It has the
saveattribute implicitly — it exists for the entire run and retains its value between calls — and it is shared: every procedure in the module, and every unit thatuses it (if the variable ispublic), sees the same single instance. Module variables are the modern, typed, controlled replacement for the FORTRAN 77COMMONblock (§8.4).
Module variables are powerful and, like all shared mutable state, easy to misuse. We will use them
sparingly and deliberately — the very next section shows the disciplined pattern, where the shared state
is private and reachable only through public procedures.
🐍 Python Comparison: A Fortran module maps closely onto a Python module:
use kinds, only: dpis spirituallyfrom kinds import dp, andpublic/privateis the intent behind Python's leading-underscore convention. Two differences matter. Fortran's boundary is enforced by the compiler, not by convention — aprivateentity is genuinely unreachable from outside, not merely discouraged. And Fortran modules are compiled:useconsults a compiler-generated interface file (§8.5), so the cost is paid once at build time, not on every import at run time. The result is Python-like organization with C-like checking and zero run-time overhead.
8.2 public, private, and Explicit Interfaces "for Free"
A module without access control is only half a module. The real power — the thing that turns a pile of procedures into an engineered component — is deciding what the outside world may touch and what stays sealed inside. Fortran gives you two attributes for this.
Definition (
publicandprivate). Inside a module,publicentities are visible to any unit thatuses the module;privateentities are visible only within the module itself. A bareprivatestatement (on its own line, no list) sets the module's default to private — everything is hidden unless you explicitly list it aspublic. This "hide by default, expose on purpose" idiom is the recommended style, and it is what every module in this book uses.
Compare the two ways to arrange this. You can start from "everything public" and hide the few internals, or start from "everything private" and expose the few externals:
! Not recommended: public by default, hide the exceptions.
module a
implicit none
private :: helper ! must remember to hide each internal
contains
! ... everything not listed is public, including things you forgot about
end module a
! Recommended: private by default, expose the exceptions.
module b
implicit none
private ! nothing escapes unless you say so
public :: step, laplacian
contains
! ... helpers you add later stay private automatically
end module b
The second is safer for the same reason implicit none is safer than implicit typing: it fails closed.
A helper you add next month is private until you decide to publish it, so you can never accidentally
leak an internal routine into your public API and then be unable to change it because someone came to
depend on it.
Here is the pattern that replaces the COMMON block cleanly: shared state kept private, mutated only
through a public interface. This little module counts how many time steps our solver has taken:
module step_counter
implicit none
private
integer :: n_done = 0 ! PRIVATE module variable: shared, but sealed in
public :: tick, steps_done ! the only two ways to touch it
contains
subroutine tick()
n_done = n_done + 1
end subroutine tick
integer function steps_done()
steps_done = n_done
end function steps_done
end module step_counter
program count_steps
use step_counter, only: tick, steps_done
implicit none
integer :: k
do k = 1, 5
call tick()
end do
print '(a, i0)', 'steps taken = ', steps_done()
end program count_steps
$ gfortran -std=f2018 -Wall step_counter.f90 count_steps.f90 -o count && ./count
steps taken = 5
We call tick five times, so n_done climbs to 5, and steps_done() reports it. The output steps
taken = 5 needs no arithmetic to verify. But look at what the private attribute bought us: the driver
cannot write n_done = 999. The name n_done does not even exist outside the module. The only way to
change the count is to call tick, which means the module has complete control over its own state — a
property FORTRAN 77's COMMON block, where any routine could scribble on any shared variable, could
never offer.
⚠️ Common Pitfall — module variables are implicitly
save. Then_done = 0initializer runs once, when the program starts, not each time a procedure is entered. Module variables persist for the whole run; that is exactly what we want for a counter, but it is a trap if you ever expect a module variable to reset itself. (The related "accidental implicitsave" bug inside a procedure is a classic error we dissect in Chapter 13.)
The explicit interface, for free
Now the deeper payoff, and the reason this chapter is load-bearing. When a procedure lives in a module,
every caller that uses the module sees its explicit interface — the compiler knows the procedure's
full signature at the call site and checks your call against it.
Definition (explicit interface). An explicit interface is a description of a procedure's signature — its arguments' types, ranks, and intents; whether they are optional; whether it is a function or a subroutine — that is visible to the caller at compile time. A procedure in a module has an explicit interface automatically. An old-style external procedure (not in a module and not declared in an
interfaceblock) has only an implicit interface, and the compiler cannot check calls to it at all.
This is not a nicety; it is the difference between a bug caught in one second at compile time and a bug
that corrupts memory silently at run time. Suppose step expects step(field, alpha, dt) with field
a rank-2 array. Call it wrongly and, because it is in a module, the compiler stops you:
call step(field, alpha) ! forgot dt
$ gfortran -std=f2018 -Wall heat_solver.f90 heat.f90 -o heat
heat.f90:20:12:
20 | call step(field, alpha)
| 1
Error: Missing actual argument for argument 'dt' at (1)
Had step been an external procedure with no interface, that same call would have compiled without
complaint and produced garbage — reading whatever happened to be on the stack where dt should have
been. The module closed that entire class of bug. And there is more: the explicit interface is what
makes all of Chapter 6's conveniences
actually work across files. Optional arguments, keyword arguments (call step(field, dt=0.5_dp,
alpha=0.1_dp)), and assumed-shape array arguments (field(:,:)) require an explicit interface. Put
your procedures in modules and you get all of them; leave them as bare external procedures and you get
none of them.
⚡ Performance Note: The explicit interface a module provides is also an optimization enabler. When the compiler knows a called procedure's exact signature — and, within a module or with link-time optimization, can see its body — it can inline the call, propagate constants across it, and vectorize loops that span it. An external procedure behind an implicit interface is an opaque wall the optimizer cannot see through. This is a quiet reason modular Fortran tends to be faster as well as safer, and it connects directly to the no-aliasing and
pure/elementaloptimization story of Chapter 27.🔄 Check Your Understanding. 1. Why does
private(default-hide) lead to safer modules thanprivate :: helper(hide-the-listed)? 2. You call a subroutine with a keyword argument,call solve(tol=1e-6, n=100). What must be true about wheresolveis defined for this to compile? 3. Aprivatemodule variable can still be changed from outside the module — true or false?
Answers
(1) New helpers you add later stay hidden automatically; you can only leak an internal by explicitly publishing it, never by forgetting to hide it. (2)solvemust have an explicit interface — i.e., be a module procedure (in a module youuse) or have aninterfaceblock — because keyword arguments require the caller to know the dummy-argument names. (3) False: aprivateentity's name is invisible outside the module, so it can only be changed through the module's own public procedures.
8.3 Submodules: Separating Interface from Implementation
Modules solve the sharing problem, but at scale they create a smaller one of their own. When many files
use a big module, they all depend on that module's .mod file (§8.5). Change anything in the
module — even the body of one private helper that no caller can see — and the build system, to be safe,
recompiles every file that used it. For a foundational module in a large code, that is a recompilation
cascade: touch one line, rebuild half the program. And there is a second, sharper problem: two modules
whose implementations genuinely need each other cannot both use each other, because Fortran forbids
circular module dependencies.
Fortran 2008 introduced the submodule to solve both problems at once, by letting you put a procedure's interface in the module and its body somewhere else.
Definition (submodule). A submodule is a program unit, introduced by
submodule (parent) name, that provides the implementations of separate module procedures whose interfaces are declared in its parent module (or an ancestor submodule). The parent module declares each such procedure with amodule procedureinterface body — signature only, no body — and the submodule supplies the body. A submodule is not something youuse; it exists solely to hold implementations.
Here is the pattern on a small, self-contained example: a module describing our plate's geometry, with the interfaces up front and the code moved into a submodule.
! plate_geom.f90 — the module: interfaces only (the "header").
module plate_geom
use, intrinsic :: iso_fortran_env, only: dp => real64
implicit none
private
public :: cell_count, plate_area
interface
module function cell_count(nx, ny) result(n)
integer, intent(in) :: nx, ny
integer :: n
end function cell_count
module function plate_area(nx, ny, dx, dy) result(area)
integer, intent(in) :: nx, ny
real(dp), intent(in) :: dx, dy
real(dp) :: area
end function plate_area
end interface
end module plate_geom
! plate_geom_impl.f90 — the submodule: the bodies (the "source").
submodule (plate_geom) plate_geom_impl
implicit none
contains
module function cell_count(nx, ny) result(n)
integer, intent(in) :: nx, ny
integer :: n
n = nx * ny
end function cell_count
module function plate_area(nx, ny, dx, dy) result(area)
integer, intent(in) :: nx, ny
real(dp), intent(in) :: dx, dy
real(dp) :: area
area = real(nx - 1, dp) * dx * real(ny - 1, dp) * dy
end function plate_area
end submodule plate_geom_impl
! area_demo.f90 — the driver uses the MODULE; it never mentions the submodule.
program area_demo
use, intrinsic :: iso_fortran_env, only: dp => real64
use plate_geom, only: cell_count, plate_area
implicit none
print '(a, i0)', 'cells = ', cell_count(101, 101)
print '(a, f6.2)', 'area (m^2) = ', plate_area(101, 101, 0.01_dp, 0.01_dp)
end program area_demo
$ gfortran -std=f2018 -Wall plate_geom.f90 plate_geom_impl.f90 area_demo.f90 -o area && ./area
cells = 10201
area (m^2) = 1.00
Hand-check the numbers. A $101\times101$ grid has $101 \times 101 = 10201$ cells. The plate it spans is
$100$ intervals wide at $0.01$ m each, so $1.0$ m on a side, and its area is $1.0 \times 1.0 = 1.00$
square metres. Notice three things about the mechanics. The interface bodies in the module carry the
module prefix (module function ...) — that prefix is what marks them as separate module procedures
whose bodies live elsewhere. The submodule repeats each signature, again with the module prefix, and
fills in the body. And the submodule uses dp without any use statement of its own: a submodule has
access to everything in its parent by host association, exactly as an internal procedure sees its
host's variables.
📜 From History: Before submodules (so, in Fortran 90 through 2008-minus-submodules practice), library authors faced an unhappy trade-off. Put everything in one module and every implementation tweak triggered a rebuild of all clients; split into many tiny modules and the dependency graph became a thicket. Submodules, added in Fortran 2008 and polished in 2018, gave the C-and-header world's clean separation — declaration in one place, definition in another — without C's preprocessor fragility. The parent module is the header; the submodule is the
.cfile; and both are real, checked Fortran.⚡ Performance Note (the real win is build time, not run time). Submodules change nothing about how fast the program runs — the generated code is identical. What they change is how fast it builds. Because a client depends only on the parent module's interface, editing a submodule's body recompiles the submodule and relinks, but does not invalidate the parent's
.modand so does not force a recompile of the dozens or hundreds of files thatusethe parent. On a million-line code, that can be the difference between a ten-second edit-build cycle and a ten-minute one.
There is a shorter way to write the submodule body. Instead of restating the full signature, you can
write module procedure name and let the interface supply the argument declarations:
submodule (plate_geom) plate_geom_impl
implicit none
contains
module procedure cell_count ! args/types inherited from the interface
n = nx * ny
end procedure cell_count
module procedure plate_area
area = real(nx - 1, dp) * dx * real(ny - 1, dp) * dy
end procedure plate_area
end submodule plate_geom_impl
Both forms compile identically and are equally standard; the explicit-signature form is a little more
readable for someone opening the submodule cold, and the module procedure shorthand is a little
DRY-er. Use whichever your team prefers, consistently.
🧩 Try It Yourself. Take the
plate_geom/plate_geom_implpair above, compile it, and confirm the output. Now change the body ofplate_areain the submodule — say, print a debug line inside it — and recompile onlyplate_geom_impl.f90and relink, leavingplate_geom.oandarea_demo.ountouched. It works: the driver never needed rebuilding, because the interface it depends on did not change. That is the submodule promise, made concrete on your own machine.
Submodules earn their keep in large libraries and are optional in small programs — our heat solver is still small enough that plain modules are the right call, so this chapter's Project Checkpoint uses plain modules, not submodules. But you should recognize the pattern on sight, because the scientific codes you will read in Chapter 36 lean on it heavily.
8.4 Why Modules Replaced COMMON and INCLUDE
To understand why modules are celebrated, you have to know the pain they ended. Before Fortran 90, two constructs did the work that modules do now, and both were treacherous. We will meet them properly in Chapter 17, when you learn to read real FORTRAN 77; here we need only enough to appreciate the modern replacement.
The COMMON block was FORTRAN 77's mechanism for sharing variables between program units. It worked
by overlaying memory: several routines each declared a COMMON block of the same name, and the variables
in it were understood to occupy the same storage. Nothing checked that the declarations agreed.
! Legacy FORTRAN 77 — shared state as a raw memory overlay.
SUBROUTINE STEP
COMMON /STATE/ X, Y, N
REAL X, Y
INTEGER N
...
END
SUBROUTINE OUTPUT
COMMON /STATE/ A, B, M ! same memory, DIFFERENT names — and no one checks
REAL A, B
INTEGER M
...
END
Both routines refer to the same three words of memory, but under different names, and — this is the
horror of it — nothing prevents one routine from declaring the block as REAL X, Y, Z (three reals)
while another declares it REAL X, Y plus INTEGER N. The types need not match. The lengths need not
match. A mismatch does not produce an error; it produces silently wrong numbers, because the bits in
that shared memory get reinterpreted according to whatever each routine happened to declare. Add
INCLUDE 'state.inc' — a raw textual paste of a file into your source — to keep those declarations "in
sync" across dozens of files, and you have FORTRAN 77's actual, historical mechanism for building large
programs: global memory, untyped, uncheckable, held together by copied-and-pasted text.
🔧 Modern vs Legacy: The module is the cure, and the contrast is stark. The same shared state, done the modern way:
fortran ! Modern: a module — one typed, checked, authoritative definition. module state use kinds, only: dp implicit none real(dp) :: x, y integer :: n end module stateThere is one declaration of
x,y, andn, written once. Every unit thatusesstatesees that same declaration, with those exact types, checked by the compiler. No overlay, no reinterpretation of bits, no textual paste to keep synchronized. If a routine misusesnas a real, the compiler says so. TheINCLUDEline vanishes entirely, becauseusedoes its job properly — importing named, typed entities instead of pasting text.
This is the payoff worth stopping on, because it changes how you see every large program you will ever open.
🚪 Threshold Concept — modules replace the global mess of
COMMON. FORTRAN 77's model of a large program was one global pool of untyped memory that every routine could reach into and modify, with no compiler able to check that any two routines agreed about what was in it. Modern Fortran's model is a set of components, each owning its own state, each exposing a checked interface, each reasoned about in isolation. This is not a cosmetic upgrade — it is the difference between a program you can understand one piece at a time and a program where any line might, in principle, have changed any variable anywhere. Once you internalize that a module is a wall and not just a folder, the entire practice of building large, correct scientific software falls into place. Every theme in this book about modern Fortran being genuinely modern rests on this one idea.
Two more advantages of modules over COMMON are worth naming. Modules can export procedures and
derived types, not just variables — a COMMON block could only share data, never code. And modules
give you selective, renamed access through use ... only:, so a file declares precisely what it
depends on, whereas COMMON forced every routine sharing a block to swallow the whole block. The result
is that the theme of this part holds without exception: modern Fortran is a modern language, and
modules are the first and clearest proof of it. This is also why, when you inherit a COMMON-riddled
legacy code, converting COMMON blocks to modules is step three of the eight-step modernization recipe
in Chapter 18 — and one of
the most valuable, because it is where the code stops being able to corrupt itself.
8.5 Compilation Order and the .mod Files
Modules introduce a build-order rule that trips up almost everyone once. Because use module_name needs
to know the module's interface while compiling the file that uses it, the module must be compiled
before any file that uses it. This is not a linker matter; it is a compile-time matter, and getting
it wrong produces an error that is baffling until you understand what the compiler is looking for.
When gfortran compiles a file containing a module, it emits two things: the object file (foo.o,
holding machine code as usual) and a module interface file.
Definition (
.modfile). A.modfile is a compiler-generated file —kinds.modfor a module namedkinds— that records a module's public interface: the names, types, and signatures of everything it exports. It is not the compiled code (that stays in the.o); it is the information a user of the module needs at compile time to checkusestatements and calls. When you compile a file that saysuse kinds, the compiler readskinds.mod. It follows thatkinds.modmust already exist — which meanskinds.f90must already have been compiled.
So a program built from several modules has a definite compile order, dictated by its dependency graph: compile the modules that depend on nothing first, then the modules that depend on those, and the main program last. If you hand gfortran all the sources in one command, it compiles them left to right, so their order on the command line matters:
$ # Correct: dependencies first, users after.
$ gfortran -std=f2018 -Wall kinds.f90 heat_solver.f90 heat_io.f90 heat.f90 -o heat
Get the order wrong and you see the signature error of a missing .mod:
$ # Wrong: heat.f90 uses kinds, but kinds.f90 hasn't been compiled yet.
$ gfortran -std=f2018 -Wall heat.f90 kinds.f90 heat_solver.f90 heat_io.f90 -o heat
heat.f90:2:6:
2 | use kinds, only: dp
| 1
Fatal Error: Cannot open module file 'kinds.mod' for reading at (1): No such file or directory
⚠️ Common Pitfall — "Cannot open module file." This is the error every Fortran newcomer eventually hits, and it almost never means what it seems to. It does not mean the module is broken; it means the module has not been compiled yet (or its
.modis in a directory the compiler was not told to look in, via-I). The fix is compile order, not code. When you compile files separately, build the dependency's.modfirst:
console $ gfortran -std=f2018 -Wall -c kinds.f90 # produces kinds.o AND kinds.mod $ gfortran -std=f2018 -Wall -c heat_solver.f90 # reads kinds.mod, produces heat_solver.{o,mod} $ gfortran -std=f2018 -Wall -c heat_io.f90 # reads kinds.mod $ gfortran -std=f2018 -Wall -c heat.f90 # reads all three .mod files $ gfortran kinds.o heat_solver.o heat_io.o heat.o -o heat # link the objects
Two consequences follow, one about correctness and one about tooling. First, circular dependencies are
impossible: if module a uses b and b uses a, neither can be compiled first, and the compiler
rejects it. That is a feature — genuine circular data dependencies signal a design that needs
rethinking — and on the rare occasion two components' implementations really must know each other, the
submodule of §8.3 is the standard escape hatch, since a submodule can use anything without creating a
cycle in the module graph.
Second, you should not be tracking this order by hand for long. Working out and re-deriving the compile
order every time a file changes is exactly the drudgery that build tools exist to eliminate. make
encodes the dependencies once; better still, the Fortran Package Manager (fpm), which we set up in
Chapter 16, scans
your use statements automatically and compiles everything in the right order with no configuration at
all. Understand the order by hand now — it is a real part of how Fortran works, and you will debug it —
but plan to delegate it to a tool the moment your project outgrows a single command line.
Definition (compilation order). The compilation order of a multi-module program is a topological ordering of its module-dependency graph: each module is compiled after every module it
uses. Equivalently, compile the leaves of the dependency tree (modules that use nothing) first and the root (the main program) last. It is a property of the dependencies, not of the file names.
For submodules there is one extra file. A module that declares separate module procedures (§8.3) also
emits a .smod file — plate_geom.smod — recording the interfaces its submodules must implement. The
submodule is compiled against that .smod, so the compile order is: parent module, then its submodule,
then the users of the parent. gfortran creates the .smod automatically alongside the .mod; you rarely
name it, but when a build complains it cannot find a .smod, the cause is the same as for a missing
.mod — something was compiled out of order.
🐛 Find the Bug. A student splits a working program into two modules and gets a compile error. What is wrong, and what are two ways to fix it?
```fortran module physics use io, only: log_message ! physics needs io's logger implicit none contains subroutine step(); call log_message('stepped'); end subroutine end module physics
module io use physics, only: step ! io needs physics's step implicit none contains subroutine log_message(msg); character(), intent(in) :: msg; print , msg; end subroutine end module io ```
Answer
It is a circular dependency:physicsusesioandiousesphysics, so neither can be compiled first, and gfortran rejects it (you will see "Cannot open module file" for whichever it tries second, or a circular-dependency diagnostic). Two fixes. (1) Refactor the cycle away: the mutual need usually means a role is misplaced — here, movelog_messageinto a third, lower-levelloggingmodule that bothphysicsandiouse, so the graph becomes a tree again. (2) Use a submodule: putio's interfaces inioand its body (which callsstep) in asubmodule (io), which mayuse physicsfreely without forming a cycle in the module graph. Fix (1) is almost always the better design; reach for (2) only when the coupling is genuinely irreducible.
8.6 Designing a Module Hierarchy for a Growing Program
You now have every mechanism you need; the remaining skill is judgment — how to carve a growing program into modules that will still make sense when it is ten times larger. The guiding principle is the one in this chapter's epigraph: a good module boundary is one that minimizes the assumptions modules make about each other. Each module should own one clear responsibility and expose the smallest interface that lets others do their job.
A few rules of thumb, learned from real scientific codes and made precise in Chapter 36:
- One responsibility per module. A module for the numerics, a module for the I/O, a module for the shared kinds and constants. When you can name a module's job in a short phrase, it is probably well scoped; when you cannot, it is probably doing too much.
- Depend downward, never sideways or up. Arrange modules in layers — foundational definitions at the
bottom, the driver at the top — and let each layer
useonly the layers below it. A dependency graph that is a tree (or at least acyclic and shallow) is one you can compile, test, and understand a piece at a time. Sideways dependencies between peers are the first sign of a boundary in the wrong place. - Expose data, hide representation. Publish the procedures that operate on your data and, where you can, keep the raw variables private (§8.2). This is the same instinct that will lead, in Chapter 9, to bundling the field into a derived type with its operations attached.
- Keep public interfaces stable. The whole point of §8.3 is that you can rework a module's insides freely as long as its interface holds still. Design the interface with care, because changing it is what forces your users to change too.
Our heat solver is the perfect size to practice on. Right now it is one file doing three unrelated jobs:
defining precision, stepping the physics, and reading and writing data. Those are three responsibilities,
so they become three modules on top of the kinds foundation, with the heat driver at the apex:
program heat ← the driver: setup, time loop, output
/ | \
/ | \ (each arrow: "uses")
heat_solver heat_io \
step read_config \
write_field \
\ | /
\ | /
kinds ← dp: the shared numerical foundation
Read the arrows as "depends on." heat_solver and heat_io are peers — neither uses the other — and
both rest on kinds; the heat driver sits above all three. The graph is a tree, so the compile order
falls right out of it: kinds first (it depends on nothing), then heat_solver and heat_io in
either order (each depends only on kinds), then heat last (it depends on all three). That is
exactly the order the Project Checkpoint compiles them in, and it is the order fpm would derive on its
own.
🔗 Connection. This layered, one-job-per-module shape is not a toy convention; it is how the production codes of Part IX are actually built — a
kinds/constantslayer at the bottom, physics and solver modules in the middle, I/O and driver at the top, often with the biggest modules split into submodules for build speed. When you open a 100,000-line Fortran code in Chapter 36 and it does not immediately overwhelm you, this section is the reason: you will recognize the hierarchy.🔄 Check Your Understanding. 1. In the heat-solver hierarchy above, which file must be compiled first, and why? 2. Your solver grows a
timersmodule thatheat_solverneeds for benchmarking. Where does it go in the layer diagram, and doesheat_iohave to change?
Answers
(1)kinds, because ituses nothing, so its.modmust exist before any other file (all of which use it) can be compiled. (2)timersgoes in the lower layers (it likely uses onlykinds), andheat_solveraddsuse timers;heat_iois untouched, because module boundaries mean a change confined to the solver's needs does not ripple sideways to the I/O.
Project Checkpoint
Your heat solver has been one file since Chapter 2.
This is the chapter where it becomes software: we split it into the three-module hierarchy of §8.6 —
kinds, heat_solver, and heat_io — with the heat program as the driver. Nothing about the physics
changes; we are re-housing code you already wrote into modules that will carry it the rest of the way to
the capstone.
The pieces map cleanly onto the modules. kinds is exactly the module from
Chapter 3. heat_solver holds
the step subroutine you wrote in
Chapter 6, made public while any future
helpers stay private. heat_io holds the namelist read and the field write from
Chapter 7, as read_config and write_field.
Here is the solver module and the driver (the full four-file program, with kinds and heat_io, is in
code/project-checkpoint.f90):
module heat_solver
use kinds, only: dp
implicit none
private
public :: step ! the one public entry point
contains
subroutine step(field, alpha, dt)
real(dp), intent(inout) :: field(:,:) ! assumed-shape: any plate size
real(dp), intent(in) :: alpha, dt
real(dp), allocatable :: old(:,:)
real(dp) :: lap
integer :: i, j, nx, ny
nx = size(field, 1); ny = size(field, 2)
allocate(old(nx, ny)); old = field
do j = 2, ny - 1
do i = 2, nx - 1
lap = old(i-1,j) + old(i+1,j) + old(i,j-1) + old(i,j+1) - 4.0_dp*old(i,j)
field(i,j) = old(i,j) + alpha*dt*lap
end do
end do
end subroutine step
end module heat_solver
The driver uses all three modules, sets up a $4\times4$ plate with a hot top edge, takes one step, and
writes the result:
program heat
use kinds, only: dp
use heat_solver, only: step
use heat_io, only: write_field
implicit none
real(dp), allocatable :: field(:,:)
real(dp), parameter :: alpha = 0.1_dp, dt = 1.0_dp
integer :: i
allocate(field(4,4)); field = 0.0_dp; field(1,:) = 100.0_dp
call step(field, alpha, dt)
print '(a)', 'field after one step (row by row):'
do i = 1, size(field,1); print '(4f8.2)', field(i,:); end do
call write_field(field, 'heat_0001.txt')
print '(a)', 'wrote heat_0001.txt'
end program heat
Compile the four files in dependency order — kinds first, heat last — exactly as §8.5 requires:
$ gfortran -std=f2018 -Wall kinds.f90 heat_solver.f90 heat_io.f90 heat.f90 -o heat && ./heat
field after one step (row by row):
100.00 100.00 100.00 100.00
0.00 10.00 10.00 0.00
0.00 0.00 0.00 0.00
0.00 0.00 0.00 0.00
wrote heat_0001.txt
The numbers are the same ones you hand-computed for the Chapter 6 step: only the four interior cells can
change, and with alpha*dt = 0.1, the two interior cells adjacent to the $100$-degree edge become
$0 + 0.1\times100 = 10.00$ while the two farther cells stay $0.00$. What changed is not the arithmetic but
the architecture: the physics now lives behind heat_solver's public step, the I/O behind heat_io,
and the precision behind kinds, each independently compilable, testable, and replaceable. When
Chapter 24 swaps the
placeholder update for the real five-point stencil, only heat_solver will change — the driver and the
I/O will not even be recompiled if you have split the solver's implementation into a submodule. This is
the payoff of the whole chapter, standing in your own project: the solver is now a modular codebase,
and every later part builds on this skeleton.
🔗 Connection. The public signature
step(field, alpha, dt)is frozen — Part VIII will provide OpenMP, coarray, and MPI versions ofstepbehind this same interface, so the driver never has to know whether it is running on one core or a thousand. That is only possible because the interface is nailed down in a module now.
Summary
This chapter introduced the module — the container that makes large Fortran programs possible — and used it to turn our heat solver into real software.
| Concept | What it is | Key syntax |
|---|---|---|
| module | A container for shared constants, variables, types, and procedures | module m … end module m |
use |
Import a module's public entities into a program unit | use m, only: a, b; rename with x => a |
| module procedure | A subroutine/function defined after a module's contains |
gets an explicit interface for free |
public / private |
Access control on module entities | private (default-hide) + public :: api |
| module variable | A variable declared in a module; shared, implicitly save |
keep it private, mutate via procedures |
| explicit interface | The compiler-visible signature a module procedure carries | enables argument checking, keyword/optional/assumed-shape args |
| submodule | Holds the bodies of a module's separate module procedures | submodule (parent) child; module procedure |
.mod file |
Compiler-generated interface file, read by users |
must exist before a using file compiles |
The rules worth memorizing:
- Private by default, public on purpose. Start every module with a bare
privateand list thepublicAPI. It fails closed. - Compile dependencies first. A module's
.modmust exist before any file thatuses it is compiled; the compile order is a topological sort of the dependency graph (leaves first,programlast). "Cannot open module file" means out of order, not broken code. - Interface in the module, implementation in the submodule when a library is large enough that recompilation cascades or circular dependencies bite — otherwise plain modules are fine.
- Modules retired
COMMONandINCLUDE: one typed, checked, authoritative definition instead of a global untyped memory overlay held together by pasted text.
Compile flags introduced this chapter: -c (compile to an object file without linking) and -I<dir>
(tell the compiler where to find .mod files) — both are about managing the module build.
Spaced Review
Retrieval practice on the two chapters this one builds on directly — procedures (Chapter 6) and I/O (Chapter 7).
-
In Chapter 6 you gave every dummy argument an
intent. Which explicit-interface feature of this chapter is what makesintent— and optional, keyword, and assumed-shape arguments — actually work when the caller is in a different file?
Answer
The explicit interface a module procedure carries (§8.2). Those Chapter 6 features all *require* an explicit interface at the call site; putting the procedure in a module and `use`-ing it supplies one automatically, so the compiler can check the call and match arguments by keyword. -
The heat solver's
steptakesfield(:,:)as an assumed-shape array (Chapter 6). Why canstepsit inheat_solverand still accept a plate of any size, and what would break ifstepwere an external procedure with no interface?
Answer
Assumed-shape arrays pass their bounds through the explicit interface, so `step` learns `nx` and `ny` from the actual argument via `size`. As a bare external procedure with no interface, the assumed-shape dummy would have no way to receive the shape — the call would not conform, and the compiler could not check it. -
From Chapter 7: our
heat_iomodule reads run parameters from anamelist. In one sentence, what is a namelist, and what is one advantage of it over reading bare numbers in a fixed order?
Answer
A `namelist` is a named group of variables read/written as `name=value` pairs (`&config nx=64 dt=0.001 /`); its advantage is that the input is self-labeling and order-independent, so you can add, omit, or reorder parameters without breaking the read — far more robust than positional list-directed input. -
From Chapter 7:
write_fieldopens a file withopen(newunit=u, ...). Why isnewunit=safer than picking a unit number likeopen(unit=10, ...)yourself?
Answer
`newunit=` asks the runtime for a guaranteed-unused unit number (returned negative), so two parts of a growing program can never collide by both hard-coding the same unit — exactly the kind of accidental global clash modules exist to prevent.
What's Next
You can now organize code into modules, control what they expose, and compile them in the right order —
the skeleton every later chapter hangs on. But our field is still a bare real(dp) :: field(:,:) plus a
loose scatter of nx, ny, dx, dy, alpha, and dt, passed around as separate arguments. That is
exactly the kind of related data that wants to be bundled into one named thing.
Chapter 9 introduces derived types — your own data
structures — and you will collect the whole field into a single field_t that travels as a unit, carries
its own dimensions, and (in Chapter 10) grows its own procedures. The module you built here is where that
type will live. On to derived types.