33 min read

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

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, and character — 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 the dp parameter 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/2 is 0 — on sight, and write code that never falls into it.
  • Reach for the intrinsic math functions, tell mod from modulo, and produce readable formatted output.
  • Add the first real code to the heat solver: a kinds module 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; the double precision keyword and the 1.0d0 literal in §3.2 are what you will see in F77 code, and the modern real(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 = 5 makes an integer, and a moment later x = "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 is x?" 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 for complex :: z = (3.0, 4.0)? 3. True or false: a character(len=5) variable can hold the string 'temperature'.

Answers (1) integer for the cell count (a whole number), real — really real(dp), once you finish §3.2 — for the temperature, and logical for 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 (plain real, plain integer) 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(...) and aimag(...) in §3.1 were intrinsics; selected_real_kind is 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 least p decimal digits of precision and a decimal exponent range of at least r (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 to r decimal digits.

With those in hand, we can define the single most important line of boilerplate in the book:

Definition (dp). Throughout this book, dp is our name for the double-precision real kind, defined once as integer, 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 as real(dp) and write real literals with a _dp suffix: 1.0_dp, 0.5_dp, 3.141592653589793_dp. The parameter attribute (§3.6) makes dp a 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 real without 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. Every real(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.1 cannot 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: use real(dp) and _dp literals 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 integer times a real, 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, is integerrealdouble precision (and on to higher-precision reals) → complex. So 2 * 3.0 first converts the integer 2 to the real 2.0, then multiplies, giving the real 6.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. So 7/2 is 3 (not 3.5), 1/2 is 0, and -7/2 is -3 (the -3.5 is truncated toward zero, not floored down to -4). The remainder is available separately through mod (§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? Of 5 * 2.0? Of 5 / 2? Of 5.0_dp / 2? 2. Your colleague writes real(dp) :: third = 1/3 and is baffled that third prints as 0.000. In one sentence, what happened, and what is the fix?

Answers (1) 5 * 2 is the integer 10. 5 * 2.0 is the real 10.0 (the 5 is promoted to real). 5 / 2 is the integer 2 (integer division truncates). 5.0_dp / 2 is 2.5_dp (the 2 is promoted to real(dp)). (2) 1/3 is evaluated in integer arithmetic first, giving 0, and only then converted to 0.0_dp; write 1.0_dp/3.0_dp (or real(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 / 9 is integer division and evaluates to 0 before the multiplication ever happens, so c is 0 * (f - 32) = 0.0 for any f. Fortran evaluates left to right at equal precedence, so 5 / 9 is computed first, in integer arithmetic. Because the two operands here are literal constants, gfortran's -Wall actually flags it — Warning: Integer division truncated to constant '0' — but that warning vanishes the instant the 5 and 9 come 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 for f = 98.6 gives 37.0. A defensive habit: write every constant in a floating-point formula with a decimal point and a _dp suffix, 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 / 2 is 3.5 (always real) and 7 // 2 is 3 (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 / 2 is -3 in Fortran but -7 // 2 is -4 in Python. The same split appears in the remainder functions — Fortran's mod matches C's % and Python's math.fmod (sign of the dividend), while Fortran's modulo matches 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 / nx where nx is an integer gives 0 for any nx > 1, even if dx is real(dp). average = sum / count with both sum and count integers truncates. theta = 2 * pi * i / n can lose the fraction if i and n are integers and 2 * pi doesn'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 in real(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**101024.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)*p takes the sign of the dividend a. So mod(-7, 3) is -1.
  • modulo(a, p) = a - floor(real(a)/real(p))*p takes the sign of the divisor p. So modulo(-7, 3) is 2.

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) and mod(10, 3) — do they agree? What about modulo(-10, 3) and mod(-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: both modulo(10,3) and mod(10,3) are 1. For the negative dividend they split: mod(-10, 3) is -1 (sign of -10), while modulo(-10, 3) is 2 (sign of 3). (2) s = sin(30.0_dp * pi / 180.0_dp) — convert degrees to radians first; the result is 0.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). The parameter attribute 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 a parameter costs nothing at run time. We have already used it for dp itself, which is a parameter of type integer.

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 parameter is a promise to the compiler, not just a note to the reader. A ! comment saying "don't change this" is advice; parameter is enforcement. Whenever a value is conceptually constant — a physical constant, a fixed array size, dp itself — declare it parameter and 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:

  • i for integers: i0 prints an integer in the minimum width it needs; i4 right-justifies it in a field four characters wide.
  • f for fixed-point reals: f8.3 means a field eight characters wide with three digits after the decimal point, so 21.5 prints as 21.500 (padded on the left to fill the width).
  • es for scientific notation: es12.4 prints 0.0001 as 1.0000E-04 — a mantissa between 1 and 10 times a power of ten, handy for very large or very small numbers.
  • a for character strings: it prints the text as-is.
  • 1x inserts 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 like 5f8.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, and a will format everything in Part I.

🔄 Check Your Understanding. 1. What does f7.2 print for the value 3.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.2 rounds to two decimals and right-justifies in seven characters: 3.141593.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 use es for 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 (integerrealdoublecomplex) 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.

  1. What does implicit none do, and why does this book call it the single most important habit in Fortran?

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

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

  3. What does the -Wall flag 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.