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.