Exercises: Modules
Modules are a skill you build by restructuring code, not just reading about it. These exercises make you
write modules, control their interfaces, split them with submodules, translate COMMON blocks, reason
about compile order, and design a hierarchy for the growing heat solver. Do them at a terminal — the
"Cannot open module file" error in particular is one you should provoke on purpose, so it never confuses
you in anger.
Difficulty: ⭐ warm-up · ⭐⭐ standard · ⭐⭐⭐ deeper. Solutions: worked solutions to the daggered (†)
and odd-numbered problems are in appendices/answers-to-selected.md; the compilable ones are in
code/exercise-solutions.f90. Every program compiles with gfortran -std=f2018 -Wall. Try each problem
before you look.
Part A — Warm-ups ⭐
8.1 † Write a module constants that exports two real(dp) parameters — pi = 3.14159265358979_dp
and two_pi = 2*pi — taking dp from a kinds module. Write a short program that uses both modules
and prints pi and two_pi. Give the compile command in the correct order.
8.2 In one or two sentences each, distinguish a module procedure from an internal procedure
(the contains-inside-a-program kind from Chapter 6).
When would you reach for each?
8.3 † Explain exactly what use kinds, only: dp does, and give two concrete reasons the only:
clause is better practice than a bare use kinds.
8.4 A module m exports alpha, beta, and gamma. Write a single use statement that imports
only beta, and renames it to b in the importing unit.
8.5 † What does a bare private statement — on its own line, with no list — do to a module, and why
is "private by default, public on purpose" the recommended idiom rather than the reverse?
Part B — Access Control and Explicit Interfaces ⭐⭐
8.6 † (Find the bug.) A module solver has a public :: step and, after contains, a helper
subroutine jacobi(...) that is private by default. A program writes use solver and then
call jacobi(field). Does this fail at compile time or link time, and what is the message? Explain
why (think about what implicit none does and does not constrain), and give two different fixes (one
that keeps jacobi private, one that does not).
8.7 Explain the phrase "explicit interface for free." Then name three features from
Chapter 6 that require an explicit interface
and therefore only work when the called procedure is in a module (or declared in an interface block).
8.8 † (Type, compile, and run — predict first.) Read this program and write down its exact output before compiling:
module accumulator
use, intrinsic :: iso_fortran_env, only: dp => real64
implicit none
private
real(dp) :: total = 0.0_dp
public :: add, total_so_far
contains
subroutine add(x)
real(dp), intent(in) :: x
total = total + x
end subroutine add
real(dp) function total_so_far()
total_so_far = total
end function total_so_far
end module accumulator
program run
use, intrinsic :: iso_fortran_env, only: dp => real64
use accumulator, only: add, total_so_far
implicit none
call add(1.5_dp); call add(2.5_dp); call add(3.0_dp)
print '(a, f6.2)', 'total = ', total_so_far()
end program run
8.9 Why is keeping a module variable private and changing it only through public procedures better
than exposing it as a public variable? Frame your answer in terms of what the compiler can and cannot
guarantee.
Part C — Submodules ⭐⭐
8.10 † (Design it.) Extend the plate_geom module from §8.3 with a third function,
perimeter(nx, ny, dx, dy), returning the plate's perimeter in metres. Declare its interface in the
module and put its body in the submodule. (The perimeter of an $(nx{-}1)\,dx$ by $(ny{-}1)\,dy$ rectangle
is $2[(nx{-}1)dx + (ny{-}1)dy]$.)
8.11 State, in your own words, the two distinct problems that submodules were introduced to solve. For each, explain in one sentence how the submodule solves it.
8.12 † True or false, with justification: "Editing the body of a procedure inside a submodule forces
every file that uses the parent module to be recompiled." What is recompiled, and what is not?
8.13 Rewrite this submodule body from the explicit module function … end function form into the
shorter module procedure … end procedure form:
submodule (plate_geom) impl
contains
module function cell_count(nx, ny) result(n)
integer, intent(in) :: nx, ny
integer :: n
n = nx * ny
end function cell_count
end submodule impl
Part D — Modernize It (COMMON → module) ⭐⭐
Part IV covers legacy Fortran in full; these are a first taste of the single most valuable modernization
move — converting shared global state from a COMMON block to a module.
8.14 † (Modernize it.) Two FORTRAN 77 subroutines share a grid description through a COMMON block:
SUBROUTINE SETUP
COMMON /GRID/ NX, NY, DX, DY
INTEGER NX, NY
DOUBLE PRECISION DX, DY
...
END
Write the modern Fortran module grid that replaces /GRID/, using dp from kinds. Then write the
first two lines of a modern setup subroutine that gains access to nx, ny, dx, dy — without a COMMON
block in sight.
8.15 The legacy program kept its COMMON /GRID/ declaration identical across twelve source files by
pasting it in with INCLUDE 'grid.inc'. Which single modern construct removes the need for that
INCLUDE, and why is it safer than a textual paste?
8.16 † List three capabilities a module has that a COMMON block does not.
Part E — Compile Order and .mod Files ⭐⭐
8.17 † A program is built from four files with these dependencies: util uses nothing; grid uses
util; physics uses util and grid; main uses grid and physics. Write a single-line gfortran
command that compiles and links them in a valid order, and explain why your order works.
8.18 You run a build and get Fatal Error: Cannot open module file 'util.mod' for reading … No such
file or directory, but util.f90 compiles fine on its own. What is actually wrong, and what are two ways
to fix it?
8.19 † What information is stored in a .mod file, and what is not? In particular, where does the
compiled machine code of a module's procedures live, and why must you still link that file even though the
.mod exists?
8.20 Explain why a circular module dependency (a uses b, b uses a) cannot compile, why that is
arguably a good thing, and one standard way to break a cycle when two components genuinely must
cooperate.
Part F — Design It: the Solver Hierarchy ⭐⭐⭐
8.21 † Your solver grows a timers module (wrapping system_clock) that heat_solver will call to
benchmark the update. (a) Where does timers belong in the layer diagram of §8.6 — above or below
heat_solver? (b) Which existing modules, if any, must change when heat_solver starts using it?
(c) Does heat_io need recompiling? Explain each answer in terms of the dependency graph.
8.22 Draw the dependency graph and give a valid compile order for a program with these modules:
kinds (uses nothing); grid (uses kinds); physics (uses kinds, grid); io (uses kinds,
grid); program main (uses kinds, grid, physics, io).
8.23 † (Critique a design.) A teammate proposes making heat_solver use heat_io (to write debug
output mid-step) and heat_io use heat_solver (to re-normalize a field before writing). Identify the
problem this creates, name the design principle from §8.6 it violates, and propose a concrete fix that
keeps both capabilities.
Part G — Back of the Envelope and Interleaved ⭐⭐⭐
8.24 † (Back of the envelope.) A foundational module is used by 200 source files, each taking about
3 seconds to compile. You need to change how one of its procedures works internally, without changing its
interface. Estimate the rebuild cost two ways: (a) if the procedure body lives in the module (so the
.mod is regenerated and all 200 users recompile), and (b) if it lives in a submodule (so only the
submodule recompiles and the program relinks, say 5 seconds total). What is the ratio, and what does this
say about how large-team Fortran projects should be structured?
8.25 (Port it.) Here is a tiny Python module. Port it to a Fortran module with the same two
functions operating on a rank-1 real(dp) array, and note one way the Fortran version is safer at the
call site.
# stats.py
def rng(x): # range = max - min
return max(x) - min(x)
def mean(x):
return sum(x) / len(x)
8.26 † (Interleaved — Chapters 5 + 8.) Write a module field_ops exporting a
pure function laplacian_interior(u) that returns an array the same shape as u, holding the five-point
neighbour sum $u_{i-1,j}+u_{i+1,j}+u_{i,j-1}+u_{i,j+1}-4u_{i,j}$ on the interior and zero on the edges
(the whole-array section technique from Chapter 5).
Test it on a $4\times4$ field with $u(i,j) = i^3$ and predict the interior values.
8.27 (Interleaved — Chapters 6 + 8.) Explain, concretely, how placing step(field, alpha, dt) in a
module is what lets its assumed-shape argument field(:,:) and a hypothetical optional :: verbose
argument work correctly when step is called from a different file. What exactly would fail if step
were an external procedure with no interface?
8.28 † (Interleaved — Chapters 7 + 8.) Write the heat_io module procedure
write_field(field, filename) so that it opens the file with newunit= and checks iostat= (the
mechanism from Chapter 7), printing a clear
message and returning without writing if the open fails. Why is newunit= especially valuable in a
module that many parts of a program will call?
Solutions to the daggered and odd-numbered problems are in appendices/answers-to-selected.md; the
compilable ones (8.1, 8.8, 8.10, 8.14, 8.25, 8.26, 8.28) are worked in full as code/exercise-solutions.f90.
Compile order is part of the answer for every multi-module problem — always state it.