> *"The quality of programmers is a decreasing function of the density of go to statements in the programs
Prerequisites
- 2
- 3
Learning Objectives
- Write `if … then … else if … else … end if` constructs, and build conditions from relational (`==`, `<`, `>=`) and logical (`.and.`, `.or.`, `.not.`) operators.
- Replace a chain of nested `if`s with a clearer `select case`, using single values, value lists, and ranges — and explain why the compiler prefers it.
- Choose and write the right `do` loop for a task — counted, `do while`, or an infinite `do` terminated by `exit` — and steer iterations with `cycle` and named constructs.
- Perform a masked whole-array assignment with `where`, and express an order-independent loop with `do concurrent`, recognizing why `forall` is now obsolescent.
- Translate control flow between Python, C, and Fortran, and assemble the heat solver's time-stepping loop skeleton with a boundary-vs-interior `if`.
In This Chapter
- Overview
- Learning Paths
- 4.1 Making a Decision: if … then … else … end if
- 4.2 select case: One Value, Many Branches
- 4.3 do Loops: Counted, do while, and Infinite-with-exit
- 4.4 cycle, exit, Named Loops, and Nesting
- 4.5 where: Your First Taste of Whole-Array Thinking
- 4.6 do concurrent vs. the Obsolescent forall
- 4.7 Side by Side: Control Flow in Python, C, and Fortran
- Project Checkpoint
- Summary
- Spaced Review
- What's Next
Chapter 4: Control Flow — IF, SELECT CASE, DO Loops, and Making Decisions
"The quality of programmers is a decreasing function of the density of go to statements in the programs they produce." — Edsger W. Dijkstra, "Go To Statement Considered Harmful" (1968)
Overview
So far your programs have run straight down the page: every statement executed once, in order, top to bottom. Real programs do not behave that way, and numerical programs least of all. A simulation decides — is this grid point on the boundary or in the interior? — and it repeats — march the solution forward a hundred thousand time steps. Deciding and repeating are what control flow is: the constructs that let a program choose which statements to run and how many times to run them. In a scientific code, control flow is not a supporting character. The loop is where the computation lives. When someone tells you a weather model runs for six hours on ten thousand cores, they are telling you about a loop.
Fortran's control-flow constructs are, on the surface, unremarkable — every language has an if and a
loop, and you already know what they mean from Python or C. What is worth your attention is the style:
modern Fortran gives you clean, block-structured constructs with no goto in sight, a select case that is
safer than C's switch, loop constructs you can name and steer, and — uniquely — two constructs, where
and do concurrent, that begin to express computation over whole arrays at once rather than one element
at a time. Those last two are your first glimpse of the idea that makes Fortran fast, and we will only crack
the door here; Chapter 5 throws it wide open.
This is also the chapter where the running project stops being a decision and starts being a program with a shape. By the end you will have written the time-stepping skeleton of your heat solver: a loop over time steps, and inside it a sweep across the plate that distinguishes edge points from interior points. There is no physics in it yet — the real numerics arrive in Chapter 24 — but the control flow it hangs on is exactly the control flow the finished solver will use.
In this chapter, you will learn to:
- Make decisions with the
ifconstruct, building conditions from relational and logical operators, and recognize the traps (comparing reals for exact equality; the danglingelse). - Use
select caseto dispatch on a value cleanly, and say precisely why it beats a tower of nestedifs. - Write all three kinds of
doloop — counted,do while, and the infinitedoended byexit— and pick the right one. - Redirect a loop mid-flight with
cycle, break out of the right loop with a named construct, and reason about nested loops. - Assign to part of an array through a mask with
where, and loop without ordering constraints usingdo concurrent— meeting, for the first time, the array-and-parallel thinking that the rest of the book develops.
Learning Paths
How to read this chapter by track. - 🔬 Scientist ("my Python is too slow") — §4.3 and §4.5 are your core: loops are your computation, and
whereis your first whole-array tool. Skim §4.2. - 📖 Standard — read straight through; this is foundational and short. - 🔧 Legacy ("I inherited old code") — note the📜 From Historyboxes: thegoto, arithmeticif, andforallyou will meet in old code are defined here and dismantled in Chapter 17. - ⚡ HPC ("I need parallel code") — §4.6 (do concurrent) is the seed of everything in Part VII; read it closely, then hold the thought until Chapter 29.
4.1 Making a Decision: if … then … else … end if
The most basic decision a program makes is this or that, and Fortran spells it with the if
construct: the keyword if, a condition in parentheses that evaluates to .true. or .false., the word
then, a block of statements, and a closing end if.
Definition (the
ifconstruct). A block of the formif (condition) then … end ifthat executes its enclosed statements only when the logicalconditionis true. It may include any number ofelse if (condition) thenbranches, tested in order, and an optional finalelsebranch that runs when none of the conditions held. Exactly one branch (at most) executes.
The condition is a logical expression — something that is true or false. You build logical expressions from two families of operators. The relational operators compare two values of the same kind:
| Operator | Meaning | Example (true when…) |
|---|---|---|
== |
equal to | n == 0 |
/= |
not equal to | n /= 0 |
< |
less than | x < 1.0_dp |
<= |
less than or equal | x <= 1.0_dp |
> |
greater than | x > 0.0_dp |
>= |
greater than or equal | x >= 0.0_dp |
The logical operators combine logical values: .and. (both true), .or. (either true), .not.
(negation), and the less common .eqv. / .neqv. (logical equivalence / exclusive-or). The dots are part
of the spelling — Fortran writes .and., not &&. Both operands and the result are of the logical type
you met in Chapter 3.
Here is the construct in full, classifying a temperature into a phase of water and, separately, flagging
whether it falls in a comfortable range — a combined condition built with .and.:
program phase_states
use, intrinsic :: iso_fortran_env, only: dp => real64
implicit none
real(dp) :: temps(5) = [-12.0_dp, 0.0_dp, 25.0_dp, 42.0_dp, 105.0_dp]
integer :: i
real(dp) :: t
logical :: comfortable
do i = 1, 5
t = temps(i)
comfortable = (t >= 10.0_dp .and. t <= 40.0_dp)
if (t < 0.0_dp) then
print '(f6.1, a, l1)', t, ' C : ice | comfort band? ', comfortable
else if (t < 100.0_dp) then
print '(f6.1, a, l1)', t, ' C : liquid water | comfort band? ', comfortable
else
print '(f6.1, a, l1)', t, ' C : steam | comfort band? ', comfortable
end if
end do
end program phase_states
$ gfortran -std=f2018 -Wall -O2 example-01-if-then-else.f90 -o states && ./states
-12.0 C : ice | comfort band? F
0.0 C : liquid water | comfort band? F
25.0 C : liquid water | comfort band? T
42.0 C : liquid water | comfort band? F
105.0 C : steam | comfort band? F
Trace it by hand and confirm the machine has nothing to teach you here. At t = 0.0, the test t < 0.0 is
false, so we fall to else if (t < 100.0), which is true — liquid water. At t = 42.0, the comfort test is
42 >= 10 .and. 42 <= 40, which is .true. .and. .false., hence .false. — the L1 edit descriptor prints
a logical as a single T or F. The branches are tried in order and the first true one wins; once a
branch fires, the rest are skipped.
Two smaller forms are worth knowing. When a single statement is all you need, the logical if puts it on
one line with no then and no end if:
if (n < 0) n = -n ! take the absolute value, in one line
And you can nest constructs freely, though a chain of else if is almost always clearer than deep nesting.
⚠️ Common Pitfall: never test two reals for exact equality. The expression
if (x == 0.1_dp)is a bug waiting to happen, because0.1cannot be represented exactly in binary floating point, and a value you expect to be0.1may differ in the last bit. Test reals with a tolerance instead —if (abs(x - 0.1_dp) < 1.0e-9_dp)— and reserve==for integers, characters, and logicals, where equality is exact. Why0.1is inexact, and how to choose the tolerance, is the subject of Chapter 20; for now, simply build the habit.🐍 Python Comparison. Fortran's
else ifis Python'selifand C'selse if; the logic is identical. The differences are cosmetic but real: Fortran demands thethenand the closingend if(there is no indentation-as-syntax as in Python, and no braces as in C), and it spells the connectives.and./.or./.not.where Python writesand/or/notand C writes&&/||/!. The explicitend ifis not bureaucracy: it means a Fortranifblock is never ambiguous about where it ends, which is why Fortran has never suffered C's classic "dangling else" and "goto fail" bugs.
4.2 select case: One Value, Many Branches
Often you are not testing several different conditions but comparing one value against a list of
possibilities: which grade does this score earn, which boundary condition did the user request, which
command did they type. You can write this as a nested if, but it reads poorly and invites mistakes. For
this pattern Fortran gives you a dedicated, safer construct.
Definition (
select case). A control construct that evaluates a single expression once and transfers control to the onecaseblock whose label matches the value. Labels may be a single value (case (3)), a list (case (1, 2, 5)), or a range (case (90:),case (60:69),case (:0)). An optionalcase defaulthandles every value not otherwise listed. The case expression must be of a discrete type —integer,character, orlogical— neverreal.
Here it is classifying exam scores. Notice the range syntax, which no switch in C can express directly:
program grade_report
implicit none
integer :: scores(5) = [95, 82, 71, 66, 40]
integer :: i, s
character(len=1) :: letter
do i = 1, 5
s = scores(i)
select case (s)
case (90:) ! 90 and above
letter = 'A'
case (80:89)
letter = 'B'
case (70:79)
letter = 'C'
case (60:69)
letter = 'D'
case default ! everything else
letter = 'F'
end select
print '(a, i3, a, a)', 'score ', s, ' -> grade ', letter
end do
end program grade_report
$ gfortran -std=f2018 -Wall -O2 example-02-select-case.f90 -o grades && ./grades
score 95 -> grade A
score 82 -> grade B
score 71 -> grade C
score 66 -> grade D
score 40 -> grade F
The case labels must not overlap — the compiler will reject case (80:89) and case (85:95) together,
because a value could match both — and this restriction is exactly what makes select case safe. The
compiler checks that your cases are disjoint, something it cannot do for a chain of ifs.
select case is not limited to integers. It works on characters, which is how you dispatch on a
single-letter command, and the label list lets several values share one block:
program dispatch_demo
implicit none
character :: cmd = 'w'
select case (cmd)
case ('n', 's', 'e', 'w')
print '(a)', 'move'
case ('q')
print '(a)', 'quit'
case default
print '(a)', 'unknown command'
end select
end program dispatch_demo
$ gfortran -std=f2018 -Wall -O2 dispatch_demo.f90 -o dispatch && ./dispatch
move
🚪 Threshold Concept:
select casehas no fall-through, and that is a feature. In C, aswitchcase continues into the next case unless you writebreak;— and forgetting thatbreakis one of the most common bugs in the language. Fortran'sselect caseruns exactly one block and then jumps toend select; there is no fall-through and nobreakto forget. You give up C's occasional cleverness of stacking cases deliberately, and in exchange you never write the bug where a missingbreaksilently corrupts your logic. This is a recurring shape in Fortran's design: the language removes a footgun even at the cost of a little flexibility, because in code that certifies reactors, correctness beats cleverness every time.⚡ Performance Note. Beyond readability,
select casecan be faster than anif-chain. Because the compiler knows it is dispatching on one discrete value against disjoint labels, it may compile the whole construct to a jump table — a single indexed jump — instead of testing each condition in turn. For a handful of cases the difference is negligible, but the point stands: expressing your intent precisely (this is a one-value dispatch) hands the compiler information it can optimize with. That theme — say what you mean and the compiler rewards you — runs through the entire book.
If you are coming from Python, select case is the older cousin of match/case (Python 3.10+) and of the
dictionary dispatch ({key: value}.get(x, default)) that predated it; Fortran has had this construct for
decades. The one thing it deliberately does not do is Python's structural pattern matching — destructuring a
tuple, say — because select case is strictly a value dispatch, which is all a numerical code needs. We
lay the three languages side by side in §4.7.
🔄 Check Your Understanding
- Why can the case expression in
select casebe anintegerorcharacterbut never areal? - In the grade example, what would the compiler do if you wrote
case (60:69)and alsocase (65:75)? - Rewrite
if (day==6 .or. day==7) then ... else ... end ifas aselect case.
Answers
1. `select case` matches against exact, discrete labels, and real numbers are neither exact (0.1 is inexact) nor discrete — there is no next real after 3.0. Matching would be ill-defined, so the standard forbids it. 2. It rejects the program with a compile-time error: the ranges overlap at 65–69, so a value could match two cases. This disjointness check is a safety feature you do not get from an `if`-chain. 3. `select case (day)` / `case (6, 7)` → weekend / `case default` → weekday / `end select`.4.3 do Loops: Counted, do while, and Infinite-with-exit
Repetition is the heart of numerical computing, and Fortran's loop keyword is do. There are three shapes,
and choosing the right one is a small but real skill.
The counted do loop runs a known number of times, driven by an integer counter:
integer :: i, total
total = 0
do i = 1, 10 ! i takes 1, 2, 3, …, 10 (both ends inclusive)
total = total + i
end do ! total is now 55
The general form is do i = start, end, step; the step defaults to 1 if omitted and may be negative for a
countdown (do i = 10, 1, -1). Three facts about the counter repay memorizing. First, the bounds are
inclusive on both ends — do i = 1, n runs n times, not n-1 — which trips up programmers arriving
from Python's half-open range. Second, the number of iterations (the trip count) is computed once, at
loop entry, from the bounds and step; changing end inside the loop does not lengthen or shorten it. Third,
the loop counter must be an integer — real-valued do counters existed in FORTRAN 77 but were deleted
from the standard in Fortran 95, and gfortran -std=f2018 will reject one. When you need a real value that
advances each step — a running time t, say — derive it from the integer counter:
real(dp) :: t
do step = 0, n_steps
t = t0 + real(step, dp) * dt ! integer counter -> real time, exactly controlled
! … use t …
end do
⚠️ Common Pitfall: don't loop on a real counter — accumulate error instead. Writing
do t = 0.0, 1.0, 0.1(were it still legal) would not even execute the number of times you expect, because0.1is inexact and ten of them do not sum to exactly1.0. Count with an integer and compute the real value from it, as above. This is the same floating-point reality as the equality pitfall in §4.1, and the reason both matter is measured in Chapter 20.
The do while loop runs as long as a condition holds, testing before each iteration. Reach for it when
you do not know the count in advance — you are iterating until something converges or a supply runs out.
Definition (
do while). A loop of the formdo while (condition) … end dothat evaluates the logicalconditionbefore each pass and executes the body only while it is true. If the condition is false at the outset, the body runs zero times.
The infinite do with exit is the most flexible: a bare do with no bounds loops forever, and you
leave it with an exit statement when a condition — tested anywhere in the body, not just at the top —
is met.
Definition (
exit). A statement that immediately terminates the enclosingdoloop (or, with a construct name, a named loop — see §4.4) and transfers control to the statement just after itsend do.
These three shapes, plus cycle (next section), cover every loop you will ever write. Here they are
together — a short tour with hand-checked output:
program loop_tour
use, intrinsic :: iso_fortran_env, only: dp => real64
implicit none
integer :: i, j, n, total, halvings
real(dp) :: x
integer :: first_pair(2)
! (1) counted do: sum 1..10
total = 0
do i = 1, 10
total = total + i
end do
print '(a, i0)', 'sum 1..10 = ', total
! (2) do while: how many halvings of 100 until it drops below 1?
x = 100.0_dp
halvings = 0
do while (x >= 1.0_dp)
x = x / 2.0_dp
halvings = halvings + 1
end do
print '(a, i0)', 'halvings of 100 until < 1 = ', halvings
! (3) infinite do + exit: smallest n with n*n > 50
n = 0
do
n = n + 1
if (n*n > 50) exit
end do
print '(a, i0)', 'smallest n with n^2 > 50 = ', n
! (4) cycle: sum only the ODD numbers in 1..10 (previews §4.4)
total = 0
do i = 1, 10
if (mod(i, 2) == 0) cycle
total = total + i
end do
print '(a, i0)', 'sum of odds in 1..10 = ', total
! (5) named nested loops + exit: first (i,j) with i*j == 12 (previews §4.4)
first_pair = [0, 0]
search: do i = 1, 5
do j = 1, 5
if (i*j == 12) then
first_pair = [i, j]
exit search
end if
end do
end do search
print '(a, i0, a, i0)', 'first (i,j) with i*j=12 : i=', first_pair(1), ', j=', first_pair(2)
end program loop_tour
$ gfortran -std=f2018 -Wall -O2 example-03-do-loops.f90 -o loops && ./loops
sum 1..10 = 55
halvings of 100 until < 1 = 7
smallest n with n^2 > 50 = 8
sum of odds in 1..10 = 25
first (i,j) with i*j=12 : i=3, j=4
Walk the two interesting ones. The do while halves 100 repeatedly: after 7 halvings the value is
$100 / 2^7 = 0.78125$, which is below 1, and after 6 it is $100/2^6 = 1.5625$, which is not — so the answer
is 7. (These particular values are exact in binary, so the comparison is safe; the halving of powers of
two is one of the rare places real arithmetic is exact.) The infinite do increments n and tests
n*n > 50: since $7^2 = 49$ is not greater than 50 but $8^2 = 64$ is, it exits at n = 8.
📜 From History. The oldest Fortran loop was the
DO … CONTINUEloop with a numeric statement label, and escaping early or restarting meant aGOTO— the very construct Dijkstra was arguing against in this chapter's epigraph. Modern Fortran'sdo … end do, withexitandcycle, expresses every one of those old patterns structurally, with no labels and nogoto. You will meet the label-and-GOTOoriginals when you read old code in Chapter 17; this is a place where modern Fortran is unambiguously a modern language.
4.4 cycle, exit, Named Loops, and Nesting
You saw cycle and named loops in the tour above; here is the reasoning behind them.
Definition (
cycle). A statement that abandons the current iteration of a loop and jumps straight to the next one — skipping the rest of the loop body but not leaving the loop. Whereexitends the loop,cyclemerely ends the pass. (In Python these arebreakandcontinue; the mapping is exact.)
cycle is the clean way to say "not this one." In the tour, if (mod(i, 2) == 0) cycle skips the even
numbers so only the odds are summed — no nested if wrapping the rest of the body, just a flat "skip and
move on." Used well, it flattens code that would otherwise drift rightward into deep nesting.
The subtler tool is the named construct. When you nest loops, a plain exit leaves only the innermost
loop — but often you want to break clean out of the whole nest the instant you find what you are looking for.
Naming the outer loop lets you say exactly which one to leave.
Definition (named construct). Any
do,if, orselect caseconstruct may be given a name, writtenname: do …and closed withend do name.exit nameandcycle namethen act on the named loop rather than the innermost one — the only clean way to break out of, or continue, an outer loop from inside an inner one.
In the tour's final block, search: do i = 1, 5 names the outer loop, and when the inner loop finds the
first pair with i*j == 12 it runs exit search, leaving both loops at once. Without the name, exit
would abandon only the inner j loop, and the outer loop would blunder on to the next i — a genuine and
common bug. Naming is not only for escape; it is also documentation. On a long nested loop, end do rows
tells the reader (and you, six months later) which do just closed.
🐛 Find the Bug. This is meant to sum the first negative number found in each row of a small table, but it exits the wrong loop. What does it actually do, and how do you fix it?
fortran do row = 1, nrows do col = 1, ncols if (a(col, row) < 0.0_dp) then hits = hits + 1 exit ! <-- intended: stop scanning THIS row end if end do end do
Diagnosis
Nothing is wrong here, in fact — a bareexitleaves the innermost (col) loop, which is exactly "stop scanning this row," and the outerrowloop correctly continues. The trap is the opposite mistake: if you had wanted to stop scanning the entire table at the first negative anywhere, a bareexitwould be the bug, and you would need to name the outer loop and writeexit rows. The lesson: a bareexit/cyclealways acts on the innermost loop, so whenever your intent is an outer loop, name it and say so.⚠️ Common Pitfall: loop order in nested loops is a performance decision, not a free choice. Two nested loops over a 2-D array give the same answer in either order, but not the same speed — Fortran stores arrays so that the first index varies fastest through memory, so the first index belongs in the innermost loop. Put it in the outer loop and you stride through memory the wrong way and pay for it, sometimes 10×. We are not ready to prove this — it needs the array layout of Chapter 5 and the cache story of Chapter 27 — but start the habit now: in a grid sweep, loop the first index innermost. Notice that the tour's
searchloop, and the project skeleton below, both do exactly that.
4.5 where: Your First Taste of Whole-Array Thinking
Everything so far has processed one value at a time. Now a glimpse of the other way Fortran thinks — and the
reason it is fast. Suppose you have an array and you want to change only the elements that satisfy some
condition: clamp every negative temperature to zero, say. The one-at-a-time way is a loop with an if:
do i = 1, size(a)
if (a(i) < 0.0_dp) a(i) = 0.0_dp
end do
That is correct, and there is nothing wrong with it. But Fortran lets you say the same thing as a single operation on the whole array, with the condition itself expressed as an array:
Definition (
where). A construct that performs a masked whole-array assignment:where (mask) …applies the enclosed array assignments only at the positions where the logical arraymaskis true. An optionalelsewhere(with or without its own mask) handles the remaining positions. The mask and the arrays must have the same shape.
program where_demo
use, intrinsic :: iso_fortran_env, only: dp => real64
implicit none
real(dp) :: a(6) = [-3.0_dp, -1.0_dp, 0.0_dp, 2.0_dp, -5.0_dp, 4.0_dp]
real(dp) :: s(6)
! Build a sign array: +1 where positive, -1 where negative, 0 where zero.
where (a > 0.0_dp)
s = 1.0_dp
elsewhere (a < 0.0_dp)
s = -1.0_dp
elsewhere
s = 0.0_dp
end where
print '(a)', 'sign of each element:'
print '(6f6.1)', s
end program where_demo
$ gfortran -std=f2018 -Wall -O2 where_demo.f90 -o wheredemo && ./wheredemo
sign of each element:
-1.0 -1.0 0.0 1.0 -1.0 1.0
Read where as a sentence: where a is positive, set s to 1; elsewhere, where a is negative, set it to
−1; elsewhere, set it to 0. The condition a > 0.0_dp is not a single true/false — it is an array of
true/false, one per element, and where uses it as a stencil. For our a, the three masks partition the six
positions, giving s = [-1, -1, 0, 1, -1, 1]. (A single-line form exists too: where (a < 0.0_dp) a =
0.0_dp for the clamp.)
Why does this matter beyond saving a few lines? Because a where — like a whole-array assignment — hands the
compiler the entire operation at once, with the promise that the elements are independent, which is exactly
the structure it needs to vectorize the work. This is the same argument you met for a = b + c in
Chapter 1, now with a condition attached. Arrays are Fortran's
superpower, and where is your first small use of it. We are deliberately not going deeper now — array
declaration, sections, and the intrinsics live in Chapter 5, which revisits
where on real data. Consider this a trailhead.
🐍 Python Comparison. If you know NumPy,
where (a < 0.0_dp) a = 0.0_dpis preciselya[a < 0] = 0, and the three-way sign construct isnp.where. This is not a coincidence: NumPy's boolean-mask assignment was inspired by the array languages, Fortran among them, and it is fast for the same reason — the loop happens in compiled code, not in the interpreter. The difference is that in Fortran the entire program is compiled, so you never hit the cliff where the computation stops being a NumPy one-liner and pure-Python looping takes over. Fortran and Python are better together, and this is a spot where they think alike.
4.6 do concurrent vs. the Obsolescent forall
Here is a question that sounds pedantic but is worth real money in Part VII: when the iterations of a loop are independent — no iteration reads a value another iteration writes — how do you tell the compiler so it can run them in any order, or all at once? Fortran has a construct for exactly this assertion.
Definition (
do concurrent). A loop of the formdo concurrent (i = 1:n) … end doin which you promise the compiler that the iterations have no ordering dependencies — any iteration may run before, after, or simultaneously with any other, and the result must be the same. The compiler is then free (but not required) to reorder, vectorize, or parallelize them. The promise is yours to keep: if the iterations secretly depend on each other, the program is invalid, and the compiler will not warn you.
program concurrent_demo
use, intrinsic :: iso_fortran_env, only: dp => real64
implicit none
integer, parameter :: n = 5
real(dp) :: y(n)
integer :: i
! Each iteration writes its own y(i) and reads no other — genuinely independent.
do concurrent (i = 1:n)
y(i) = real(i, dp)**2
end do
print '(a)', 'y(i) = i**2 :'
print '(5f8.1)', y
end program concurrent_demo
$ gfortran -std=f2018 -Wall -O2 concurrent_demo.f90 -o conc && ./conc
y(i) = i**2 :
1.0 4.0 9.0 16.0 25.0
Two honesty notes, because this construct is widely misunderstood. First, do concurrent does not by
itself make anything run in parallel. Plain gfortran -std=f2018 compiles it and, by default, runs it as an
ordinary serial loop — the output above is what you get on any machine. What do concurrent does is grant
permission: it tells the compiler the iterations are independent, so that with the right flags (or the
right compiler) it may parallelize or vectorize them. Turning that permission into actual speed is a
performance topic, and its home is
Chapter 29. Second, the promise is
unchecked. A do concurrent whose body writes y(i) = y(i-1) + 1 is wrong — the iterations are not
independent — and nothing will stop you; you will simply get garbage on a parallel run. Use do concurrent
only when the independence is genuinely true, as it is above.
You will also meet, mostly in older code, a construct that tried to do a similar job and did not survive:
forall. Introduced in Fortran 95, forall was a masked array-assignment construct:
forall (i = 1:n) a(i) = b(i) * 2.0_dp ! obsolescent — do not write new code like this
It looks handy, but it was hemmed in by restrictive rules (its body could contain only assignments, and its
semantics required every right-hand side to be evaluated before any assignment, which often prevented the
optimizations it was supposed to enable). It so rarely paid off that the 2018 standard declared forall
obsolescent — still legal, but on the way out, and flagged by the committee as a feature to avoid. Modern
code does not use it. For a masked assignment, use where (§4.5); for an independent loop, use do
concurrent; for a plain whole-array operation, just write the array expression (a = b * 2.0_dp), which
Chapter 5 makes your default.
📜 From History: how the standard prunes itself. Fortran almost never deletes a feature (fifty-year-old programs must still compile), but it does mark features obsolescent — a formal on-notice status — to steer people away from constructs that turned out to be mistakes. Real
docounters went from obsolescent (Fortran 90) to deleted (Fortran 95);forallis obsolescent as of 2018. Watching what the committee deprecates is a quiet way to learn good style: the language is telling you, in writing, what it wishes it had never added. That a seventy-year-old language still curates itself this carefully is one more piece of evidence that Fortran is not dead.
🔄 Check Your Understanding
- True or false: writing
do concurrentguarantees your loop runs on multiple cores. - Why is a
do concurrentwith the bodyy(i) = y(i-1) + 1invalid? - What should you write instead of
forall (i = 1:n) a(i) = b(i) * 2.0_dpin new code?
Answers
1. False. It *permits* parallel/reordered execution but does not compel it; plain `gfortran` runs it serially. Actual parallelism needs the right flags or compiler ([Chapter 29](../../part-07-performance/chapter-29-optimization-techniques/index.md)). 2. Because iteration `i` reads `y(i-1)`, which iteration `i-1` writes — the iterations are *not* independent, so the "any order" promise is false and the result is undefined under reordering. 3. The whole-array assignment `a = b * 2.0_dp` (or, if you truly need a loop body, `do concurrent (i = 1:n) a(i) = b(i) * 2.0_dp`). `forall` is obsolescent.4.7 Side by Side: Control Flow in Python, C, and Fortran
You already program, so the fastest way to lock in Fortran's control flow is to lay it beside the two languages you most likely know. The constructs map almost one-to-one; the value is in the small, sharp differences.
| Idea | Fortran | Python | C |
|---|---|---|---|
| Two-way branch | if (c) then … else … end if |
if c: … else: … |
if (c) { … } else { … } |
| Chained branch | else if (c) then |
elif c: |
else if (c) |
One-line if |
if (c) stmt |
if c: stmt |
if (c) stmt; |
| Value dispatch | select case (x) (no fall-through) |
match x: / dict |
switch (x) (needs break) |
| Range in a case | case (60:69) |
case _ if 60<=x<=69 |
(not directly) |
| Counted loop | do i = 1, n (inclusive) |
for i in range(1, n+1) |
for (i=1; i<=n; i++) |
| Conditional loop | do while (c) |
while c: |
while (c) |
| Infinite + break | do … exit … end do |
while True: … break |
for(;;){ … break; } |
| Skip iteration | cycle |
continue |
continue |
| Break outer loop | exit name (named) |
(flag or exception) | (goto or flag) |
| Masked array set | where (mask) a = … |
a[mask] = … (NumPy) |
(manual loop) |
| Independent loop | do concurrent (i=1:n) |
(numba prange) |
(OpenMP #pragma) |
| Logical AND/OR/NOT | .and. .or. .not. |
and or not |
&& \|\| ! |
Three differences deserve a sentence each. Fortran's counted loop is inclusive on both ends and
conventionally 1-based, so do i = 1, n visits n elements — a Python programmer's most frequent
off-by-one when crossing over. Fortran's select case has no fall-through, so unlike C you never write
the missing-break bug. And Fortran alone has a first-class way to break out of a named outer loop
(exit rows) and to declare an independent loop (do concurrent) — where Python and C reach for a flag,
an exception, or a compiler pragma. These are not large differences, but they are the difference between code
that reads clearly and code that hides a footgun.
🔗 Connection. The two rows at the bottom of that table —
whereanddo concurrent— are the ones without clean Python or C equivalents, and that is precisely because they are array-and-parallel constructs, the things Fortran was built for. They are the seam between this chapter and the rest of the book: Chapter 5 developswhereand whole-array operations, and Part VII turnsdo concurrentinto measured speed. Everything else in this chapter you already knew from another language; those two are new, and they are the point.
Project Checkpoint
Your solver has, so far, a name and (from Chapter 3) a precision. Now it gets its skeleton: the control structure that every explicit time-marching simulation shares — a loop over time steps, and inside it a sweep over the grid that treats boundary points differently from interior points. There is no physics yet — and not even a temperature array yet; the field becomes a real 2D array in Chapter 5. That restraint is deliberate. We are wiring the frame the numerics will hang on, and getting the control flow right first, on a case we can check by hand, means that when the real stencil arrives in Chapter 24 we drop it into a frame we already trust.
program heat_skeleton
implicit none
integer, parameter :: nx = 4, ny = 3 ! a tiny plate: 4 by 3 grid points
integer, parameter :: n_steps = 3 ! march three time steps
integer :: step, i, j
integer :: n_boundary, n_interior
do step = 1, n_steps ! the TIME loop
n_boundary = 0
n_interior = 0
do j = 1, ny ! sweep the grid; first index innermost (see §4.4)
do i = 1, nx
if (i == 1 .or. i == nx .or. j == 1 .or. j == ny) then
n_boundary = n_boundary + 1 ! BOUNDARY: held fixed (Dirichlet, Chapter 24)
else
n_interior = n_interior + 1 ! INTERIOR: the stencil update goes here later
end if
end do
end do
print '(a, i0, a, i0, a, i0)', 'step ', step, ': boundary=', n_boundary, ' interior=', n_interior
end do
end program heat_skeleton
$ gfortran -std=f2018 -Wall -O2 project-checkpoint.f90 -o heat_skeleton && ./heat_skeleton
step 1: boundary=10 interior=2
step 2: boundary=10 interior=2
step 3: boundary=10 interior=2
Check the geometry by hand: a 4×3 grid has 12 points. A point is on the boundary when it sits in the first or
last column (i == 1 or i == nx) or the first or last row (j == 1 or j == ny); everything else is
interior. Only i ∈ {2, 3} with j == 2 escapes the edges, so there are 2 interior points and 10
boundary points — and since nothing changes the grid, every step reports the same split. That constancy is
the point: the skeleton is correct and inert, ready for physics.
Three things to notice, each a seed for later. The if that separates boundary from interior is the same
test Chapter 24 will use to
decide where to apply the five-point stencil and where to hold a fixed temperature. The do step loop is the
one that, in Part VIII, we will not parallelize (steps
depend on each other) even as we parallelize the grid sweep inside it. And the loop nest already puts the
first index i innermost — the column-major habit from §4.4 — so the code is laid out for speed before speed
is even on the syllabus. Save this as heat-solver/heat.f90's core loop; the
capstone is this exact structure,
grown up.
Summary
Control flow is how a program decides and repeats. Modern Fortran gives you block-structured constructs — no
goto — that map closely onto what you know, with a few sharp edges that favor correctness.
| Construct | Use it when | Key point |
|---|---|---|
if … else if … else … end if |
branching on several conditions | first true branch wins; needs then and end if |
select case |
dispatching on one discrete value | disjoint labels, ranges (60:69), no fall-through |
counted do i = a, b, s |
a known number of iterations | inclusive bounds; integer counter only; trip count fixed at entry |
do while (c) |
loop until a condition fails | tests before each pass; may run zero times |
infinite do + exit |
condition tested mid-body | exit leaves the loop |
cycle |
skip the rest of this iteration | like Python continue |
named loop + exit name |
break out of an outer loop | the only clean way; bare exit leaves the innermost |
where (mask) … |
change part of an array by condition | masked whole-array assignment; a taste of arrays |
do concurrent (i=…) |
independent iterations | permits reordering/parallelism; promise must be true |
Operators to memorize. Relational: ==, /=, <, <=, >, >=. Logical: .and., .or., .not.
(and .eqv., .neqv.). The dots are part of the spelling.
The three rules worth taping to your monitor. (1) Never compare two reals with ==; test abs(x-y) <
tol. (2) A counted do loop's counter is an integer and its bounds are inclusive on both ends. (3) select
case does not fall through and forall is obsolescent — prefer where, do concurrent, or a plain array
expression.
Spaced Review
Three questions reaching back to Chapter 3 — because control-flow conditions are built from the arithmetic and types you met there, and the two chapters interlock.
-
In the loop
do i = 1, 10, the counteriis aninteger. If you instead needed a real timetthat advances bydt = 0.1_dpeach step, why must you computetfrom the integer counter rather than loopingdo t = 0.0_dp, 1.0_dp, 0.1_dp?
Answer
Two reasons, both from Chapter 3's floating-point reality. First, real `do` counters were deleted from the standard (Fortran 95), so `gfortran -std=f2018` rejects them. Second, even if allowed, `0.1` is not exact in binary, so ten steps would not land exactly on `1.0` and the trip count would be unpredictable. Count with an integer and set `t = t0 + real(step, dp) * dt`. -
The expression
mod(i, 2) == 0tests whetheriis even. Ifiand2are both integers, what type doesmod(i, 2)return, and what does7 / 2evaluate to in Fortran?
Answer
`mod` of two integers returns an `integer` (0 for even `i`, 1 for odd). And `7 / 2` is **integer division**, which truncates toward zero to give `3`, not `3.5` — the classic Chapter 3 trap. To get `3.5` you would write `7.0_dp / 2.0_dp` or `real(7, dp) / 2`. -
A condition reads
if (t >= 10.0_dp .and. t <= 40.0_dp). What is the type of the whole expression, and what is the_dpsuffix on the literals doing?
Answer
The whole expression is of type `logical` (each comparison yields a logical, and `.and.` combines them). The `_dp` suffix makes each literal a `real(dp)` constant — the double-precision kind defined in Chapter 3 — so the comparison is done in the same precision as `t`, with no silent mixed-kind conversion.
What's Next
You now have the verbs of computation — decide, repeat, skip, break — but they have been acting on lonely
scalars, one number at a time. That is not how Fortran wants to work, and it is not why Fortran is fast. The
where and do concurrent of this chapter were a hint: the natural unit of a Fortran program is not the
number but the array. Chapter 5 is the pivotal chapter of Part I, where
arrays become first-class objects — declared, sliced, operated on whole, and laid out in memory in the
column-major order that makes the loop-order habit you started here pay off in raw speed. Your heat solver's
temperature field, a flat idea today, becomes a real two-dimensional array there. Let's give the numbers
some shape.