Chapter 3 Control Flow and Functions
Control flow determines which expressions run and how often they run. Functions package those decisions into reusable units that can be tested and combined.
3.1 Scalar decisions
An if condition must represent one logical decision. Test missingness before
using a value in a condition because if (NA) cannot choose a branch.
classify_score <- function(score) {
if (is.na(score)) {
"missing"
} else if (score >= 90) {
"high"
} else if (score >= 70) {
"medium"
} else {
"low"
}
}
classify_score(84)
#> [1] "medium"
classify_score(NA_real_)
#> [1] "missing"Use && and || for scalar control-flow conditions. They evaluate from left
to right and stop as soon as the result is known.
3.2 Vectorized decisions
ifelse() chooses element by element. It is convenient for simple recoding,
but the output type is determined by its branches and may not preserve every
class attribute.
response_times <- c(1.2, 2.8, NA, 4.1)
status <- ifelse(
is.na(response_times),
"missing",
ifelse(response_times <= 3, "on time", "late")
)
status
#> [1] "on time" "on time" "missing" "late"For complex business rules, write explicit tests and verify that categories are mutually exclusive and collectively exhaustive.
3.3 for loops
A loop is appropriate when each step has side effects or depends on previous steps. Preallocate the result instead of repeatedly growing an object.
inputs <- c(2, 4, 6, 8)
squared <- numeric(length(inputs))
for (i in seq_along(inputs)) {
squared[i] <- inputs[i]^2
}
squared
#> [1] 4 16 36 64seq_along(x) safely handles an empty vector, unlike 1:length(x).
3.4 while loops
A while loop is useful when the number of iterations is not known in advance.
Its state must move toward a stopping condition.
remaining <- 100
periods <- 0L
while (remaining > 10) {
remaining <- remaining * 0.6
periods <- periods + 1L
}
c(periods = periods, remaining = remaining)
#> periods remaining
#> 5.000 7.776In production code, consider a maximum-iteration guard when convergence is not guaranteed.
3.5 Function contracts
A useful function has a focused responsibility, meaningful argument names, and clear behavior for invalid input.
weighted_mean <- function(x, weights, na.rm = FALSE) {
if (!is.numeric(x) || !is.numeric(weights)) {
stop("x and weights must be numeric.")
}
if (length(x) != length(weights)) {
stop("x and weights must have equal lengths.")
}
if (any(weights < 0, na.rm = TRUE)) {
stop("weights must be non-negative.")
}
stats::weighted.mean(x, weights, na.rm = na.rm)
}
weighted_mean(c(10, 20, 30), c(1, 2, 1))
#> [1] 20Defaults should be safe and unsurprising. Validate assumptions close to the function boundary so failures explain what is wrong.
3.6 Lexical scope and closures
R resolves names in the environment where a function was created. A closure is a function bundled with that environment.
make_counter <- function(start = 0L) {
value <- as.integer(start)
function() {
value <<- value + 1L
value
}
}
counter <- make_counter(10)
counter()
#> [1] 11
counter()
#> [1] 12<<- changes a binding outside the current function. It can support deliberate
state, caching, or counters, but ordinary analysis functions should usually
return a value instead of modifying hidden state.