Exercises: Derived Types

These exercises make you design with types, not just recognize them. You will define your own types, give them methods, predict what programs print, hunt bugs that come from the type/class distinction and from shallow copies, and start turning the heat solver's loose variables into a real data structure. Type and compile every code problem — the point of the chapter is that the compiler now checks your data design for you, and you should feel it doing so.

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 compilable in code/exercise-solutions.f90. Try every problem before you look.


Part A — Warm-ups ⭐

9.1 † In one sentence each, define derived type and component, and give the operator used to access a component.

9.2 Write the declaration of a derived type interval with two real(dp) components lo and hi. Then write a declaration of a variable i of that type and a structure-constructor assignment setting it to the interval $[0, 1]$.

9.3 † What is the single most important rule about the passed-object dummy argument of a type-bound procedure? State it precisely.

9.4 Explain the difference between pass, pass(name), and nopass on a type-bound procedure, in one sentence each.

9.5 † In one sentence, state the key behavioural difference between an allocatable component and a pointer component when a derived-type variable is assigned with b = a.


Part B — Type, Compile, and Run ⭐⭐

Predict the exact output first, then compile and check. If your prediction is wrong, find out why before moving on — that gap is where the learning is.

9.6 Predict the output:

program p6
  use, intrinsic :: iso_fortran_env, only: dp => real64
  implicit none
  type :: box
    real(dp) :: w = 1.0_dp, h = 2.0_dp
  end type box
  type(box) :: b
  print '(f6.2)', b%w * b%h
  b = box(3.0_dp, 4.0_dp)
  print '(f6.2)', b%w * b%h
end program p6

9.7 † Predict the output, paying attention to the nested access:

program p7
  use, intrinsic :: iso_fortran_env, only: dp => real64
  implicit none
  type :: vec2
    real(dp) :: x, y
  end type vec2
  type :: segment
    type(vec2) :: a, b
  end type segment
  type(segment) :: s
  s = segment(vec2(0.0_dp, 0.0_dp), vec2(3.0_dp, 4.0_dp))
  print '(f6.2)', sqrt((s%b%x - s%a%x)**2 + (s%b%y - s%a%y)**2)
end program p7

9.8 Predict the output, then explain why the second printed value is what it is (this is the deep-copy question):

program p8
  use, intrinsic :: iso_fortran_env, only: dp => real64
  implicit none
  type :: bag
    real(dp), allocatable :: v(:)
  end type bag
  type(bag) :: a, c
  a%v = [1.0_dp, 2.0_dp, 3.0_dp]
  c = a
  c%v(1) = 99.0_dp
  print '(f6.2)', a%v(1)
  print '(f6.2)', c%v(1)
end program p8

9.9 † Predict the output. (The method is called through the object; the object is passed as self.)

module m9
  use, intrinsic :: iso_fortran_env, only: dp => real64
  implicit none
  type :: acct
    real(dp) :: balance = 0.0_dp
  contains
    procedure :: deposit => acct_deposit
  end type acct
contains
  subroutine acct_deposit(self, amount)
    class(acct), intent(inout) :: self
    real(dp), intent(in) :: amount
    self%balance = self%balance + amount
  end subroutine acct_deposit
end module m9

program p9
  use m9
  implicit none
  type(acct) :: a
  call a%deposit(100.0_dp)
  call a%deposit(25.0_dp)
  print '(f8.2)', a%balance
end program p9

9.10 Predict the output of this array-of-derived-types program, then explain what sum(swarm%mass) does:

program p10
  use, intrinsic :: iso_fortran_env, only: dp => real64
  implicit none
  type :: body
    real(dp) :: mass
  end type body
  type(body) :: swarm(3)
  swarm(1)%mass = 1.0_dp
  swarm(2)%mass = 2.0_dp
  swarm(3)%mass = 3.0_dp
  print '(f6.2)', sum(swarm%mass)
end program p10

Part C — Design It ⭐⭐

These ask you to build types from scratch. Each has a compilable reference solution; write yours first.

9.11 † Define a derived type rectangle with real(dp) components width and height, and give it two type-bound functions, area and perimeter. Write a program that builds a $3 \times 4$ rectangle and prints both. (Reference: code/exercise-solutions.f90.)

9.12 Define a type grid_cell with a type(vec3) component center, a real(dp) component volume, and a real(dp), dimension(6) component flux. Add a type-bound function net_flux(self) returning the sum of the six face fluxes. Test it with fluxes [1, -1, 2, -2, 3, -3] (net flux $0$).

9.13 † Define a type vec2 with components x, y and two type-bound procedures: a function add(self, other) returning a new vec2, and a function norm(self) returning $\sqrt{x^2 + y^2}$. In a program, add $(3, 4)$ and $(1, 2)$ and print the sum and the norm of $(3, 4)$. (Reference: code/exercise-solutions.f90.)

9.14 Add a nopass type-bound function zero() to your vec2 that returns the zero vector, and call it as v%zero(). Explain why nopass is the right choice here.

9.15 † Design a running_stat accumulator type holding an integer count and the running sum and sum-of-squares, with type-bound procedures push(self, x), mean(self), and var(self) (population variance $\overline{x^2} - \overline{x}^2$). Feed it the sample [2, 4, 4, 4, 5, 5, 7, 9] and print the count, mean, and variance. Verify by hand. (Reference: code/exercise-solutions.f90.)

9.16 Give your running_stat a user-defined constructor (an interface running_stat mapping to a function) that returns a fresh, zeroed accumulator, so you can write st = running_stat(). What does the default structure constructor already give you here, and why might the named constructor still be worth it?


Part D — Find the Bug ⭐⭐

Each snippet is wrong. Say what the compiler or the result will do, and give the fix.

9.17 † Won't compile. Why?

type :: circle
  real(dp) :: r
contains
  procedure :: area => circle_area
end type circle
! ...
pure function circle_area(self) result(a)
  type(circle), intent(in) :: self      ! <-- here
  real(dp) :: a
  a = 3.14159265358979_dp * self%r**2
end function circle_area

9.18 Compiles, but crashes or gives garbage at run time. Why?

type :: bag
  real(dp), allocatable :: v(:)
end type bag
type(bag) :: b
b%v(1) = 10.0_dp        ! <-- here

9.19 † The intent is for q to be an independent copy, but changing q also changes p. What kind of component must data be, and how would you fix it?

type :: table
  real(dp), pointer :: data(:)
end type table
type(table) :: p, q
allocate(p%data(3)); p%data = 1.0_dp
q = p                    ! shallow: q%data points at p%data
q%data(1) = 5.0_dp       ! also changes p%data(1)!

9.20 Won't compile. The constructor call does not match the type. What is wrong?

type :: point3
  real(dp) :: x, y, z
end type point3
type(point3) :: p
p = point3(1.0_dp, 2.0_dp)     ! <-- here

Part E — Port It ⭐⭐

Translate the given code to modern Fortran using a derived type, then note what the type buys you over the original.

9.21 † Port this Python @dataclass to a Fortran derived type with an equivalent method:

from dataclasses import dataclass
from math import hypot

@dataclass
class Particle:
    x: float
    y: float
    mass: float
    def speed_from(self, vx, vy):   # kinetic energy helper
        return 0.5 * self.mass * (vx**2 + vy**2)

9.22 Port this C struct and function to a Fortran derived type with a type-bound procedure. Note the one thing Fortran's version gives you that the C version does not (think about copying the whole struct).

struct Complexish { double re, im; };
double magnitude(struct Complexish z) { return sqrt(z.re*z.re + z.im*z.im); }

9.23 † A colleague models 1000 particles with six parallel arrays: px(1000), py(1000), pz(1000), vx(1000), vy(1000), vz(1000). Rewrite the declarations two ways — (a) as an array of a particle derived type (Array-of-Structures), and (b) as one particles type holding six component arrays (Structure-of-Arrays). Say, in one sentence, when each layout is preferable (you will measure it in Part VII).


Part F — Back of the Envelope ⭐⭐⭐

Order-of-magnitude reasoning about memory and layout. Show your work.

9.24 † A particle type holds three real(dp) positions, three real(dp) velocities, and one real(dp) mass. How many bytes is one particle (a real(dp) is 8 bytes)? How much memory does an array of 1,000,000 particles occupy? Give the answer in megabytes (take $1\ \text{MB} = 10^{6}$ bytes).

9.25 You store the heat field as real(dp), allocatable :: u(:,:) inside a field_t. For a $4096 \times 4096$ grid, how much memory does u occupy? If the solver keeps two fields (current and next time step), what is the total? Would single precision (real32, 4 bytes) halve it?

9.26 † Array-of-Structures vs Structure-of-Arrays. A loop needs only the mass of all 1,000,000 particles from Exercise 9.24. In the Array-of-Structures layout, consecutive masses are 56 bytes apart in memory; in a Structure-of-Arrays layout they are 8 bytes apart (contiguous). Explain, in terms of cache lines (typically 64 bytes), why the second layout can be dramatically faster for this loop, and estimate the ratio of useful-to-fetched bytes for each.


Part G — Interleaved ⭐⭐

Mixing this chapter with earlier ones. These are the retrieval reps that make it stick.

9.27 † (with Chapter 8) Put your rectangle type from 9.11 in a module shapes with private/public so that only the type name (and its methods, via objects) are visible, and the underlying function rect_area cannot be called by name from outside. Show the module skeleton and explain what private plus public :: rectangle achieves.

9.28 (with Chapter 5) Given type(field_t) :: f with f%u(:,:) allocated $nx \times ny$, write a single whole-array statement that sets every interior temperature to the average of the whole field's current values, and a statement that holds column ny at $100$. Which array intrinsic gives you the mean?

9.29 † (with Chapter 6) Your type-bound functions are marked pure. State what pure promises the compiler, and why a method like norm or area qualifies while a method like deposit (which modifies self) does not.

9.30 (with Chapter 3 and the Project) Refactor a driver that currently declares five separate variables — nx, ny, dx, dy and u(:,:) — to instead declare a single type(field_t) :: f and call f%init(...). List two categories of bug this refactor makes impossible to write.


Solutions to the daggered and odd-numbered problems are in appendices/answers-to-selected.md; the compilable ones (9.11, 9.13, 9.15) are in code/exercise-solutions.f90, each with hand-computed expected output. Design problems accept any correct type layout — the reference solution is one good answer, not the only one.