> *"Show me your flowcharts and conceal your tables, and I shall continue to be mystified. Show me your
Prerequisites
- 3
- 5
- 6
- 8
Learning Objectives
- Define a derived type with scalar, array, and nested components, and access those components with the `%` operator.
- Attach behaviour to data with type-bound procedures, declaring the passed-object dummy correctly as `class(...)` and controlling it with `pass`/`nopass`.
- Explain what parameterized derived types offer, write one, and judge honestly when the current compiler support makes them worth using.
- Construct derived-type values with the default and a user-defined constructor, and use allocatable components to get automatic deep-copy and automatic cleanup.
- Design small scientific data structures — a particle, a grid cell, a labeled field — that bundle related data behind one meaningful name.
In This Chapter
Chapter 9: Derived Types — Building Your Own Data Structures
"Show me your flowcharts and conceal your tables, and I shall continue to be mystified. Show me your tables, and I won't usually need your flowcharts; they'll be obvious." — Frederick P. Brooks, Jr., The Mythical Man-Month
Overview
Look back at what your heat solver has become. By the end of Chapter 8
it is a clean set of modules, but the data those modules pass around is still a loose handful of
separate variables: nx and ny for the grid size, dx and dy for the spacing, and a 2D
allocatable array u for the temperature. Every procedure that touches the field must take all five as
separate arguments, in the right order, and every call site must supply them, in the right order. Nothing
in the language stops you from passing the x-spacing where the y-spacing belongs, or handing step
a u that was sized for a different grid. The five variables belong together — they describe one
thing, a discretized temperature field — but the compiler has no idea, because you have never told it.
A derived type is how you tell it. In one declaration you bundle those five variables into a single
new type of your own making, give it a name — field_t — and from then on you pass one object where you
used to pass five, the compiler keeps the pieces together, and a whole category of ordering mistakes
becomes impossible to write. This is the same idea as a struct in C or a @dataclass in Python, and it
is the foundation of everything structural that follows: the object orientation of
Chapter 10 is built on derived types, and every
serious scientific code you will ever read organizes its state into them. Brooks's line at the top of the
chapter is the whole philosophy in one sentence: get the data structures right and the algorithms
become obvious; get them wrong and no amount of clever code will save you.
In this chapter, you will learn to:
- Define your own types with
type … end type, give them scalar, array, and even nested-type components, and read and write those components with the%operator. - Bind procedures directly to a type — type-bound procedures — so an object carries its own behaviour,
and get the one non-negotiable detail (the
class(...)passed object) exactly right. - Understand parameterized derived types, which let a type carry compile-time kind and run-time length parameters — and judge honestly when your compiler is ready for them.
- Construct type values with the built-in constructor and with your own, and use allocatable components to get automatic deep copies and automatic memory cleanup — the reasons they beat pointer components for almost all scientific data.
- Design the small, sturdy data structures that scientific code is made of: a particle, a grid cell, a
labeled field — and finally, the
field_tthat will carry your solver's state for the rest of the book.
Learning Paths
How to read this chapter by track. - 🔬 Scientist — this is one of your load-bearing chapters. Read §§9.1, 9.2, 9.4, 9.5 closely; they are how every real code you will touch organizes its data. Skim §9.3 (parameterized types) — good to recognize, rarely essential. - 📖 Standard — read straight through. Derived types are where Fortran stops being "Fortran with arrays" and becomes a language you design in. - 🔧 Legacy — §9.1 is the modern replacement for the
COMMON-block bundling of state you will meet in Part IV; the "From History" notes connect the two eras. - ⚡ HPC — read §9.4 (allocatable components, no aliasing) and the Performance Notes on memory layout (Array-of-Structures vs Structure-of-Arrays); they decide whether your types vectorize. Case Study 1 is for you.
9.1 Type Definitions and Components
Every language for serious work eventually lets you invent your own data types, because real problems come
with their own natural units of data. A molecular-dynamics code thinks in particles; a mesh code thinks
in cells and nodes; a finance code thinks in trades. Forcing all of that into bare real and
integer variables is possible, but it is like describing a chessboard as sixty-four unrelated integers:
technically complete, humanly hopeless. Fortran 90 gave the language the tool to describe the board as a
board.
Definition (derived type). A derived type is a data type you define yourself by grouping together other data — of intrinsic types like
realandinteger, or of other derived types — under a single new type name. A variable of a derived type is one object that holds all of the grouped values at once. ("Derived" because it is derived from — built out of — the intrinsic types the language gives you.)
Here is the smallest useful example, a point in two dimensions:
type :: point2d
real(dp) :: x
real(dp) :: y
end type point2d
That block does not create any variables; it defines a new type called point2d, exactly the way
integer and real are types, that you can now use in declarations. The two variables inside it — x
and y — are its components.
Definition (component). A component is one of the named data members that a derived type groups together. You read or write a component with the percent operator
%: ifpis apoint2d, thenp%xis itsxcomponent, an ordinaryreal(dp)you can use anywhere areal(dp)is allowed. (Other languages use a dot for this; Fortran uses%because the dot was already taken by things like.and.and.eqv..)
Declaring and using a variable of the new type looks like this:
type(point2d) :: p
p%x = 3.0_dp
p%y = 4.0_dp
print *, sqrt(p%x**2 + p%y**2) ! prints 5.0
Note the shape of the declaration: type(point2d) :: p, with the type name wrapped in type( ), the same
way you would write real(dp) :: q. The wrapper is how the compiler knows point2d is a derived type you
defined rather than an intrinsic one.
Default initialization. You can give a component a default value in the type definition, and every new object of that type starts with it — a small but genuine safety feature, because it kills a class of uninitialized-variable bugs before they hatch:
type :: counter
integer :: count = 0 ! every counter starts at zero
end type counter
Structure constructors. Rather than assign components one at a time, you can build a whole value in one expression using the type's name as a function — its structure constructor:
p = point2d(3.0_dp, 4.0_dp) ! positional: x=3, y=4
p = point2d(x=3.0_dp, y=4.0_dp) ! keyword form — clearer, reorderable
The keyword form is worth a habit: point2d(y=4.0_dp, x=3.0_dp) is identical and immune to your
forgetting the order. Components that have a default initializer may be omitted.
Whole-object assignment just works. Because the compiler knows a point2d is a self-contained value,
q = p copies all of it — every component — with one statement. This will matter enormously in §9.4
when the components are allocatable.
Nested types: types built from types. A component can itself be of a derived type, and this is how you model structured things. A particle has a position and a velocity, and each of those is naturally a 3-vector:
type :: vec3
real(dp) :: x, y, z ! several components of the same type on one line
end type vec3
type :: particle
type(vec3) :: pos ! a component that is itself a derived type
type(vec3) :: vel
real(dp) :: mass
end type particle
Now type(particle) :: p gives you an object with structure, and you reach into it by chaining the
percent operator: p%pos%x is the x of the position, p%vel%z is the z of the velocity. The chain
reads left to right like a path — "particle, its position, its x" — and there is no depth limit beyond
readability. Let us make it concrete and compute something real.
Worked Example. A particle carries a position, a velocity, and a mass. We build one, advance its position by one unit of time (
pos = pos + vel), and report its kinetic energy $\tfrac{1}{2}m v^2$.
program particle_demo
use, intrinsic :: iso_fortran_env, only: dp => real64
implicit none
type :: vec3
real(dp) :: x, y, z
end type vec3
type :: particle
type(vec3) :: pos
type(vec3) :: vel
real(dp) :: mass
end type particle
type(particle) :: p
real(dp) :: speed2
p = particle( pos = vec3(0.0_dp, 0.0_dp, 0.0_dp), &
vel = vec3(1.0_dp, -2.0_dp, 0.5_dp), &
mass = 4.0_dp )
! advance the position by one time unit: pos = pos + vel
p%pos%x = p%pos%x + p%vel%x
p%pos%y = p%pos%y + p%vel%y
p%pos%z = p%pos%z + p%vel%z
speed2 = p%vel%x**2 + p%vel%y**2 + p%vel%z**2
print '(a, 3f8.3)', 'position : ', p%pos%x, p%pos%y, p%pos%z
print '(a, f8.3)', 'speed^2 : ', speed2
print '(a, f8.3)', 'k. energy: ', 0.5_dp * p%mass * speed2
end program particle_demo
$ gfortran -std=f2018 -Wall -O2 example-01-nested-types.f90 -o particle && ./particle
position : 1.000 -2.000 0.500
speed^2 : 5.250
k. energy: 10.500
Trace it by hand and you will trust it: the position updates to $(0{+}1,\,0{-}2,\,0{+}0.5) = (1, -2, 0.5)$; the speed-squared is $1^2 + (-2)^2 + 0.5^2 = 1 + 4 + 0.25 = 5.25$; and the kinetic energy is $\tfrac{1}{2}\cdot 4 \cdot 5.25 = 10.5$. Notice how the code reads like the physics — that is the payoff of naming your data after the thing it represents.
Arrays of derived types. A type is a type, so you can make arrays of it. A swarm of a thousand particles is simply
type(particle) :: swarm(1000)
and swarm(i)%mass is the mass of particle i. Fortran even lets you gather one component across the
whole array as a section — swarm%mass is a real array of all thousand masses, so sum(swarm%mass) is
the total mass in one line. (We will meet a performance caveat about this layout in Case Study 1; for now,
enjoy the expressiveness.)
💡 Intuition: A derived type is a labeled box with labeled compartments.
type(point2d) :: pputs a fresh box namedpon the table;p%xopens the compartment labeledx. Nesting is boxes inside boxes; an array of the type is a shelf of identical boxes. Everything about%is just "reach into the compartment named …".🐍 Python Comparison: In Python you would reach for a
@dataclassor acollections.namedtuple; in C, astruct. Fortran's derived type is closest to the Cstruct— a fixed set of named fields laid out in memory — but with two upgrades the C version lacks: the compiler knows the whole object's shape, soq = pcopies the entire structure correctly, and (as of §9.2) the type can carry its own procedures. Unlike a Python object, a Fortran derived type is statically typed and fixed: its components are frozen at compile time, which is exactly what lets the compiler lay it out as tightly and access it as fast as a hand-written struct.📜 From History: Before Fortran 90 there were no derived types at all. Programmers faked them with parallel arrays (
px,py,pz,vx, …, one array per field) or, worse, overlaid unrelated variables onto the same memory withCOMMONandEQUIVALENCE— the fragile, untyped bundling you will meet in Chapter 17. Derived types replaced a memory trick with a language feature the compiler actually understands and checks.🔄 Check Your Understanding. 1. Given
type(particle) :: p, how do you refer to theycomponent of the particle's velocity? 2. Write a structure-constructor expression that builds avec3with all three components equal to 1. 3. What does the single statementq = pdo whenpandqare bothtype(particle)?Answers
1.p%vel%y— chain the percent operator: particle → its velocity → that vector's y. 2.vec3(1.0_dp, 1.0_dp, 1.0_dp)(orvec3(x=1.0_dp, y=1.0_dp, z=1.0_dp)). 3. It copies every component ofpintoq— a complete, whole-object copy — because the compiler knows the full shape of aparticle. (When components are allocatable, §9.4, this becomes a deep copy.)
9.2 Type-Bound Procedures and the pass Argument
So far a derived type is pure data. But data usually comes with operations that belong to it: a circle has
an area, a particle has a kinetic energy, a field has a way to initialize itself. You could write
those as ordinary module procedures — area(c), kinetic_energy(p) — and that is perfectly good Fortran.
Fortran 2003 offers something tidier: you can bind a procedure to the type itself, so the object carries
its own behaviour and you call it through the object.
Definition (type-bound procedure). A type-bound procedure is a procedure attached to a derived type in the type definition, invoked through an object of that type with the percent operator:
object%procedure(args). By default the object itself is passed automatically to the procedure as its first argument — the passed-object dummy argument — soc%area()is really a call to the underlying function withchanded in for you. It is the Fortran spelling of what other languages call a method.
The binding goes in a contains section inside the type definition, and it maps a binding name (what you
call) to a module procedure (what actually runs):
type :: circle
real(dp) :: radius
contains
procedure :: area => circle_area ! binding name => actual procedure
end type circle
and the procedure itself lives in the module's own contains, written almost like any function — with one
detail that is not optional:
pure function circle_area(self) result(a)
class(circle), intent(in) :: self ! NOTE: class(...), not type(...)
real(dp) :: a
a = 3.14159265358979_dp * self%radius**2
end function circle_area
The passed object — here named self, though the name is yours to choose — must be declared with
class, not type. This is the single most common first error with type-bound procedures, and the
compiler will reject type(circle) here outright. Why class? Because a type-bound procedure is allowed
to be called on the type or on any later extension of it, and class(circle) is the declaration that
says "a circle, or something built from a circle." You will not extend a type until
Chapter 10; until then, simply read class(circle) as
"the object this method was called on," and always write class. That one habit will save you a great deal
of confusion.
Calling it is exactly as advertised — the object goes on the left of the %, and it is passed in for you:
type(circle) :: c
c = circle(2.0_dp)
print *, c%area() ! passes c as self; prints ~12.566
🚪 Threshold Concept. The shift from
area(c)toc%area()looks cosmetic, but it changes how you think. The object is no longer inert data that functions act upon from outside; it is a thing that knows how to compute its own area. Once you see data structures as bundling state and the behaviour that belongs to it, the whole design of large scientific frameworks — solvers that know how to step themselves, fields that know how to write themselves — falls into place. This is the doorway to Chapter 10, and it opens here.
The pass attribute — which argument is the object? By default the passed object is the first dummy
argument of the procedure. Two attributes let you change that:
nopass— the object is not passed at all. Use this for a procedure that logically belongs to the type but needs no particular instance, such as a factory that builds a unit vector. You still call it through an object (v%axis(2)), but the object is ignored and only the explicit arguments go in.pass(name)— the object is passed as the dummy argument calledname, which need not be the first. Use this when the natural argument order puts the object second, e.g.procedure, pass(self) :: scalewhere the procedure isscale(factor, self).
Here is a complete module that shows the default pass, a two-argument method, and a nopass factory,
all on a small 3-vector:
module vector_mod
use, intrinsic :: iso_fortran_env, only: dp => real64
implicit none
private
public :: vec3
type :: vec3
real(dp) :: x = 0.0_dp, y = 0.0_dp, z = 0.0_dp
contains
procedure :: norm => vec3_norm ! passed object = self (default, 1st arg)
procedure :: dot => vec3_dot ! two objects: self and other
procedure, nopass :: axis => vec3_axis ! no object passed at all
end type vec3
contains
pure function vec3_norm(self) result(r)
class(vec3), intent(in) :: self
real(dp) :: r
r = sqrt(self%x**2 + self%y**2 + self%z**2)
end function vec3_norm
pure function vec3_dot(self, other) result(r)
class(vec3), intent(in) :: self, other
real(dp) :: r
r = self%x*other%x + self%y*other%y + self%z*other%z
end function vec3_dot
pure function vec3_axis(i) result(v) ! i = 1,2,3 -> unit x,y,z
integer, intent(in) :: i
type(vec3) :: v
v = vec3(0.0_dp, 0.0_dp, 0.0_dp)
select case (i)
case (1); v%x = 1.0_dp
case (2); v%y = 1.0_dp
case (3); v%z = 1.0_dp
end select
end function vec3_axis
end module vector_mod
program tbp_demo
use vector_mod, only: vec3
implicit none
type(vec3) :: a, b, e
a = vec3(3.0_dp, 4.0_dp, 0.0_dp)
b = vec3(1.0_dp, 0.0_dp, 2.0_dp)
e = a%axis(2) ! nopass: 'a' is ignored, returns unit-y
print '(a, f8.3)', '|a| = ', a%norm()
print '(a, f8.3)', 'a . b = ', a%dot(b)
print '(a, 3f6.1)', 'axis(2) = ', e%x, e%y, e%z
end program tbp_demo
$ gfortran -std=f2018 -Wall -O2 example-02-type-bound.f90 -o tbp && ./tbp
|a| = 5.000
a . b = 3.000
axis(2) = 0.0 1.0 0.0
Every value is hand-checkable: $|a| = \sqrt{3^2 + 4^2 + 0^2} = 5$; the dot product
$a\cdot b = 3\cdot 1 + 4\cdot 0 + 0\cdot 2 = 3$; and axis(2), ignoring the object it was called on, returns
the unit vector along $y$, $(0, 1, 0)$. Notice that a%dot(b) passes two vec3 objects — a as the
automatic self, b as the explicit other — which is the ordinary way binary operations look as
methods.
🐛 Find the Bug. A reader writes the area method's signature as
pure function circle_area(self) result(a); type(circle), intent(in) :: self. It will not compile. What is wrong, and what is the fix?Answer
The passed-object dummy of a type-bound procedure must be polymorphic — declaredclass(circle), nottype(circle). The fix is a one-word change:class(circle), intent(in) :: self. (typewould forbid the procedure from ever being inherited by an extension ofcircle, which the standard does not allow for a passed object.)⚡ Performance Note: A type-bound procedure call carries essentially no overhead here — because the object is not polymorphic in this chapter, the compiler resolves
c%area()to a direct, inlinable call tocircle_area, identical in speed to writingcircle_area(c). The runtime dispatch that can cost something appears only with true polymorphism in Chapter 10, and even then only when the compiler cannot prove the dynamic type. For now, methods are free.
9.3 Parameterized Derived Types
Sometimes a type needs to be the same shape of thing at different sizes or precisions. A mathematical vector might have length 2 in one place and length 100 in another; a buffer might be single precision in a draft run and double in production. You could hard-code the size, or make the storage allocatable (§9.4), but Fortran 2003 offers a third option that bakes the parameter into the type itself.
Definition (parameterized derived type). A parameterized derived type (PDT) is a derived type that carries one or more type parameters, written in parentheses after the type name. Each parameter is declared inside the type as either a kind parameter (a compile-time constant, such as a precision, declared
integer, kind) or a length parameter (a value fixed when an object is created, such as an array extent, declaredinteger, len). It is the user-defined analogue of the built-in parameters you already use:real(dp)has a kind parameter;character(len=20)has a length parameter.
The syntax puts the parameter names after the type name, then declares each one's role inside:
type :: rvector(k, n)
integer, kind :: k = dp ! a KIND parameter: the precision (compile-time constant)
integer, len :: n ! a LEN parameter: the length (fixed per object)
real(k) :: comp(n) ! components may use the parameters
end type rvector
You then supply the parameters where you declare a variable, just as you pass dp to real:
type(rvector(dp, 3)) :: v ! k = dp, n = 3 -> comp is real(dp), length 3
A length parameter can also be deferred (:) for an allocatable object, or assumed (*) for a
dummy argument that inherits its caller's length — the very same : and * you already use with
allocatable arrays and assumed-length strings. Inside the program, the parameter reads back as an inquiry:
v%n is 3.
Put together, a minimal, standard-conforming PDT program looks like this:
module pdt_demo
use, intrinsic :: iso_fortran_env, only: dp => real64
implicit none
type :: rvector(k, n)
integer, kind :: k = dp
integer, len :: n
real(k) :: comp(n)
end type rvector
end module pdt_demo
program use_pdt
use pdt_demo
use, intrinsic :: iso_fortran_env, only: dp => real64
implicit none
type(rvector(dp, 3)) :: v
v%comp = [1.0_dp, 2.0_dp, 2.0_dp]
print '(a, i0)', 'length n = ', v%n
print '(a, f8.3)', 'norm = ', sqrt(sum(v%comp**2))
end program use_pdt
A conforming compiler prints:
length n = 3
norm = 3.000
since $\sqrt{1^2 + 2^2 + 2^2} = \sqrt{9} = 3$.
⚠️ Common Pitfall — the one modern feature where the compiler may not be ready. Parameterized derived types are fully standard (Fortran 2003 and 2018), but they are, honestly, the corner of modern Fortran where compiler support has lagged the standard the most. gfortran has supported PDTs since version 8, yet bugs in the more elaborate cases have persisted for years, and different compilers disagree at the edges. If the example above fails to compile on your gfortran, that is a known weakness, not your mistake — try a newer gfortran or the Intel
ifxcompiler, or, for anything nontrivial, prefer the allocatable components of §9.4, which achieve most of what length parameters offer with rock-solid support across every compiler. This is the rare place in this book where the pragmatic advice is "recognize the feature, but reach for the alternative." We flag it plainly rather than pretend the ground is firmer than it is.🔗 Connection: The reason we can afford to be relaxed about PDTs is that the far more common need — "a field of some size decided at run time" — is met beautifully by allocatable components, which is exactly how the project's
field_twill hold its temperature array. Length parameters are elegant; for our purposes, allocatable arrays inside the type are both elegant and universally supported.
9.4 Constructors and Allocatable Components
We have already met the built-in structure constructor — point2d(3.0_dp, 4.0_dp). It is generated for
you from the type definition, it takes the components in declaration order (or by keyword), and it is
usually all you need. But two things push you further: sometimes you want a constructor that does work
(validating, converting units, allocating storage), and very often you want a component whose size is not
known until run time. Both lead to the most important idea in this chapter for scientific code:
allocatable components.
A user-defined constructor. You can make the type's name also call a function of your own by
declaring a generic interface with that name. Then field(...) runs your function instead of (or in
addition to) the built-in constructor, as long as the argument list is distinguishable:
type :: labeled_field
character(:), allocatable :: name
real(dp), allocatable :: values(:)
end type labeled_field
interface labeled_field ! overload the constructor with our own
module procedure new_field
end interface labeled_field
function new_field(name, n, fill) result(f)
character(*), intent(in) :: name
integer, intent(in) :: n
real(dp), intent(in) :: fill
type(labeled_field) :: f
f%name = name ! deferred-length string: auto-allocates to fit
allocate(f%values(n))
f%values = fill ! broadcast the scalar to every element
end function new_field
Now labeled_field("temperature", 4, 10.0_dp) calls new_field, which allocates a four-element array and
fills it — a constructor that actually builds something. (The built-in constructor, which would want the
array itself, still exists; the compiler picks between them by the argument types, which differ here.)
Definition (allocatable component). An allocatable component is a component of a derived type that has the
allocatableattribute — its size (or, for a string, its length) is not fixed in the type definition but is set at run time withallocateor by assignment. Theallocatablearrays of Chapter 5 were variables; here the very same attribute lives inside a type, so an object can carry a right-sized array of its own.
Allocatable components are what let a single type describe fields of any size: field_t will hold a
real(dp), allocatable :: u(:,:), and the same type works for a $10\times 10$ plate or a $10{,}000 \times
10{,}000$ one. But their real superpower is subtler, and it is the reason they are the default choice over
the alternative (pointer components, which you will study properly in
Chapter 11).
The deep-copy guarantee. When a derived type has allocatable components, ordinary assignment b = a
does the right thing automatically: it allocates b's components to match a's and copies the values
across. b ends up with its own independent storage holding the same numbers — a deep copy. Change
b afterward and a is untouched, because they were never sharing memory. This is value semantics, and it
is what your intuition already expects from b = a.
🚪 Threshold Concept — why allocatable beats pointer components. Imagine the same type built with a pointer component instead. Then
b = awould copy the pointer, not the data:b's pointer would end up aimed at the very same array asa's. Nowbandasecretly share storage — write through one and the other silently changes; deallocate one and the other dangles. That shallow-copy aliasing is a classic, hard-to-find bug, and it is the default behaviour of pointer components. Allocatable components make it impossible: assignment always deep-copies, storage is never shared by accident, and — because the compiler knows two allocatables cannot alias — it can also optimize more aggressively. Prefer allocatable components for essentially all scientific data; reach for a pointer component only when you genuinely need sharing or a self-referential structure (Chapter 11 makes that case).
Automatic cleanup. The second gift is memory hygiene. When a variable with allocatable components goes
out of scope — a local of a procedure returning, say — every allocatable component is deallocated
automatically. There is no deallocate to remember, no leak if you forget it. Pointer components have no
such guarantee; a lost pointer is a leaked allocation. Combined with the deep-copy rule, this means a type
built from allocatable components behaves like a well-mannered value: it copies cleanly and cleans up after
itself.
Here is the whole story in one runnable program — a custom constructor, an allocatable component, and a direct demonstration that assignment deep-copies:
program ctor_demo
use, intrinsic :: iso_fortran_env, only: dp => real64
use field_mod, only: labeled_field
implicit none
type(labeled_field) :: a, b
a = labeled_field("temperature", 4, 10.0_dp) ! custom constructor: [10,10,10,10]
a%values(1) = 20.0_dp ! a is now [20,10,10,10]
b = a ! DEEP copy (allocatable component)
b%values(1) = 99.0_dp ! change b ...
print '(a, a)', 'name(a) = ', a%name
print '(a, i0)', 'size(a) = ', size(a%values)
print '(a, f8.3)', 'mean(a) = ', a%mean()
print '(a, f8.3)', 'a%val(1) = ', a%values(1) ! ... a must be unchanged
print '(a, f8.3)', 'b%val(1) = ', b%values(1)
end program ctor_demo
$ gfortran -std=f2018 -Wall -O2 example-03-allocatable-components.f90 -o ctor && ./ctor
name(a) = temperature
size(a) = 4
mean(a) = 12.500
a%val(1) = 20.000
b%val(1) = 99.000
The last two lines are the point of the whole section. After b = a and then b%values(1) = 99, the value
a%values(1) is still 20.000, not 99.000 — proof that b got its own copy of the array. The mean is
$\tfrac{20 + 10 + 10 + 10}{4} = \tfrac{50}{4} = 12.5$, and a's name and size are intact. (The mean
method and the module wrapping this type are in the chapter's code/ directory as
example-03-allocatable-components.f90.) Had values been a pointer component, a%values(1) would read
99.000 — the bug that this design makes unwriteable.
⚠️ Common Pitfall: An allocatable component that you never allocate is unallocated, and touching its elements is an error the same way an unallocated array is. Building an object with the default structure constructor leaves an allocatable component unallocated unless you supply it; a custom constructor (as above) is the clean place to guarantee allocation. When in doubt,
allocated(a%values)tells you the truth, and in Chapter 13 we will check thestat=of everyallocateso a failure is caught rather than trusted.🔄 Check Your Understanding. 1. Why does
b = aleaveaunchanged whenahas an allocatable component, but might not ifahad a pointer component instead? 2. What happens to the storage of a local variable with an allocatable component when its procedure returns? 3. Innew_field, the linef%name = namenever callsallocate. How doesf%nameend up the right length?Answers
1. Allocatable-component assignment deep-copies:bgets its own storage holding a copy of the values, so the two never share memory. A pointer component would be copied as a pointer (shallow), leavingbaimed ata's data — change one, change both. 2. It is deallocated automatically — no leak, no explicitdeallocateneeded. 3.f%nameis a deferred-length allocatable character (Chapter 12); assigning to it automatically (re)allocates it to exactly the length of the right-hand side.
9.5 Building Scientific Data Structures
You now have every piece. The craft of the chapter is design: choosing what to bundle into a type so that the code that uses it reads like the science it models. A good rule of thumb is that a derived type should gather data that shares an invariant — quantities that only make sense together and must change together. Three small, recurring scientific structures show the range.
A particle. We built it in §9.1: position, velocity, mass, perhaps charge or a species tag, and — with
§9.2 — methods like kinetic_energy or momentum. Its invariant is "these describe one body," and
bundling them means an N-body code passes bodies(i) rather than juggling six parallel arrays and hoping
the indices stay in step. (Whether an array of particles or parallel arrays inside one particles type
is faster is a real performance question — Array-of-Structures versus Structure-of-Arrays — and Case
Study 1 measures the trade honestly.)
A grid cell. Finite-volume and unstructured codes think in cells, and a cell bundles geometry with state:
type :: grid_cell
type(vec3) :: center ! where the cell is
real(dp) :: volume ! how big it is
real(dp) :: temperature ! the state it carries
real(dp) :: flux(6) ! flux through its six faces
end type grid_cell
The invariant is "one control volume and everything true of it." A method net_flux(self) that sums the
face fluxes belongs naturally on the type. This is precisely how production CFD and combustion codes
organize their unknowns.
A labeled field. We built this in §9.4: a name (and, in a fuller version, physical units), plus an allocatable array of values, plus methods to summarize it. Its invariant is "these numbers, and what they mean." Attaching the metadata to the data is what lets a diagnostics routine print "temperature: mean 12.5 K" instead of an anonymous number — the difference between output you can trust and output you have to decode. Case Study 2 designs exactly such a container from scratch.
These are not toy patterns; they are the actual vocabulary of scientific software. Read the source of a weather model, a molecular-dynamics engine, or a linear-algebra library and you will find its state organized into derived types like these, with type-bound procedures that let each object act on itself. And now it is time to build the one that will carry your own project for the next thirty chapters.
🔗 Connection: The same libraries that make Fortran the substrate of scientific computing organize their data this way. When you call LAPACK in Chapter 21 you will pass plain arrays across the boundary for speed and Fortran-77 compatibility, but the modern codes that wrap and drive those libraries — and your own solver — hold their state in derived types exactly like the ones in this section.
Project Checkpoint
Until now the solver's state has been a scatter of separate variables. This checkpoint gathers them into a
single purpose-built type, field_t, living in a new module heat_types — the piece the whole rest of the
book will build on. It bundles the grid dimensions (nx, ny), the spacings (dx, dy), and the
temperature array (u) as an allocatable component, and it gives the type a type-bound init that
sizes and zeroes the field in one call.
module heat_types
use kinds, only: dp ! the dp parameter from Chapter 3's kinds.f90
implicit none
private
public :: field_t
type :: field_t
integer :: nx = 0, ny = 0
real(dp) :: dx = 0.0_dp, dy = 0.0_dp
real(dp), allocatable :: u(:,:) ! the temperature field, sized at run time
contains
procedure :: init => field_init
end type field_t
contains
subroutine field_init(self, nx, ny, dx, dy)
class(field_t), intent(out) :: self ! class(...), and intent(out) resets it
integer, intent(in) :: nx, ny
real(dp), intent(in) :: dx, dy
self%nx = nx; self%ny = ny
self%dx = dx; self%dy = dy
allocate(self%u(nx, ny))
self%u = 0.0_dp
end subroutine field_init
end module heat_types
The refactor at the call sites is the reward. Where step and the I/O routines once took
(nx, ny, dx, dy, u) as five separate arguments, they now take one type(field_t) — impossible to pass in
the wrong order, and self-describing. A driver builds a field with a single call:
type(field_t) :: f
call f%init(nx=5, ny=4, dx=0.25_dp, dy=0.25_dp)
f%u(:, f%ny) = 100.0_dp ! hold the top edge (column ny) at 100 degrees
After this, size(f%u) is $5\times 4 = 20$, f%u(1,1) is 0.000, and because only the last column is
set to 100 across all five rows, sum(f%u) is $5\times 100 = 500.0$. The full compilable version — bundling
a small kinds module so it builds on its own — is code/project-checkpoint.f90, with the hand-computed
expected output. Two design choices are worth naming: the passed object of init is class(field_t) (never
type, §9.2), and its intent(out) means a second init call cleanly deallocates the old u before
reallocating — the automatic cleanup of §9.4 working for you. From Chapter 24
onward, when the solver becomes real, and all the way to the Chapter 38
capstone, every routine will speak in terms of this one type.
Summary
This chapter turned Fortran from a language you compute in into one you design in: you can now invent data types that match your problem.
| Idea | The short version |
|---|---|
| Derived type | type :: name … end type name groups data under one type; declare with type(name) :: v. |
| Component | A named member; read/write with % (v%comp, p%pos%x). Give defaults with = value. |
| Structure constructor | Build a value with the type name: point2d(3.0_dp, 4.0_dp) or keyword form. |
| Nested types | A component can be a derived type; chain the %: p%vel%z. |
| Type-bound procedure | procedure :: m => proc in the type; call v%m(...); the object is passed automatically. |
| The passed object | Declare it class(name), never type(name). pass(arg) renames it; nopass omits it. |
| Parameterized type | type :: t(k, n) with integer, kind :: k / integer, len :: n. Elegant, but check compiler support. |
| Custom constructor | An interface with the type's name, mapping to a function that returns the type. |
| Allocatable component | real(dp), allocatable :: u(:,:) inside a type: sized at run time, deep-copied on =, auto-freed. |
The two things to memorize. First: the passed-object dummy of a type-bound procedure is
class(...), not type(...) — get this wrong and nothing compiles. Second: allocatable components
give you value semantics — b = a makes an independent deep copy and cleanup is automatic, which is
exactly why you prefer them to pointer components for scientific data.
Spaced Review
Retrieval practice on the two chapters this one leans on hardest — arrays (Chapter 5) and modules (Chapter 8). Answer before peeking.
-
Our
field_tholdsreal(dp), allocatable :: u(:,:). Recalling column-major order from Chapter 5, when we later sweep this array in a nested loop, which index — the first or the second — should vary in the inner loop for cache-friendly access?
Answer
The **first** index. Fortran stores arrays column-major: the first subscript is contiguous in memory, so the inner loop should run over the first index (`i`), with the outer loop over the second (`j`). Getting this backwards is the classic 10× slowdown of Chapter 27. -
In Chapter 5 you allocated a bare array with
allocate(u(nx, ny)). In this chapter that same array is a component of a type. What does an allocatable component gain you that a bare allocatable array does not, when you writeb = a?
Answer
Automatic **deep copy** as part of whole-object assignment: `b = a` allocates `b%u` and copies the values, giving `b` independent storage. It also gets automatic deallocation when the object goes out of scope. A bare allocatable array copies fine too, but the type lets `u` travel *bundled with* its `nx, ny, dx, dy` as one value. -
heat_typesdeclaresprivatethenpublic :: field_t. From Chapter 8, what does this pair achieve, and can code that doesuse heat_typesstill reachfield_initdirectly?
Answer
`private` makes everything in the module inaccessible by default; `public :: field_t` re-exposes only the type. So `field_init` is **not** callable by name from outside — users reach it only as the binding `f%init(...)`. This is the "clean interface, hidden implementation" discipline of Chapter 8. -
From Chapter 8, a module gives its procedures an explicit interface "for free." Why does that matter the moment
field_inittakes aclass(field_t)and an allocatable-component-bearing object as arguments?
Answer
Passing polymorphic (`class`) objects and allocatable-component types *requires* an explicit interface at the call site — the compiler must know the full shape of the dummy arguments. Putting the type and its procedures in a module supplies that interface automatically; the same code outside a module (with only an implicit interface) would be invalid. Modules and derived types are designed to work together.
What's Next
You have built types that bundle data and carry their own procedures — and you wrote class(...) on every
passed object without fully cashing in what the word buys you. Chapter 10
collects that debt. It shows how one type can extend another (a charged_particle that is a particle
plus a charge), how class enables genuine polymorphism so a single routine can operate on a whole family
of types, and how select type lets you ask, at run time, which specific type you actually hold. That is
full object-oriented Fortran — abstraction, inheritance, dynamic dispatch — and it is built entirely on the
derived types you just learned. We will also be honest, as few treatments are, about when that machinery
earns its keep in scientific code and when it merely adds overhead and indirection. Bring field_t with
you; the next chapter asks what happens when a solver needs to be one of several interchangeable solvers.