33 min read

> "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.

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 public and private, 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 COMMON block and INCLUDE line — 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 .mod files 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/INCLUDE world 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 name and closed by end 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 no program line 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 contains statement of a module. Because it lives in a module, it has an explicit interface automatically visible to every unit that uses the module, and it can be public (callable from outside) or private (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). The use statement, written use module_name, gives the current program unit access to the public entities of a module. Written use 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 public label 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 save attribute 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 that uses it (if the variable is public), sees the same single instance. Module variables are the modern, typed, controlled replacement for the FORTRAN 77 COMMON block (§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: dp is spiritually from kinds import dp, and public/private is the intent behind Python's leading-underscore convention. Two differences matter. Fortran's boundary is enforced by the compiler, not by convention — a private entity is genuinely unreachable from outside, not merely discouraged. And Fortran modules are compiled: use consults 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 (public and private). Inside a module, public entities are visible to any unit that uses the module; private entities are visible only within the module itself. A bare private statement (on its own line, no list) sets the module's default to private — everything is hidden unless you explicitly list it as public. 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. The n_done = 0 initializer 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 implicit save" 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 interface block) 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/elemental optimization story of Chapter 27.

🔄 Check Your Understanding. 1. Why does private (default-hide) lead to safer modules than private :: 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 where solve is defined for this to compile? 3. A private module 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) solve must have an explicit interface — i.e., be a module procedure (in a module you use) or have an interface block — because keyword arguments require the caller to know the dummy-argument names. (3) False: a private entity'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 a module procedure interface body — signature only, no body — and the submodule supplies the body. A submodule is not something you use; 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 .c file; 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 .mod and so does not force a recompile of the dozens or hundreds of files that use the 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_impl pair above, compile it, and confirm the output. Now change the body of plate_area in the submodule — say, print a debug line inside it — and recompile only plate_geom_impl.f90 and relink, leaving plate_geom.o and area_demo.o untouched. 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 state

There is one declaration of x, y, and n, written once. Every unit that uses state sees 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 misuses n as a real, the compiler says so. The INCLUDE line vanishes entirely, because use does 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 (.mod file). A .mod file is a compiler-generated file — kinds.mod for a module named kinds — 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 check use statements and calls. When you compile a file that says use kinds, the compiler reads kinds.mod. It follows that kinds.mod must already exist — which means kinds.f90 must 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 .mod is 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 .mod first:

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: physics uses io and io uses physics, 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, move log_message into a third, lower-level logging module that both physics and io use, so the graph becomes a tree again. (2) Use a submodule: put io's interfaces in io and its body (which calls step) in a submodule (io), which may use physics freely 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 use only 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/constants layer 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 timers module that heat_solver needs for benchmarking. Where does it go in the layer diagram, and does heat_io have to change?

Answers (1) kinds, because it uses nothing, so its .mod must exist before any other file (all of which use it) can be compiled. (2) timers goes in the lower layers (it likely uses only kinds), and heat_solver adds use timers; heat_io is 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 of step behind 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 mend 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:

  1. Private by default, public on purpose. Start every module with a bare private and list the public API. It fails closed.
  2. Compile dependencies first. A module's .mod must exist before any file that uses it is compiled; the compile order is a topological sort of the dependency graph (leaves first, program last). "Cannot open module file" means out of order, not broken code.
  3. 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.
  4. Modules retired COMMON and INCLUDE: 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).

  1. In Chapter 6 you gave every dummy argument an intent. Which explicit-interface feature of this chapter is what makes intent — and optional, keyword, and assumed-shape arguments — actually work when the caller is in a different file?

    AnswerThe 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.

  2. The heat solver's step takes field(:,:) as an assumed-shape array (Chapter 6). Why can step sit in heat_solver and still accept a plate of any size, and what would break if step were an external procedure with no interface?

    AnswerAssumed-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.

  3. From Chapter 7: our heat_io module reads run parameters from a namelist. In one sentence, what is a namelist, and what is one advantage of it over reading bare numbers in a fixed order?

    AnswerA `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.

  4. From Chapter 7: write_field opens a file with open(newunit=u, ...). Why is newunit= safer than picking a unit number like open(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.