29 min read

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

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 if construct, building conditions from relational and logical operators, and recognize the traps (comparing reals for exact equality; the dangling else).
  • Use select case to dispatch on a value cleanly, and say precisely why it beats a tower of nested ifs.
  • Write all three kinds of do loop — counted, do while, and the infinite do ended by exit — 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 using do 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 where is 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 History boxes: the goto, arithmetic if, and forall you 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 if construct). A block of the form if (condition) then … end if that executes its enclosed statements only when the logical condition is true. It may include any number of else if (condition) then branches, tested in order, and an optional final else branch 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, because 0.1 cannot be represented exactly in binary floating point, and a value you expect to be 0.1 may 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. Why 0.1 is inexact, and how to choose the tolerance, is the subject of Chapter 20; for now, simply build the habit.

🐍 Python Comparison. Fortran's else if is Python's elif and C's else if; the logic is identical. The differences are cosmetic but real: Fortran demands the then and the closing end 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 writes and / or / not and C writes && / || / !. The explicit end if is not bureaucracy: it means a Fortran if block 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 one case block 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 optional case default handles every value not otherwise listed. The case expression must be of a discrete type — integer, character, or logical — never real.

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 case has no fall-through, and that is a feature. In C, a switch case continues into the next case unless you write break; — and forgetting that break is one of the most common bugs in the language. Fortran's select case runs exactly one block and then jumps to end select; there is no fall-through and no break to forget. You give up C's occasional cleverness of stacking cases deliberately, and in exchange you never write the bug where a missing break silently 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 case can be faster than an if-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

  1. Why can the case expression in select case be an integer or character but never a real?
  2. In the grade example, what would the compiler do if you wrote case (60:69) and also case (65:75)?
  3. Rewrite if (day==6 .or. day==7) then ... else ... end if as a select 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 endsdo 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, because 0.1 is inexact and ten of them do not sum to exactly 1.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 form do while (condition) … end do that evaluates the logical condition before 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 enclosing do loop (or, with a construct name, a named loop — see §4.4) and transfers control to the statement just after its end 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 … CONTINUE loop with a numeric statement label, and escaping early or restarting meant a GOTO — the very construct Dijkstra was arguing against in this chapter's epigraph. Modern Fortran's do … end do, with exit and cycle, expresses every one of those old patterns structurally, with no labels and no goto. You will meet the label-and-GOTO originals 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. Where exit ends the loop, cycle merely ends the pass. (In Python these are break and continue; 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, or select case construct may be given a name, written name: do … and closed with end do name. exit name and cycle name then 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 bare exit leaves the innermost (col) loop, which is exactly "stop scanning this row," and the outer row loop correctly continues. The trap is the opposite mistake: if you had wanted to stop scanning the entire table at the first negative anywhere, a bare exit would be the bug, and you would need to name the outer loop and write exit rows. The lesson: a bare exit/cycle always 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 search loop, 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 array mask is true. An optional elsewhere (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_dp is precisely a[a < 0] = 0, and the three-way sign construct is np.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 form do concurrent (i = 1:n) … end do in 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 do counters went from obsolescent (Fortran 90) to deleted (Fortran 95); forall is 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

  1. True or false: writing do concurrent guarantees your loop runs on multiple cores.
  2. Why is a do concurrent with the body y(i) = y(i-1) + 1 invalid?
  3. What should you write instead of forall (i = 1:n) a(i) = b(i) * 2.0_dp in 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 — where and do 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 develops where and whole-array operations, and Part VII turns do concurrent into 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.

  1. In the loop do i = 1, 10, the counter i is an integer. If you instead needed a real time t that advances by dt = 0.1_dp each step, why must you compute t from the integer counter rather than looping do t = 0.0_dp, 1.0_dp, 0.1_dp?

    AnswerTwo 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`.

  2. The expression mod(i, 2) == 0 tests whether i is even. If i and 2 are both integers, what type does mod(i, 2) return, and what does 7 / 2 evaluate 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`.

  3. 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 _dp suffix on the literals doing?

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