Lesson 7 / 25

Control Flow and Vectorisation

Write conditions and loops, and prefer vectorised alternatives.

When to loop and when not to

R has if/else, for, while, repeat with break, and next to skip an iteration. if expects a single TRUE or FALSE; passing a longer logical vector is an error in recent R versions, which is why vectorised work uses ifelse() or dplyr's if_else() and case_when() instead. Loops are fine for things that are genuinely sequential, such as simulations where each step depends on the previous one, reading files one by one, or calling an API per item. But for element-wise calculations, vectorised functions (sum, cumsum, arithmetic, comparisons) are much faster and clearer, because they run in compiled code. When you do write a loop that builds a result, pre-allocate the output (results <- numeric(n)) rather than growing a vector with c() each time, which copies it repeatedly. seq_along(x) is safer than 1:length(x), which misbehaves when x is empty (it gives c(1, 0)).

Loop versus vectorised

A loop processes elements one at a time; a vectorised function processes the whole vector in one call.

Top: a small arrow visiting each box of a row in turn. Bottom: one wide arrow covering the whole row at once.
Figure 3.1 — Element-by-element loops versus vectorised operations.

Loops done right, and vectorised alternatives

seq_along, pre-allocation and case_when.

marks <- c(72, 45, 90, 38, 66)

# vectorised: preferred
grades <- dplyr::case_when(
  marks >= 85 ~ "A",
  marks >= 70 ~ "B",
  marks >= 40 ~ "C",
  .default = "Fail"
)

# loop with pre-allocation, when each step depends on the previous one
balance <- numeric(12)
balance[1] <- 10000
for (m in seq_along(balance)[-1]) {
  balance[m] <- balance[m - 1] * 1.01 + 500      # interest plus monthly deposit
}
round(tail(balance, 1))

# safe iteration over a possibly empty vector
empty <- c()
for (i in seq_along(empty)) print(i)             # runs zero times
# for (i in 1:length(empty)) ...                 # runs for i = 1 and i = 0: a bug

if (mean(marks) > 60) {
  message("Class average is above 60")
} else {
  message("Class needs support")
}

if needs one TRUE or FALSE

if (marks > 40) with a vector of marks is an error. Decide whether you mean "for each element" (use ifelse/case_when) or "any" or "all" (use any() or all()).

Quick check: Why is seq_along(x) preferred over 1:length(x) in loops?

  • It correctly produces an empty sequence when x is empty
  • It is faster for long vectors
  • It sorts x
  • It works only with lists
Answer

It correctly produces an empty sequence when x is empty — 1:length(x) gives c(1, 0) for an empty x, causing a loop to run when it should not.