28 min read

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

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_t that 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 real and integer, 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 %: if p is a point2d, then p%x is its x component, an ordinary real(dp) you can use anywhere a real(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) :: p puts a fresh box named p on the table; p%x opens the compartment labeled x. 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 @dataclass or a collections.namedtuple; in C, a struct. Fortran's derived type is closest to the C struct — 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, so q = p copies 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 with COMMON and EQUIVALENCE — 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 the y component of the particle's velocity? 2. Write a structure-constructor expression that builds a vec3 with all three components equal to 1. 3. What does the single statement q = p do when p and q are both type(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) (or vec3(x=1.0_dp, y=1.0_dp, z=1.0_dp)). 3. It copies every component of p into q — a complete, whole-object copy — because the compiler knows the full shape of a particle. (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 — so c%area() is really a call to the underlying function with c handed 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) to c%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 called name, which need not be the first. Use this when the natural argument order puts the object second, e.g. procedure, pass(self) :: scale where the procedure is scale(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?

AnswerThe passed-object dummy of a type-bound procedure must be polymorphic — declared class(circle), not type(circle). The fix is a one-word change: class(circle), intent(in) :: self. (type would forbid the procedure from ever being inherited by an extension of circle, 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 to circle_area, identical in speed to writing circle_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, declared integer, 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 ifx compiler, 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_t will 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 constructorpoint2d(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 allocatable attribute — its size (or, for a string, its length) is not fixed in the type definition but is set at run time with allocate or by assignment. The allocatable arrays 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 = a would copy the pointer, not the data: b's pointer would end up aimed at the very same array as a's. Now b and a secretly 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 the stat= of every allocate so a failure is caught rather than trusted.

🔄 Check Your Understanding. 1. Why does b = a leave a unchanged when a has an allocatable component, but might not if a had a pointer component instead? 2. What happens to the storage of a local variable with an allocatable component when its procedure returns? 3. In new_field, the line f%name = name never calls allocate. How does f%name end up the right length?

Answers 1. Allocatable-component assignment deep-copies: b gets 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), leaving b aimed at a's data — change one, change both. 2. It is deallocated automatically — no leak, no explicit deallocate needed. 3. f%name is 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 semanticsb = 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.

  1. Our field_t holds real(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?

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

  2. 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 write b = a?

    AnswerAutomatic **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.

  3. heat_types declares private then public :: field_t. From Chapter 8, what does this pair achieve, and can code that does use heat_types still reach field_init directly?

    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.

  4. From Chapter 8, a module gives its procedures an explicit interface "for free." Why does that matter the moment field_init takes a class(field_t) and an allocatable-component-bearing object as arguments?

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