Exercises: Procedures
These exercises drill the one habit and the one tool that make Fortran code robust: an intent on every
argument, and arrays passed as assumed-shape. Type and compile everything — the compiler is a teacher here,
and several problems are designed to be rejected by it so you learn to read its messages.
Difficulty: ⭐ warm-up · ⭐⭐ standard · ⭐⭐⭐ deeper. Solutions: worked solutions to the daggered (†)
and odd-numbered problems are in appendices/answers-to-selected.md; the computational ones are also
provided as compilable code in code/exercise-solutions.f90. Predict every output before you compile.
Throughout, assume use, intrinsic :: iso_fortran_env, only: dp => real64 and implicit none unless a
snippet shows otherwise.
Part A — Warm-ups ⭐
6.1 † For each task, say whether you would write a subroutine or a function, and why:
(a) compute the mean of a vector; (b) swap two variables; (c) return the area of a circle; (d) write a
field to a file; (e) solve $A\mathbf{x}=\mathbf{b}$, returning the solution vector x.
6.2 Give the correct intent for each argument: (a) subroutine normalize(v, s) divides v by the
scalar s in place and leaves s unchanged; (b) subroutine minmax(x, lo, hi) reads array x and returns
its extremes; (c) subroutine accumulate(total, x) adds x to a running total.
6.3 Predict the exact printed line, then compile to check:
print '(f6.2)', twice(triple(2.0_dp))
! ...
pure function triple(x) result(y)
real(dp), intent(in) :: x
real(dp) :: y
y = 3.0_dp * x
end function triple
pure function twice(x) result(y)
real(dp), intent(in) :: x
real(dp) :: y
y = 2.0_dp * x
end function twice
6.4 The function skeleton below is missing its result clause and an intent. Add both so it compiles.
function hypotenuse(a, b)
real(dp) :: a, b
hypotenuse = sqrt(a**2 + b**2)
end function hypotenuse
Part B — Functions and Subroutines ⭐⭐
6.5 † Write a pure function norm2_of(x) result(nrm) that returns the Euclidean norm
$\sqrt{\sum_i x_i^2}$ of an assumed-shape vector x(:). Test it on [3.0_dp, 4.0_dp] and predict the result.
(Code solution provided.)
6.6 A colleague writes a function that "returns the mean and the standard deviation." Explain why a Fortran function cannot do this directly, and rewrite the interface as a subroutine.
6.7 † Predict the output, then compile:
real(dp) :: lo, hi
call bounds([4.0_dp, -1.0_dp, 7.0_dp, 2.0_dp], lo, hi)
print '(2f7.2)', lo, hi
! ...
subroutine bounds(x, mn, mx)
real(dp), intent(in) :: x(:)
real(dp), intent(out) :: mn, mx
mn = minval(x)
mx = maxval(x)
end subroutine bounds
Part C — intent, the Contract ⭐⭐
6.8 † Find the bug. This will not compile. What does gfortran say, and what is the one-word fix?
subroutine normalize(v, s)
real(dp), intent(in) :: v(:)
real(dp), intent(in) :: s
v = v / s
end subroutine normalize
6.9 Find the bug. This compiles but gives a wrong (and unpredictable) answer. Diagnose it and fix it
without changing any intent.
subroutine add_all(x, total)
real(dp), intent(in) :: x(:)
real(dp), intent(out) :: total
integer :: i
do i = 1, size(x)
total = total + x(i)
end do
end subroutine add_all
6.10 † Explain, in terms of "definable entities," why call swap(1.0_dp, 2.0_dp) fails to compile when
swap's arguments are intent(inout), while call swap(a, b) (with variables a, b) succeeds.
Part D — Optional and Keyword Arguments ⭐⭐
6.11 † Write a pure function clamp(x, lo, hi) result(y) that returns x limited to the range
[lo, hi], where hi is optional — if omitted, there is no upper limit. Predict the results of
clamp(5.0_dp, 0.0_dp, 3.0_dp), clamp(-1.0_dp, 0.0_dp, 3.0_dp), and clamp(2.0_dp, 0.0_dp).
(Code solution provided.)
6.12 A subroutine has the interface subroutine plot(x, y, color, width) where color and width are
both optional. Write a call that supplies x, y, and width but not color. Why is a keyword
mandatory here?
6.13 † Find the bug. Why is this dangerous, and what is the fix?
subroutine scale(x, factor)
real(dp), intent(inout) :: x(:)
real(dp), intent(in), optional :: factor
x = x * factor
end subroutine scale
Part E — pure and elemental ⭐⭐
6.14 † Write an elemental function sigmoid(x) result(s) computing $s = 1/(1 + e^{-x})$, and apply it
to the array [-1.0_dp, 0.0_dp, 1.0_dp] in a single statement. Predict the three values to four decimals.
(Code solution provided.)
6.15 For each, state whether it can be pure, and why: (a) a function returning sum(x) of its array
argument; (b) a function that prints a debug line and returns 2*x; (c) a function that increments a
module-level call counter and returns it; (d) a function returning x**2 + 1.0_dp. Which of the pure ones
could also be elemental?
6.16 † Take the impure function from 6.15(b) and make it pure, or argue that it cannot be. If it can,
show the corrected version and say where the debug output should go instead.
Part F — Recursion and Internal Procedures ⭐⭐⭐
6.17 † Write a recursive function gcd(a, b) result(g) implementing Euclid's algorithm
(gcd(a, 0) = a, otherwise gcd(b, mod(a, b))). Predict gcd(48, 36) and gcd(1071, 462).
(Code solution provided.)
6.18 Rewrite the recursive factorial from §6.5 as an iterative function using a do loop. Which
version would you ship in a numerical library, and why?
6.19 † Find the hazard. The internal subroutine below shares the host's loop variable. Describe the bug this can cause and give the one-line fix.
program p
implicit none
integer :: i
do i = 1, 3
call greet()
end do
contains
subroutine greet()
do i = 1, 5 ! uses the host's i
! ... some work ...
end do
end subroutine greet
end program p
Part G — Array Arguments, Design, and Estimation ⭐⭐/⭐⭐⭐
6.20 † Modernize it. Rewrite this FORTRAN 77-style routine in modern Fortran: assumed-shape,
real(dp), intent, implicit none, and a whole-array operation. Note how many arguments disappear.
SUBROUTINE DSCALE(A, N, S)
INTEGER N, I
DOUBLE PRECISION A(*), S
DO 10 I = 1, N
A(I) = A(I) * S
10 CONTINUE
END
6.21 Port it. Translate this NumPy-style Python function to an elemental Fortran function, then
say why the Fortran version needs no loop even when applied to a large array.
def relu(x):
return x if x > 0.0 else 0.0 # applied elementwise to a NumPy array
6.22 † Design it (heat solver). Extend step(field, alpha, dt) with an optional argument
bc_value that, when present, resets the plate's four edges to that fixed temperature before the interior
update. Write the new interface and the edge-setting code (use array sections). Why is optional the right
choice rather than always requiring it?
6.23 Back of the envelope. In our step, allocate(old(nx,ny)); old = field copies the whole field
each call. For a $1000 \times 1000$ grid of real(dp) run for 10,000 steps: (a) how many bytes is one copy?
(b) how much data is copied over the whole run? (c) what cheap change removes the per-step allocate?
6.24 † Interleaved (Ch. 5). Write a pure function max_interior(f) result(mx) that returns the
largest value among the interior cells of an assumed-shape 2-D array f(:,:) (exclude the first/last row
and column) using a single maxval on an array section. Test it on the column-major matrix holding
1.0_dp through 16.0_dp in a $4\times4$ and predict the answer. (Code solution provided.)
6.25 Interleaved (Ch. 4 & 5). Write subroutine apply_dirichlet(field, edge) that sets all four
boundaries of field(:,:) to the scalar edge using array-section assignments. Which intent does
field need, and why not intent(out)?
6.26 † Find the bug. A program declares real(dp) :: g(5) and calls an external subroutine
(defined in a separate file, not in a module or contains) whose dummy argument is x(:). The results are
garbage. What is missing, and what are the two clean fixes?
6.27 Design it. Refactor the §6.1 describe subroutine into two pure functions — mean(x) and
variance(x) — and rebuild describe on top of them. What did you gain, and what (if anything) did you lose?
6.28 † Back of the envelope. Someone proposes summing a $10^6$-element array with a recursive
function that peels off one element per call. Estimate the recursion depth, explain why this risks a stack
overflow where a do loop does not, and give the loop version's stack cost.
Solutions to the daggered and odd-numbered problems are in appendices/answers-to-selected.md; the
computational ones (6.5, 6.11, 6.14, 6.17, 6.24) are compilable in code/exercise-solutions.f90. The
"find the bug" problems are worth extra attention — being able to read a compiler's intent and interface
errors fluently will save you more time than any other skill in this chapter.