> — Richard Hamming, Numerical Methods for Scientists and Engineers
Prerequisites
- 1
- 2
Learning Objectives
- Declare variables of every Fortran intrinsic type (integer, real, double precision, complex, logical, character) and choose the appropriate one for a quantity.
- Use kind parameters and selected_real_kind to request portable precision, and define the dp parameter that the rest of this book relies on.
- Predict both the value and the type of any mixed-mode arithmetic expression, and never again fall into the integer-division trap.
- Apply Fortran's intrinsic math functions (sqrt, sin, cos, abs, mod, modulo, **) and explain how mod differs from modulo.
- Define named constants with parameter and produce clean, aligned output with a format string.
In This Chapter
Chapter 3: Variables, Types, and Arithmetic — How Fortran Thinks About Numbers
"The purpose of computing is insight, not numbers." — Richard Hamming, Numerical Methods for Scientists and Engineers
Overview
Every simulation you will ever write is, underneath, a very large number of very small arithmetic operations: add these two temperatures, multiply that velocity by this timestep, divide an energy by a mass. Get the arithmetic right and the physics follows; get it subtly wrong — divide two integers where you meant two reals, or run a decade-long climate integration in single precision — and the program compiles, runs, produces plausible-looking numbers, and lies to you. This chapter is about how Fortran represents numbers and how it computes with them, so that when your solver disagrees with reality you can be confident the disagreement is physics and not a type you declared carelessly on line 12.
In Chapter 2 you installed a compiler, wrote
program heat, and learned the one habit — implicit none — that forces you to declare every variable
before you use it. Now we make good on that promise: we declare variables, and to declare a variable you
must first answer the question Fortran cares about most, the question that a dynamically typed language like
Python lets you dodge forever. What kind of number is this? Fortran asks it once, at declaration, and then
holds you to the answer for the life of the variable — and that single strictness is a large part of why the
compiler can turn your code into some of the fastest numerical machine code on the planet.
In this chapter, you will learn to:
- Declare and use all six of Fortran's intrinsic types —
integer,real,double precision,complex,logical, andcharacter— and pick the right one deliberately. - Ask for the precision you need in a portable way, with kind parameters and
selected_real_kind, and define thedpparameter that will appear in nearly every program in this book. - Read any arithmetic expression and predict both its value and its type, including the mixed-mode rules that trip up every newcomer.
- Recognize the integer-division trap — why
1/2is0— on sight, and write code that never falls into it. - Reach for the intrinsic math functions, tell
modfrommodulo, and produce readable formatted output. - Add the first real code to the heat solver: a
kindsmodule and the plate's physical constants.
Learning Paths
How to read this chapter by track. - 🔬 Scientist ("I want my numbers to be right and fast") — this is a core chapter; read it straight through. §3.2 (precision) and §3.4 (integer division) are the two that will save you from silent, published-in-a-paper errors. - 📖 Standard — read all of it; every later chapter assumes fluent command of types, kinds, and
dp. - 🔧 Legacy ("I inherited old code") — read §3.1 and §3.2 closely; thedouble precisionkeyword and the1.0d0literal in §3.2 are what you will see in F77 code, and the modernreal(dp)beside them is what you will write. - ⚡ HPC ("I need it fast") — §3.2 has your Performance Note on single vs. double precision, which is one of the biggest and most under-appreciated levers on the speed of a memory-bound code.
3.1 The Intrinsic Types
Before a program can compute, it must decide what its numbers are. Fortran gives you six built-in, or
intrinsic, types, and every variable you declare is one of them (or, later, a type you build yourself
out of them). Five of the six will be familiar from other languages; one — complex — is a genuine luxury
that Fortran has had since 1957 and that most languages still make you simulate with a library.
Here they are, each in one sentence:
integer— a whole number, positive, negative, or zero, stored exactly. Loop counters, array indices, grid dimensions, step numbers.real— a floating-point number with a fractional part, stored approximately. Temperatures, velocities, physical quantities. By default this is single precision, about seven significant digits — a fact §3.2 will make you care about intensely.double precision— a real with roughly twice the digits (about fifteen). Historically a distinct keyword; today we get the same thing more flexibly through kinds (§3.2), but you will meet the keyword in older code and should recognize it.complex— a pair of reals treated as one number $a + b\,i$, with arithmetic that follows the rules of complex algebra. Indispensable in signal processing, quantum mechanics, and electrical engineering.logical— a truth value, either.true.or.false.Conditions, flags, masks.character— text: a single letter or a fixed-length string. Labels, filenames, messages.
A declaration names a type and then one or more variables of that type. Watch how each looks, and note that we assign initial values right in the declaration, which is legal and often tidy:
program intrinsic_types
implicit none
integer :: n_steps = 42
real :: ratio = 3.5
complex :: z = (3.0, 4.0)
logical :: is_ready = .true.
character(len=5) :: label = 'plate'
print '(a, i0)', 'n_steps = ', n_steps
print '(a, f6.3)', 'ratio = ', ratio
print '(a, l1)', 'is_ready = ', is_ready
print '(a, a)', 'label = ', label
print '(a, f0.1, a, f0.1, a)', 'z = ', real(z), ' + ', aimag(z), 'i'
end program intrinsic_types
$ gfortran -std=f2018 -Wall intrinsic_types.f90 -o types && ./types
n_steps = 42
ratio = 3.500
is_ready = T
label = plate
z = 3.0 + 4.0i
Several details in that small program repay attention. The character(len=5) says the string holds exactly
five characters — Fortran's classic strings are fixed-length, a constraint we lift in
Chapter 12; 'plate' happens
to be exactly five characters, so it fits without padding. The complex literal (3.0, 4.0) is the pair
(real part, imaginary part); the intrinsic real(z) extracts the real part and aimag(z) the imaginary
part, which is why the last line prints 3.0 + 4.0i. The logical prints as a bare T under the l1
descriptor. And every variable had to be declared, because implicit none (from
Chapter 2) is watching — comment it out and the
compiler would silently invent types for any name you forgot, which is the single most productive source of
bugs in the language's history.
💡 Intuition: think of a type as a contract about a box. When you write
integer :: n_steps, you promise the compiler this box will only ever hold whole numbers, and in return the compiler promises to store it compactly, compute with it exactly, and refuse — at compile time — to let you accidentally put a string in it. A dynamically typed language makes no such contract and pays for the freedom at run time, checking the type of every value on every operation. Fortran checks once, at compile time, and then runs flat out.🐍 Python Comparison: In Python you never declare a type:
x = 5makes an integer, and a moment laterx = "hello"cheerfully makes it a string. That flexibility is wonderful for a quick script and ruinous for a fast numerical loop, because the interpreter must re-check "what isx?" on every single operation. Fortran's variables have exactly one type for their whole lifetime, fixed the moment you declare them. This is not Fortran being primitive; it is Fortran refusing to do at run time what can be settled at compile time — the recurring bargain that makes the language fast.
Two of these types deserve a closer look before we move on. integer stores numbers exactly, but only
within a fixed range: a default Fortran integer is almost always 32 bits, which spans roughly
$-2.1\times 10^{9}$ to $+2.1\times 10^{9}$. Ask it to hold nine billion and it will silently overflow and
wrap around to a negative number — a hazard we will arm you against in
Chapter 13, and one
that §3.2 shows you how to avoid by requesting a wider integer. real, by contrast, stores numbers
approximately and over a vast range, trading exactness for reach — and how approximately is precisely
the subject of the next section, and the reason the innocuous keyword real is not, by itself, good enough
for serious work.
🔄 Check Your Understanding. 1. Which intrinsic type would you use for the number of grid cells along one edge of the plate? For the temperature at a cell? For a flag saying "has the simulation converged?" 2. What does
aimag(z)return forcomplex :: z = (3.0, 4.0)? 3. True or false: acharacter(len=5)variable can hold the string'temperature'.Answers
(1)integerfor the cell count (a whole number),real— reallyreal(dp), once you finish §3.2 — for the temperature, andlogicalfor the convergence flag. (2)4.0, the imaginary part. (3) False — it holds exactly five characters, so'temperature'would be truncated to'tempe'. Fixed-length strings are a FORTRAN 77 inheritance; Chapter 12 shows the modern deferred-length alternative.
3.2 Kinds and Portable Precision
Here is a claim that should worry you: the program above declared real :: ratio, and on essentially every
compiler that gives you a single-precision number with only about seven significant decimal digits. Run a
simulation for a million timesteps, accumulating a tiny rounding error at each one, and seven digits is not
enough — your answer can be wrong in the third significant figure, which in a scientific result is a
catastrophe. The fix is to use more precision. The question is how to ask for it in a way that will still
work when your code moves to a different compiler or a different machine, which scientific code always
eventually does.
The clumsy old answer was the double precision keyword, or literals like 1.0d0 (the d marks a
double-precision constant, the way e marks an exponent). These still work, and you will see them
everywhere in legacy code. But they are inflexible: "double" means one specific thing, and if tomorrow you
want quadruple precision, or you want to guarantee a minimum number of digits regardless of what the
hardware calls "double," the keyword cannot express your intent. Modern Fortran replaces it with something
better: kinds.
Definition (kind parameter). Every intrinsic type comes in one or more kinds — variants that differ in how many bytes they occupy and therefore in their range and precision. A kind parameter is an integer that selects one of them. You write it in parentheses after the type:
real(dp),integer(i64). The default kind (plainreal, plaininteger) is whatever the compiler chose as its default, which is exactly the thing you do not want to depend on in portable numerical code.
The crucial move is to never write a kind number literally — real(8) may mean double precision on
gfortran and something else, or nothing at all, elsewhere. Instead you describe the precision you need and
let the compiler hand you a kind that satisfies it. The tool for that is an intrinsic function.
Definition (intrinsic function). An intrinsic function is a function built into the language itself — always available, never needing to be written by you or imported from a library.
real(...)andaimag(...)in §3.1 were intrinsics;selected_real_kindis another; and §3.5 is entirely about the intrinsic math functions. When you call one, you are calling code the compiler supplies and, often, knows how to optimize specially.Definition (
selected_real_kind).selected_real_kind(p, r)is the intrinsic function that returns a real kind parameter guaranteeing at leastpdecimal digits of precision and a decimal exponent range of at leastr(that is, it can represent numbers up to about $10^{r}$). If no available kind can meet the request it returns a negative number, so a negative result is a portable "this machine can't do that."selected_int_kind(r)is its integer cousin: it returns an integer kind that can hold every value with up tordecimal digits.
With those in hand, we can define the single most important line of boilerplate in the book:
Definition (
dp). Throughout this book,dpis our name for the double-precision real kind, defined once asinteger, parameter :: dp = selected_real_kind(15, 307)— at least 15 significant digits and a range out to $10^{307}$, which is exactly IEEE 754 double precision on every mainstream system. We then declare reals asreal(dp)and write real literals with a_dpsuffix:1.0_dp,0.5_dp,3.141592653589793_dp. Theparameterattribute (§3.6) makesdpa named constant fixed at compile time. This one habit — never rely on default real precision in numerical code — is non-negotiable house style, and you will thank yourself the first time your results reproduce on a colleague's machine.
Let us confirm what these requests actually deliver. The intrinsics precision(x) and range(x) report the
decimal digits and exponent range of a variable's kind, and kind(x) reports the raw kind number — useful
only for seeing that the numbers are, as promised, not something you should hardcode:
program kinds_demo
use, intrinsic :: iso_fortran_env, only: real64
implicit none
integer, parameter :: dp = selected_real_kind(15, 307)
real(dp) :: x = 0.1_dp
print '(a, i0)', 'kind(dp) = ', dp
print '(a, i0)', 'real64 = ', real64
print '(a, i0)', 'precision default = ', precision(1.0)
print '(a, i0)', 'precision dp = ', precision(x)
print '(a, i0)', 'range dp = ', range(x)
end program kinds_demo
$ gfortran -std=f2018 -Wall kinds_demo.f90 -o kinds && ./kinds
kind(dp) = 8
real64 = 8
precision default = 6
precision dp = 15
range dp = 307
Read that output as a story. The kind number is 8 on gfortran (it is the byte count) — but that 8 is a
compiler-specific detail, which is the whole reason we asked for selected_real_kind(15, 307) instead of
writing real(8) and praying. Default real gives a precision of 6 digits; our dp gives 15, with an
exponent range of 307, exactly as requested. Those last three numbers are the ones that matter, and they
are portable because we described them rather than hardcoded them.
There is an even shorter road to the same destination, and you will see both in the wild. The intrinsic
module iso_fortran_env exports ready-made kind parameters named for their bit width: real32, real64,
real128, and int8/int16/int32/int64. Here real64 is 8 — the same kind our selected_real_kind
call produced. Many modern codes simply write use iso_fortran_env, only: dp => real64 and are done. Both
styles are correct house style; this book leans on the explicit selected_real_kind(15, 307) because it
states the scientific requirement — "I need fifteen digits" — rather than the representation — "I need
sixty-four bits" — and the requirement is what you actually care about.
The integer side works the same way. When 32 bits are not enough — counting particles in a large simulation, say, where you might exceed two billion — you request a wider integer by its digit count:
integer, parameter :: i64 = selected_int_kind(18) ! at least 18 decimal digits
integer(i64) :: particle_count = 9000000000_i64
A default integer cannot hold nine billion (it tops out near $2.1\times 10^{9}$), but i64 — which
selected_int_kind(18) gives you, a 64-bit integer good to about $9.2\times 10^{18}$ — holds it easily. Note
the _i64 suffix on the literal, the integer analogue of _dp: it says "this constant is of kind i64,"
which matters because 9000000000 written without a kind would itself overflow the default integer type
before it was ever assigned.
🚪 Threshold Concept. Precision is a decision you make, not a default you accept. The moment you internalize that
realwithout a kind is a specific, limited, seven-digit thing — and that a serious numerical program should say what precision it needs and get exactly that, portably — you stop writing code that "works on my machine" and start writing code that works. Everyreal(dp)in this book is that decision, made once and honored everywhere.⚡ Performance Note: Precision is not free, and the cost runs the opposite way from what beginners expect — single precision is often the faster choice, when you can afford its accuracy. A single-precision real is 4 bytes; a double is 8. Most serious numerical codes are memory-bandwidth-bound — the processor spends its time waiting for numbers to arrive from memory, not doing arithmetic — so halving the bytes can nearly double the speed of a bandwidth-limited loop. On top of that, a SIMD vector register (Chapter 27) holds twice as many singles as doubles, so vectorized single-precision code does twice the arithmetic per instruction. The reason this book still defaults to double is that scientific accuracy usually demands it; but choosing precision deliberately, weighing accuracy against this real speed cost, is exactly the kind of decision a computational scientist is paid to make. We return to it in Chapter 20.
🔗 Connection: This section introduces how to ask for precision, but it deliberately stops short of why floating-point numbers are approximate in the first place — why
0.1cannot be stored exactly, what machine epsilon is, when subtraction quietly destroys your digits. That is the domain of Chapter 20, which owns IEEE 754 in full. For now, one rule carries you a long way: usereal(dp)and_dpliterals for every real quantity in numerical code, and the precision question is settled until you reach Part V.
3.3 Arithmetic and Mixed-Mode Expressions
The five arithmetic operators are the ones you expect — +, -, *, /, and ** for exponentiation —
and they obey the usual precedence: exponentiation first, then multiplication and division, then addition
and subtraction, with parentheses overriding everything. Exponentiation associates right-to-left, so
2**3**2 is 2**(3**2) = 2**9 = 512, not (2**3)**2 = 64 — a rare case worth knowing but rarer still
worth relying on; parenthesize when in doubt.
What genuinely needs your attention is not the operators but the types they act on. Fortran lets you mix types in a single expression, and the rules for what happens are simple, fixed, and the source of the most famous beginner bug in the language.
Definition (mixed-mode arithmetic). An expression whose operands have different types — an
integertimes areal, say — is mixed-mode. For each binary operation, Fortran converts the "lower" operand up to the type of the "higher" one, then computes in that higher type. The ranking, from lower to higher, isinteger→real→double precision(and on to higher-precision reals) →complex. So2 * 3.0first converts the integer2to the real2.0, then multiplies, giving the real6.0.
The word each in that definition is the one to underline, and it leads directly to the trap. Conversion happens per operation, based only on that operation's two operands — Fortran does not look ahead to see where the result is going. Consider what that means for a division of two integers. Both operands are integers, so the operation is integer arithmetic, and the result is an integer — before anything downstream, including an assignment to a real variable, ever gets a say.
Definition (integer division). When both operands of
/are integers, Fortran performs integer division: it computes the mathematical quotient and then discards any fractional part, truncating toward zero. So7/2is3(not3.5),1/2is0, and-7/2is-3(the-3.5is truncated toward zero, not floored down to-4). The remainder is available separately throughmod(§3.5).
Here is the whole story in one program — the trap, and the two ways out of it:
program arithmetic
implicit none
! In real projects dp lives in a module (see the Project Checkpoint); here it
! is local so the program stands alone.
integer, parameter :: dp = selected_real_kind(15, 307)
integer :: a = 1, b = 2, m = 7, n = 3
real(dp) :: wrong, right1, right2
! --- The integer-division trap ---
wrong = a / b ! 1/2 in INTEGER arithmetic = 0, then -> 0.0
right1 = real(a, dp) / real(b, dp) ! convert first -> real division
right2 = 1.0_dp / 2.0_dp ! real literals -> real division
print '(a, f6.3)', 'a / b (integer) = ', wrong
print '(a, f6.3)', 'real(a)/real(b) (real) = ', right1
print '(a, f6.3)', '1.0_dp / 2.0_dp (real) = ', right2
! --- Integer division and the two remainder functions (m = 7, n = 3) ---
print '(a, i0)', 'm / n = ', m / n
print '(a, i0)', 'mod(m, n) = ', mod(m, n)
print '(a, i0)', 'mod(-m, n) = ', mod(-m, n)
print '(a, i0)', 'modulo(-m, n) = ', modulo(-m, n)
! --- A couple of intrinsic math functions (more in Section 3.5) ---
print '(a, f10.6)', 'sqrt(2.0_dp) = ', sqrt(2.0_dp)
print '(a, f10.6)', 'abs(-3.5_dp) = ', abs(-3.5_dp)
end program arithmetic
$ gfortran -std=f2018 -Wall arithmetic.f90 -o arith && ./arith
a / b (integer) = 0.000
real(a)/real(b) (real) = 0.500
1.0_dp / 2.0_dp (real) = 0.500
m / n = 2
mod(m, n) = 1
mod(-m, n) = -1
modulo(-m, n) = 2
sqrt(2.0_dp) = 1.414214
abs(-3.5_dp) = 3.500000
Look hard at the first line. We assigned the result to a real(dp) variable, and still it printed 0.000,
because a / b was computed entirely in integer arithmetic — yielding 0 — and only then converted to
0.0_dp for the assignment. The real variable on the left could not save it; the damage was done on the
right, before the = was reached. The two fixes both make sure at least one operand is real before the
division happens: real(a, dp) converts the integer a to a real(dp), making the whole division
real-valued; or you write the literals as reals in the first place. This is the crux of mixed-mode
arithmetic, and it is worth the paragraph.
🔄 Check Your Understanding. 1. What is the value and type of
5 * 2? Of5 * 2.0? Of5 / 2? Of5.0_dp / 2? 2. Your colleague writesreal(dp) :: third = 1/3and is baffled thatthirdprints as0.000. In one sentence, what happened, and what is the fix?Answers
(1)5 * 2is the integer10.5 * 2.0is the real10.0(the5is promoted to real).5 / 2is the integer2(integer division truncates).5.0_dp / 2is2.5_dp(the2is promoted toreal(dp)). (2)1/3is evaluated in integer arithmetic first, giving0, and only then converted to0.0_dp; write1.0_dp/3.0_dp(orreal(1,dp)/3.0_dp).
3.4 The Integer-Division Trap
Integer division is not a bug in Fortran; it is a feature, and a necessary one — array indices, loop
counters, and "how many whole buckets fit" questions all want integer answers. The trap is not that
integer division exists. The trap is forgetting which division you are in, because the operators look
identical and only the operand types tell you what will happen. This deserves its own section because it is,
by a wide margin, the most common wrong-answer bug that Fortran beginners ship. Unlike a type mismatch, it
usually slips past the compiler in silence: sum / count with two integer variables is perfectly legal
code that simply does not mean what you thought, and no warning fires. (gfortran's -Wall does catch the
narrow special case where you divide two literal constants — we will see it below — but that safety net
disappears the moment a variable is involved, which is nearly always.)
The textbook example is a temperature conversion. To turn Fahrenheit into Celsius you compute $C = \frac{5}{9}(F - 32)$. Transcribe that formula literally and you get a program that compiles, runs, and reports that every temperature on Earth is exactly $0^{\circ}$C:
🐛 Find the Bug. This is meant to convert Fahrenheit to Celsius. Why does it return the same wrong answer —
0.0— for every input?
fortran real(dp) :: f = 98.6_dp, c c = 5 / 9 * (f - 32.0_dp) ! body temperature: expect 37.0
Diagnosis and fix
5 / 9is integer division and evaluates to0before the multiplication ever happens, socis0 * (f - 32) = 0.0for anyf. Fortran evaluates left to right at equal precedence, so5 / 9is computed first, in integer arithmetic. Because the two operands here are literal constants, gfortran's-Wallactually flags it —Warning: Integer division truncated to constant '0'— but that warning vanishes the instant the5and9come from variables, which is how the bug reaches production. The fix is to make the fraction real:c = 5.0_dp / 9.0_dp * (f - 32.0_dp), which forf = 98.6gives37.0. A defensive habit: write every constant in a floating-point formula with a decimal point and a_dpsuffix, so no integer can sneak into the arithmetic.
Why does Fortran do this, when a language like Python 3 makes 5 / 9 give 0.5555... and reserves a
separate // operator for the truncating kind? Because Fortran predates that convention by decades, and
because for the numerical work it was built for, integer-in/integer-out is the mathematically honest result:
the integers are a closed system under division-with-remainder, and hiding that by silently producing a real
would be its own kind of lie. The cost is that the burden of knowing your operand types falls on you. Pay it
gladly; it is a small tax for a language that never surprises you about what type a result is.
🐍 Python Comparison: The polyglot reader should hold three behaviors in mind at once. In Python 3,
7 / 2is3.5(always real) and7 // 2is3(floor division). Fortran's integer/is closest to Python's//, but with a subtle and important difference: Fortran truncates toward zero while Python's//floors toward negative infinity. So-7 / 2is-3in Fortran but-7 // 2is-4in Python. The same split appears in the remainder functions — Fortran'smodmatches C's%and Python'smath.fmod(sign of the dividend), while Fortran'smodulomatches Python's%operator (sign of the divisor). §3.5 lays this out in a table; the lesson here is that "integer division" is not one universal thing, so port arithmetic between languages with your eyes open.⚠️ Common Pitfall: The trap loves to hide inside an otherwise real expression.
dx = 1 / nxwherenxis an integer gives0for anynx > 1, even ifdxisreal(dp).average = sum / countwith bothsumandcountintegers truncates.theta = 2 * pi * i / ncan lose the fraction ifiandnare integers and2 * pidoesn't happen to sit first. The rule that never fails: for a real result, make sure at least one operand of every/is real — by writing a real literal, or by wrapping an integer inreal(k, dp).
3.5 Intrinsic Math Functions
Because Fortran was built for numerical work, its mathematical toolbox is built in — no #include, no
import math, no library to link. The intrinsic functions are simply there, and the compiler often knows
how to implement them with special hardware instructions. You have already met a few (real, sqrt,
abs); here is the working set for scientific arithmetic, all of which accept real(dp) arguments and
return real(dp) results (except where noted):
| Function | Meaning | Example → result |
|---|---|---|
sqrt(x) |
square root | sqrt(2.0_dp) → 1.41421356... |
abs(x) |
absolute value | abs(-3.5_dp) → 3.5; for complex, the modulus |
sin(x), cos(x), tan(x) |
trig, argument in radians | sin(0.0_dp) → 0.0 |
exp(x), log(x), log10(x) |
$e^x$, natural log, base-10 log | exp(0.0_dp) → 1.0 |
x ** y |
exponentiation | 2.0_dp**10 → 1024.0 |
mod(a, p) |
remainder, sign of a (the dividend) |
mod(-7, 3) → -1 |
modulo(a, p) |
remainder, sign of p (the divisor) |
modulo(-7, 3) → 2 |
max(...), min(...) |
largest / smallest of the arguments | max(3, 7, 1) → 7 |
Two points guard against real mistakes. First, the trig functions take radians, not degrees; feeding
sin a value in degrees is a classic wrong-answer bug, so convert with radians = degrees * pi / 180.0_dp.
(Fortran 2023 adds degree variants like sind; we note them in
Chapter 39, but radians are
the portable default.) Second, mod and modulo agree for positive arguments and disagree for negative
ones, and the difference is exactly the truncate-vs-floor distinction from §3.4:
mod(a, p) = a - int(a/p)*ptakes the sign of the dividenda. Somod(-7, 3)is-1.modulo(a, p) = a - floor(real(a)/real(p))*ptakes the sign of the divisorp. Somodulo(-7, 3)is2.
For clock-like wraparound — "which grid column is index i in a periodic domain of width n?" — you almost
always want modulo, because it returns a result in [0, n) even for negative i. Reach for mod when you
genuinely want the sign to follow the dividend. Mixing them up is a subtle bug that only shows up at the
edges, which is where simulations tend to go wrong.
Here is a compact program exercising the math functions, with every output computed by hand:
program math_functions
implicit none
integer, parameter :: dp = selected_real_kind(15, 307)
real(dp), parameter :: pi = 3.141592653589793_dp
print '(a, f12.6)', 'sqrt(2.0_dp) = ', sqrt(2.0_dp)
print '(a, f12.6)', 'sin(pi/6) = ', sin(pi / 6.0_dp)
print '(a, f12.6)', 'cos(pi/3) = ', cos(pi / 3.0_dp)
print '(a, f12.6)', '2.0_dp**10 = ', 2.0_dp**10
end program math_functions
$ gfortran -std=f2018 -Wall math_functions.f90 -o mathf && ./mathf
sqrt(2.0_dp) = 1.414214
sin(pi/6) = 0.500000
cos(pi/3) = 0.500000
2.0_dp**10 = 1024.000000
Those middle two lines are quietly instructive. Mathematically $\sin(\pi/6)$ and $\cos(\pi/3)$ are exactly
$0.5$, and to six printed digits they are 0.500000. But pi here is a stored approximation of an
irrational number, so the computed results differ from $0.5$ far out in the fifteenth digit — a difference
invisible at f12.6 but very much present, and exactly the kind of thing
Chapter 20 will teach you to reason
about. For now, notice that sqrt(2.0_dp) printed 1.414214: the sixth decimal is a 4 because the true
value 1.4142135... rounds up at that place. Getting output like this exactly right, by hand, is a skill
this book insists on — because a worked example whose output is wrong is worse than none at all.
🔄 Check Your Understanding. 1.
modulo(10, 3)andmod(10, 3)— do they agree? What aboutmodulo(-10, 3)andmod(-10, 3)? 2. You have an angle of 30 degrees and want its sine. Write the one line that computes it correctly.Answers
(1) For positive arguments they agree: bothmodulo(10,3)andmod(10,3)are1. For the negative dividend they split:mod(-10, 3)is-1(sign of-10), whilemodulo(-10, 3)is2(sign of3). (2)s = sin(30.0_dp * pi / 180.0_dp)— convert degrees to radians first; the result is0.5.
3.6 Named Constants with parameter
Physical constants, grid dimensions, conversion factors — a simulation is full of numbers that are fixed for
the life of the run and should never be accidentally overwritten. Hardcoding them as bare literals scattered
through the code (3.141592653589793 here, 9.81 there) is a maintenance and correctness hazard: change
one and you must find them all, and a mistyped digit in one copy is a silent bug. Fortran's answer is the
named constant.
Definition (
parameter). Theparameterattribute declares a named constant: a name bound to a value that is fixed at compile time and can never be reassigned. You write it as part of the declaration —real(dp), parameter :: pi = 3.141592653589793_dp— and any attempt to assign to it later is a compile-time error, not a run-time surprise. Because the value is known at compile time, the compiler can fold it directly into the generated code, so aparametercosts nothing at run time. We have already used it fordpitself, which is aparameterof typeinteger.
Named constants can be built from other named constants, as long as the values are all known at compile time:
program constants
implicit none
integer, parameter :: dp = selected_real_kind(15, 307)
real(dp), parameter :: pi = 3.141592653589793_dp
real(dp), parameter :: two_pi = 2.0_dp * pi ! derived from another parameter
real(dp) :: radius = 2.0_dp
print '(a, f10.6)', 'pi = ', pi
print '(a, f10.6)', 'two_pi = ', two_pi
print '(a, f10.6)', 'circumference = ', two_pi * radius
end program constants
$ gfortran -std=f2018 -Wall constants.f90 -o consts && ./consts
pi = 3.141593
two_pi = 6.283185
circumference = 12.566371
two_pi is computed once, at compile time, from pi; radius is an ordinary variable, so two_pi * radius
is computed at run time when the circumference is printed. The output confirms the arithmetic:
$2\pi \approx 6.283185$ and $2\pi \times 2 \approx 12.566371$ (the printed digits are rounded to six places,
which is why $\pi$ shows as 3.141593, rounding up at the sixth decimal).
The parameter attribute is also a correctness tool, not just a tidiness one. Declaring pi a
parameter means the compiler will physically stop you from writing pi = 3.0 by mistake fifty lines
later — it becomes an error the build catches, not a bug the physics catches. This is the same philosophy as
implicit none and intent (which you will meet in
Chapter 6): push errors as early as possible, ideally to compile time,
where they are cheapest to fix.
💡 Intuition: a
parameteris a promise to the compiler, not just a note to the reader. A!comment saying "don't change this" is advice;parameteris enforcement. Whenever a value is conceptually constant — a physical constant, a fixed array size,dpitself — declare itparameterand let the compiler hold you to it.
3.7 Formatted Output Basics
You have been reading format strings like '(a, f6.3)' all chapter; now let us name their parts, lightly.
The full treatment of formatted input and output belongs to
Chapter 7, which owns the topic; here we cover just enough to make your
programs print cleanly.
A print (or write(*, ...), which is the same thing writing to the screen) can take a format string: a
parenthesized list of edit descriptors, each describing how to render one item. The handful you need now:
ifor integers:i0prints an integer in the minimum width it needs;i4right-justifies it in a field four characters wide.ffor fixed-point reals:f8.3means a field eight characters wide with three digits after the decimal point, so21.5prints as21.500(padded on the left to fill the width).esfor scientific notation:es12.4prints0.0001as1.0000E-04— a mantissa between 1 and 10 times a power of ten, handy for very large or very small numbers.afor character strings: it prints the text as-is.1xinserts one space;/starts a new line.
Put together:
program formatting
implicit none
integer, parameter :: dp = selected_real_kind(15, 307)
real(dp) :: temp = 21.5_dp
integer :: step = 7
print '(a, i0, a, f6.2)', 'step ', step, ': T = ', temp
print '(a, es12.4)', 'alpha = ', 1.0e-4_dp
print '(f8.3)', temp
end program formatting
$ gfortran -std=f2018 -Wall formatting.f90 -o fmt && ./fmt
step 7: T = 21.50
alpha = 1.0000E-04
21.500
Trace the first line: 'step ' prints literally, i0 prints 7 in minimal width, ': T = ' prints
literally, and f6.2 prints 21.5 as 21.50 — five characters 21.50 right-justified in a six-wide
field, hence the leading space that yields T = 21.50 with two spaces. If a number does not fit its field —
try to print 1024.0 with f6.2, which needs seven characters — Fortran fills the field with asterisks
(******) rather than print a misleading truncated number. That asterisk-fill is a feature: it makes a
too-narrow field loud, so you notice and widen it.
🔗 Connection: This is deliberately a sketch. Edit descriptors have many more forms —
e,g,d, repeat counts like5f8.2, and the rules for how a format cycles when it runs out of descriptors — and Chapter 7 develops all of them, along with reading input and writing to files. For now,i0,f,es, andawill format everything in Part I.🔄 Check Your Understanding. 1. What does
f7.2print for the value3.14159? For-3.14159? 2. Why might a program print a column of******where you expected numbers, and what is the fix?Answers
(1)f7.2rounds to two decimals and right-justifies in seven characters:3.14159→3.14(three leading spaces), and-3.14159→-3.14(the minus sign occupies a character, so two leading spaces). (2) The values are too wide for the field, so Fortran fills it with asterisks rather than print a wrong number; widen the field (e.g.,f10.2) or useesfor numbers of unpredictable magnitude.
Project Checkpoint
Time to add the first real code to the heat solver. In
Chapter 1 you chose your domain and wrote a problem statement; in
Chapter 2 you compiled a program heat that printed
a banner. This chapter gives the project its numerical foundation: a kinds module that defines dp once
for the whole codebase, and the plate's physical constants declared as real(dp) parameters. The
temperature itself will become a two-dimensional real(dp) array in
Chapter 5, where arrays — Fortran's superpower — take center stage; here we
settle what a single such value is and what precision it carries.
We have written integer, parameter :: dp = selected_real_kind(15, 307) at the top of several programs in
this chapter. That repetition is a smell: dp should be defined in exactly one place and used
everywhere. A module is Fortran's tool for that — you will study modules properly in
Chapter 8, but the pattern is simple
enough to adopt now. Put this in a file kinds.f90, and the physical constants in the driver that uses it:
module kinds
implicit none
private
public :: dp
integer, parameter :: dp = selected_real_kind(15, 307)
end module kinds
program checkpoint
use kinds, only: dp
implicit none
! Physical setup for the plate (illustrative values, all real(dp) constants).
real(dp), parameter :: alpha = 1.0e-4_dp ! thermal diffusivity, m^2/s
real(dp), parameter :: length = 1.0_dp ! plate side length, m
integer, parameter :: nx = 101 ! grid points along a side
real(dp), parameter :: dx = length / real(nx - 1, dp) ! grid spacing, m
print '(a, es10.3)', 'alpha (m^2/s) = ', alpha
print '(a, f8.5)', 'dx (m) = ', dx
print '(a, es10.3)', 'dx^2 (m^2) = ', dx**2
end program checkpoint
$ gfortran -std=f2018 -Wall project-checkpoint.f90 -o checkpoint && ./checkpoint
alpha (m^2/s) = 1.000E-04
dx (m) = 0.01000
dx^2 (m^2) = 1.000E-04
Three things are worth savoring. The kinds module declares dp public and everything else private, so
the only name it exports is dp — a clean, single-purpose module you will never touch again but will
use in every file for the rest of the book. The grid spacing dx is computed with real(nx - 1, dp), not
nx - 1, because length / (nx - 1) with an integer denominator would be integer division — exactly the
§3.4 trap, defused by converting the denominator to real(dp) first. And the constants carry their physical
meaning in comments and units, which is not decoration: a diffusivity without units is a bug waiting to
happen.
The numbers check out by hand. With nx = 101 grid points there are 100 intervals, so dx = 1.0 / 100 =
0.01 metres, printed as 0.01000. Then dx**2 = 0.0001 = 1.000E-04 square metres — a quantity that will
matter enormously in Chapter 24,
because the stable timestep of an explicit heat solver scales with dx**2 / alpha. You are not computing the
timestep yet, but you are laying down the constants it will be built from. This kinds.f90 is the first
permanent brick of the solver; it survives, unchanged, all the way to the Chapter 38 capstone.
Summary
This chapter taught how Fortran represents numbers and computes with them — the bedrock every later chapter stands on.
| Idea | The short version |
|---|---|
| Six intrinsic types | integer (exact whole numbers), real (approximate, single precision by default), double precision (legacy 15-digit real), complex (a pair $a+bi$), logical (.true./.false.), character (fixed-length text). |
| Kinds | A kind parameter selects a type's precision/size. Never hardcode kind numbers; request precision with selected_real_kind(p, r) (and selected_int_kind(r)). |
dp |
integer, parameter :: dp = selected_real_kind(15, 307) — the book's double-precision kind. Declare reals real(dp); write literals 1.0_dp. |
| Mixed-mode | In an expression of mixed types, each operation converts up to the higher type (integer→real→double→complex) before computing. Type is decided by operands, not by where the result goes. |
| Integer division | int / int truncates toward zero: 1/2 == 0, -7/2 == -3. For a real result, make at least one operand real (real(a, dp) or a _dp literal). |
mod vs modulo |
mod takes the sign of the dividend; modulo takes the sign of the divisor. They differ only for negative arguments; modulo is what you want for periodic wraparound. |
parameter |
A compile-time named constant; reassigning it is a compile error. Zero run-time cost. Use it for dp, physical constants, and fixed sizes. |
| Formatting | i0/i4 for integers, f8.3 for fixed-point, es12.4 for scientific, a for text; a too-narrow field prints *s. Full story in Chapter 7. |
The three things to memorize. First, 1/2 is 0 — integer division truncates, and the type of an
expression is decided before the result is used. Second, dp = selected_real_kind(15, 307), declared once in
a kinds module, real(dp) everywhere, _dp on every real literal. Third, the trig intrinsics take
radians, and mod and modulo differ on negative numbers.
Spaced Review
Three questions revisiting Chapter 2. Answer from memory before opening the details.
-
What does
implicit nonedo, and why does this book call it the single most important habit in Fortran?
Answer
It switches off Fortran's legacy *implicit typing*, under which any undeclared variable is silently given a type based on the first letter of its name. With `implicit none`, every variable must be explicitly declared, so a typo like `temperatrue` becomes a compile error instead of a new, silently-zero variable — catching a whole class of bugs at compile time. It is why this chapter could insist you declare a type for everything. -
Name the three stages of the compile–link–run cycle and what each one produces.
Answer
*Compile* turns source (`.f90`) into an object file (`.o`) of machine code with unresolved references; *link* combines object files and libraries into a single executable; *run* loads and executes that executable. `gfortran file.f90 -o prog` does the compile and link in one command. -
What does the
-Wallflag ask of the compiler, and why turn it on from your very first program?
Answer
`-Wall` enables *all* the common warnings — unused variables, suspicious conversions, uninitialized values — which flag likely mistakes that are still technically legal code. Turning it on from day one (alongside `-std=f2018` for standard-conformance and, during development, `-fcheck=all`) means the compiler is helping you find bugs before they run.
What's Next
You can now declare numbers, compute with them correctly, and print them. What you cannot yet do is make the
program decide or repeat — and a simulation is nothing but decisions repeated:
if a cell is on the boundary, hold it fixed; for each timestep, update every interior cell.
Chapter 4 is about control flow — if, select case, and the do
loop — the constructs that turn a straight-line calculation into an algorithm. With them, the heat solver
gets its time-stepping skeleton: a do loop over steps, with an if guarding the fixed edges. Let's teach
the program to make decisions.