R from the ground up

Loops, lists and the apply family

Repeating work with for and while, steering a loop with next and break, and knowing when lapply or sapply says the same thing better.

Part 8 of 12

Intermediate18 min readBack to Courses

For each value, once

for (n in 1:5) { ... } runs the body once per value, with n holding the current one. The sequence does not have to be numbers: for (day in days) walks a vector of names, and in the next chapters you will walk the rows of a table.

while (condition) { ... } repeats while a test stays TRUE, which is the right loop when you do not know in advance how many turns you need. Both put the body in braces, and both are plain readable loops rather than anything clever. The loop variable is an ordinary variable that keeps its last value after the loop ends, which is occasionally useful and more often a sign that the loop should have stored its answer.

Steering a loop

next abandons the rest of the current turn and starts the following one — the way to skip the values you do not care about. break leaves the loop altogether, which is how you stop as soon as you have found what you were looking for.

An accumulator is the other half of loop writing: start a variable before the loop and add to it inside, as in total <- total + n. The variable keeps its value between turns, which is what makes the running total work. Both next and break act on the loop, not on a function: inside a function they leave the loop, while return is the one that leaves the function.

When a loop is the wrong tool

Before you write a loop over a vector, ask whether the whole-vector version exists. It usually does: scaling every value by a number, or calling sum(scores), cumsum(spend) and diff(spend), all work on the vector at once. Loops are for work with a side effect, such as printing a report line per row.

The apply family sits in between. lapply gives a list, sapply simplifies to a vector, and vapply checks the answers against a shape you state. On a matrix, apply(m, 1, sum) works along each row and apply(m, 2, sum) down each column, and the common cases have fast names of their own: colSums(m), rowMeans(m). sapply simplifies when every answer has the same length and type and returns a list when they do not, so vapply is the form to use when the shape is a promise you want checked.

Four loops and one lapply

total <- 0
for (n in 1:5) {
  total <- total + n
}

for (n in 1:6) {
  if (n %% 2 == 0) next
  cat(n, " ")
}

countdown <- 3
while (countdown > 0) {
  countdown <- countdown - 1
}

lapply(c(2, 4), function(n) n * 10)

m <- matrix(1:6, nrow = 2)
colSums(m)
rowMeans(m)

A sum, the odd numbers, and a countdown

Run it and read the order the lines come out in. Then change the cut-off in the last loop from 20 to 40 and predict how many amounts are added before it stops.

Output
Click "Run" to execute your code

Growing a vector inside a loop, as in odds <- c(odds, n), copies the whole vector on every turn. It is fine at the size of these lessons, but for real work start from a vector of the final size or reach for sapply instead.

Preallocate, then fill

A vector has a fixed length. When you write odds <- c(odds, n) inside a loop, R copies the whole vector into a new one on every turn, because the old one has no room for the new value. At six values you will not notice; at sixty thousand the loop becomes slow for a reason that has nothing to do with the arithmetic.

The cure is to decide the size first: out <- numeric(n), then fill out[i]. seq_along(out) gives the positions, and it is safer than 1:length(out) because an empty vector makes 1:0 equal c(1, 0) while seq_along gives nothing. When the answers are not numbers, vector("list", n) or character(n) does the same job.

Stopping, skipping and running forever

next and break are control flow, not expressions: they do not return a value, they change which line runs next. next belongs before the work you want to skip, and break belongs after the condition that tells you to stop. Putting break before the accumulator silently drops the value that triggered it.

A while loop needs a change inside the body that moves the condition toward FALSE. Forgetting countdown <- countdown - 1 gives a loop that never ends, and in a browser that means the tab stops responding. Write the update next to the test, and check the loop on a small number before trusting it on a large one.

Three loop mistakes to recognise. 1:length(x) misbehaves when x is empty, because the colon counts down; seq_along(x) does not. A vector grown with c() inside the loop copies every turn, which is the slow pattern preallocation avoids. And a while whose condition never changes runs forever — the update line is not optional.

Try these next

  • Fill a vector of ten squares with a for loop, then check it against (1:10) ^ 2.
  • Write a while loop that halves 100 until it drops below 10, and print the number of turns.
  • Use next to sum only the numbers from 1 to 20 that are not divisible by 3.

A payback plan, with the vector sized first

Run it and check the payment count against the balance. Then change the payment from 250 to 300, predict the number of months, and change the monthly squares from 12 to 6.

Output
Click "Run" to execute your code

Check what you learned

One question at a time. A wrong answer explains itself — take another go.

Part 1 of 5

What does next do inside a loop?