Chapter 18 Control Flow
What You’ll Learn:
- if/else statements
- for, while, repeat loops
- break and next
- switch statements
- Common control flow errors
Key Errors Covered: 15+ control flow errors
Difficulty: ⭐⭐ Intermediate
18.1 Introduction
Control flow directs code execution, but has pitfalls:
# Works, but what about this?
x <- c(5, 15)
if (x > 3 & x < 10) { # Warning!
print("In range")
}
#> Error in if (x > 3 & x < 10) {: the condition has length > 1Let’s master control flow to avoid these issues.
18.2 if/else Basics
💡 Key Insight: if Requires Single Logical
# Correct: single TRUE/FALSE
if (TRUE) {
print("Yes")
}
#> [1] "Yes"
if (5 > 3) {
print("Five is greater")
}
#> [1] "Five is greater"
# With else
x <- 10
if (x > 5) {
print("Large")
} else {
print("Small")
}
#> [1] "Large"
# else if
x <- 5
if (x > 10) {
print("Large")
} else if (x > 5) {
print("Medium")
} else {
print("Small")
}
#> [1] "Small"
# ifelse for vectors (different!)
x <- c(3, 7, 12)
ifelse(x > 5, "Large", "Small")
#> [1] "Small" "Large" "Large"Key points:
- if needs single logical value
- ifelse() is vectorized (for vectors)
- if can have else and else if
- Braces {} recommended even for single lines
18.3 Error #1: the condition has length > 1
⭐ BEGINNER 🧠 LOGIC
18.3.1 The Error
x <- c(5, 15)
if (x > 10) {
print("Greater than 10")
}
#> Error in if (x > 10) {: the condition has length > 1🔴 ERROR (R >= 4.2)
Error in if (x > 10) { : the condition has length > 1
In older R versions, this gives a warning and uses only the first element.
18.3.4 Solutions
✅ SOLUTION 1: Use && or || for Scalar Conditions
✅ SOLUTION 2: Use all() or any() for Vectors
✅ SOLUTION 3: Use ifelse() for Vectorized Operations
ages <- c(15, 25, 35)
# Vectorized: returns vector
status <- ifelse(ages >= 18, "Adult", "Minor")
status
#> [1] "Minor" "Adult" "Adult"
# Or with dplyr::case_when (cleaner for multiple conditions)
library(dplyr)
case_when(
ages < 13 ~ "Child",
ages < 18 ~ "Teen",
ages < 65 ~ "Adult",
TRUE ~ "Senior"
)
#> [1] "Teen" "Adult" "Adult"⚠️ Common Pitfall: & vs && and | vs ||
# For if statements (scalar)
x <- 5
y <- 10
# Use && and || (short-circuit evaluation)
if (x > 3 && y > 8) {
print("Both true")
}
#> [1] "Both true"
# For vectors
v1 <- c(TRUE, FALSE, TRUE)
v2 <- c(TRUE, TRUE, FALSE)
# Use & and | (element-wise)
v1 & v2
#> [1] TRUE FALSE FALSE
v1 | v2
#> [1] TRUE TRUE TRUE
# Never use && or || on vectors in if!Key difference:
- & and |: Vectorized, return vector
- && and ||: Scalar, return single value, short-circuit
18.4 Error #2: argument is of length zero
⭐⭐ INTERMEDIATE 🧠 LOGIC
18.4.1 The Error
x <- numeric(0) # Empty vector
if (x > 5) {
print("Greater")
}
#> Error in if (x > 5) {: argument is of length zero🔴 ERROR
Error in if (x > 5) { : argument is of length zero
18.5 for Loops
💡 Key Insight: for Loop Patterns
# 1. Loop over vector
for (i in 1:5) {
print(i)
}
#> [1] 1
#> [1] 2
#> [1] 3
#> [1] 4
#> [1] 5
# 2. Loop over elements
fruits <- c("apple", "banana", "cherry")
for (fruit in fruits) {
print(fruit)
}
#> [1] "apple"
#> [1] "banana"
#> [1] "cherry"
# 3. Loop with indices
for (i in seq_along(fruits)) {
cat(i, ":", fruits[i], "\n")
}
#> 1 : apple
#> 2 : banana
#> 3 : cherry
# 4. Nested loops
for (i in 1:3) {
for (j in 1:2) {
cat("(", i, ",", j, ") ")
}
cat("\n")
}
#> ( 1 , 1 ) ( 1 , 2 )
#> ( 2 , 1 ) ( 2 , 2 )
#> ( 3 , 1 ) ( 3 , 2 )
# 5. Pre-allocate results (important for performance!)
n <- 1000
result <- numeric(n) # Pre-allocate
for (i in 1:n) {
result[i] <- i^2
}Best practices:
- Use seq_along() instead of 1:length()
- Pre-allocate result vectors
- Consider vectorization instead
- Use for for side effects (plots, files)
18.6 Error #3: object not found in loops
⭐ BEGINNER 🔍 SCOPE
18.6.1 The Error
# Empty vector
values <- numeric(0)
for (i in 1:length(values)) {
print(values[i])
}
#> [1] NA
#> numeric(0)🔴 ERROR
Error in 1:length(values) : argument of length 0
Wait, that’s different. Let me show the real issue:
18.7 while Loops
💡 Key Insight: while vs for
# for: known number of iterations
for (i in 1:5) {
print(i)
}
#> [1] 1
#> [1] 2
#> [1] 3
#> [1] 4
#> [1] 5
# while: iterate until condition false
count <- 1
while (count <= 5) {
print(count)
count <- count + 1
}
#> [1] 1
#> [1] 2
#> [1] 3
#> [1] 4
#> [1] 5
# while with break
count <- 1
while (TRUE) {
print(count)
count <- count + 1
if (count > 5) break
}
#> [1] 1
#> [1] 2
#> [1] 3
#> [1] 4
#> [1] 5
# Infinite loop danger!
# while (TRUE) {
# # Never breaks!
# }When to use while: - Unknown number of iterations - Convergence checking - Reading until end - Waiting for condition
18.8 break and next
💡 Key Insight: Loop Control
# break: exit loop immediately
for (i in 1:10) {
if (i > 5) break
print(i)
}
#> [1] 1
#> [1] 2
#> [1] 3
#> [1] 4
#> [1] 5
# Prints 1-5, then stops
# next: skip to next iteration
for (i in 1:10) {
if (i %% 2 == 0) next # Skip even numbers
print(i)
}
#> [1] 1
#> [1] 3
#> [1] 5
#> [1] 7
#> [1] 9
# Prints only odd numbers
# Combined
for (i in 1:20) {
if (i > 15) break # Stop at 15
if (i %% 2 == 0) next # Skip evens
print(i)
}
#> [1] 1
#> [1] 3
#> [1] 5
#> [1] 7
#> [1] 9
#> [1] 11
#> [1] 13
#> [1] 15
# Prints odd numbers up to 15
# In nested loops
for (i in 1:3) {
for (j in 1:3) {
if (i == j) next # Skip diagonal
cat("(", i, ",", j, ") ")
}
cat("\n")
}
#> ( 1 , 2 ) ( 1 , 3 )
#> ( 2 , 1 ) ( 2 , 3 )
#> ( 3 , 1 ) ( 3 , 2 )18.9 repeat Loops
💡 Key Insight: repeat Loop
# repeat: infinite loop with break
count <- 0
repeat {
count <- count + 1
print(count)
if (count >= 5) break
}
#> [1] 1
#> [1] 2
#> [1] 3
#> [1] 4
#> [1] 5
# Common pattern: read until done
# repeat {
# line <- readLines(connection, n = 1)
# if (length(line) == 0) break
# process(line)
# }
# Convergence checking
tolerance <- 0.001
value <- 10
repeat {
old_value <- value
value <- value / 2 + 1
if (abs(value - old_value) < tolerance) {
break
}
}
cat("Converged to:", value, "\n")
#> Converged to: 2.00097718.10 switch Statement
🎯 Best Practice: switch()
# Cleaner than multiple if/else
operation <- "add"
result <- switch(operation,
add = 10 + 5,
subtract = 10 - 5,
multiply = 10 * 5,
divide = 10 / 5,
"Unknown operation" # Default
)
result
#> [1] 15
# With functions
calculate <- function(op, x, y) {
switch(op,
"+" = x + y,
"-" = x - y,
"*" = x * y,
"/" = x / y,
stop("Unknown operation: ", op)
)
}
calculate("+", 10, 5)
#> [1] 15
calculate("*", 10, 5)
#> [1] 50
# Numeric switch (uses position)
type <- 2
switch(type,
"First", # 1
"Second", # 2
"Third" # 3
)
#> [1] "Second"
# Multiple cases to same result
grade <- "B"
message <- switch(grade,
"A" = ,
"B" = "Good job!",
"C" = ,
"D" = "Need improvement",
"F" = "Failed",
"Invalid grade"
)
message
#> [1] "Good job!"18.11 Common Loop Patterns
🎯 Best Practice: Loop Patterns
# 1. Accumulation
total <- 0
for (i in 1:10) {
total <- total + i
}
total
#> [1] 55
# Better: use sum()
sum(1:10)
#> [1] 55
# 2. Building a result vector
n <- 5
squares <- numeric(n) # Pre-allocate!
for (i in 1:n) {
squares[i] <- i^2
}
squares
#> [1] 1 4 9 16 25
# Better: vectorize
(1:5)^2
#> [1] 1 4 9 16 25
# 3. Processing with indices
data <- c(10, 20, 30, 40)
for (i in seq_along(data)) {
cat("Element", i, "is", data[i], "\n")
}
#> Element 1 is 10
#> Element 2 is 20
#> Element 3 is 30
#> Element 4 is 40
# 4. Conditional accumulation
values <- c(1, 5, 3, 8, 2, 9, 4)
count <- 0
for (val in values) {
if (val > 5) {
count <- count + 1
}
}
count
#> [1] 2
# Better: use sum()
sum(values > 5)
#> [1] 2
# 5. Early exit
find_first <- function(x, threshold) {
for (i in seq_along(x)) {
if (x[i] > threshold) {
return(i)
}
}
return(NA)
}
find_first(c(1, 3, 7, 2, 9), 5)
#> [1] 3
# 6. Nested iteration
matrix_data <- matrix(1:9, nrow = 3)
for (i in 1:nrow(matrix_data)) {
for (j in 1:ncol(matrix_data)) {
cat(matrix_data[i, j], " ")
}
cat("\n")
}
#> 1 4 7
#> 2 5 8
#> 3 6 9
# Better: often use apply family or vectorization18.12 Vectorization vs Loops
⚠️ Performance: Vectorization Usually Better
n <- 10000
# Loop (slow)
system.time({
result <- numeric(n)
for (i in 1:n) {
result[i] <- sqrt(i)
}
})
#> user system elapsed
#> 0.003 0.000 0.003
# Vectorized (fast)
system.time({
result <- sqrt(1:n)
})
#> user system elapsed
#> 0 0 0
# When to use loops:
# 1. Sequential dependencies
fibonacci <- function(n) {
fib <- numeric(n)
fib[1] <- 1
fib[2] <- 1
for (i in 3:n) {
fib[i] <- fib[i-1] + fib[i-2] # Depends on previous
}
fib
}
# 2. Side effects (printing, plotting, file I/O)
for (i in 1:3) {
plot(1:10, main = paste("Plot", i))
Sys.sleep(0.1)
}
# 3. Complex logic that can't be vectorized
# 4. Early termination conditions


18.13 ifelse() Details
💡 Key Insight: ifelse() Behavior
# Basic ifelse
x <- c(1, 5, 3, 8, 2)
ifelse(x > 4, "High", "Low")
#> [1] "Low" "High" "Low" "High" "Low"
# Nested ifelse
ifelse(x < 3, "Low",
ifelse(x < 7, "Medium", "High"))
#> [1] "Low" "Medium" "Medium" "High" "Low"
# With NAs
x_na <- c(1, 5, NA, 8, 2)
ifelse(x_na > 4, "High", "Low") # NA stays NA
#> [1] "Low" "High" NA "High" "Low"
# Type coercion in ifelse
ifelse(c(TRUE, FALSE, TRUE), 1, "No") # Coerces to character!
#> [1] "1" "No" "1"
# More control with dplyr::case_when
library(dplyr)
case_when(
x < 3 ~ "Low",
x < 7 ~ "Medium",
TRUE ~ "High" # Default
)
#> [1] "Low" "Medium" "Medium" "High" "Low"
# Maintains types better
case_when(
c(TRUE, FALSE, TRUE) ~ 1L,
TRUE ~ NA_integer_
)
#> [1] 1 NA 1Prefer case_when() for: - Multiple conditions - Type preservation - Clearer code
18.14 Summary
Key Takeaways:
- if needs single logical - Use
&&/||not&/| - Check length first - Avoid length-zero errors
- Use seq_along() - Not
1:length()in loops - Pre-allocate vectors - Important for performance
- break exits loop -
nextskips iteration - Vectorize when possible - Usually faster than loops
- ifelse() is vectorized - Different from
if - Use case_when() - Cleaner than nested ifelse
Quick Reference:
| Error | Cause | Fix |
|---|---|---|
| condition has length > 1 | Vector in if | Use all(), any(), or &&/|| |
| argument is of length zero | Empty vector in if | Check length() first |
| Infinite loop | No break condition | Add break or fix condition |
| Wrong 1:length() | Empty vector | Use seq_along() |
Control Flow:
# if/else
if (condition) {
# code
} else if (other_condition) {
# code
} else {
# code
}
# ifelse (vectorized)
ifelse(test, yes, no)
# for loop
for (i in seq_along(x)) {
# code
}
# while loop
while (condition) {
# code
}
# repeat loop
repeat {
# code
if (condition) break
}
# switch
switch(value,
case1 = result1,
case2 = result2,
default
)Best Practices:
# ✅ Good
if (length(x) > 0 && x[1] > 5) # Check length
for (i in seq_along(x)) # Safe indexing
result <- numeric(n); for... # Pre-allocate
result <- sqrt(x) # Vectorize when possible
# ❌ Avoid
if (x > 5) # Vector in if
for (i in 1:length(x)) # Fails on empty
for... result <- c(result, new) # Growing vector (slow)
for (i in 1:n) result[i] <- x[i]^2 # Loop when vectorization works18.15 Exercises
📝 Exercise 1: Safe Condition Checker
Write safe_if(condition, true_val, false_val) that:
1. Checks if condition is single logical
2. Handles NA in condition
3. Returns appropriate value
4. Gives helpful errors
📝 Exercise 2: Loop Converter
Convert this loop to vectorized code:
📝 Exercise 3: Find First
Write find_first(x, condition) that:
1. Finds first element satisfying condition
2. Returns index and value
3. Handles case where none match
4. Uses early exit for efficiency
📝 Exercise 4: Grade Classifier
Write classify_grade(score) using switch() that:
1. Converts numeric score to letter grade
2. Handles vectorized input
3. Validates input range (0-100)
4. Returns appropriate grade
18.16 Exercise Answers
Click to see answers
Exercise 1:
safe_if <- function(condition, true_val, false_val) {
# Check if condition is logical
if (!is.logical(condition)) {
stop("Condition must be logical, got ", class(condition)[1])
}
# Check length
if (length(condition) == 0) {
stop("Condition has length zero")
}
if (length(condition) > 1) {
warning("Condition has length ", length(condition),
", using first element only")
condition <- condition[1]
}
# Handle NA
if (is.na(condition)) {
warning("Condition is NA, returning NA")
return(NA)
}
# Return appropriate value
if (condition) {
true_val
} else {
false_val
}
}
# Test
safe_if(TRUE, "yes", "no")
#> [1] "yes"
safe_if(5 > 3, "greater", "less")
#> [1] "greater"
safe_if(NA, "yes", "no") # Warning
#> Warning in safe_if(NA, "yes", "no"): Condition is NA, returning NA
#> [1] NA
safe_if(c(TRUE, FALSE), "yes", "no") # Warning
#> Warning in safe_if(c(TRUE, FALSE), "yes", "no"): Condition has length 2, using
#> first element only
#> [1] "yes"safe_if("not logical", "yes", "no") # Error
#> Error in safe_if("not logical", "yes", "no"): Condition must be logical, got characterExercise 2:
# Original loop
x <- 1:1000
result_loop <- numeric(length(x))
for (i in seq_along(x)) {
if (x[i] %% 2 == 0) {
result_loop[i] <- x[i]^2
} else {
result_loop[i] <- x[i]^3
}
}
# Vectorized version 1: ifelse
result_vec1 <- ifelse(x %% 2 == 0, x^2, x^3)
# Vectorized version 2: case_when
library(dplyr)
result_vec2 <- case_when(
x %% 2 == 0 ~ x^2,
TRUE ~ x^3
)
# Vectorized version 3: logical indexing
result_vec3 <- numeric(length(x))
even <- x %% 2 == 0
result_vec3[even] <- x[even]^2
result_vec3[!even] <- x[!even]^3
# Verify all give same result
all.equal(result_loop, result_vec1)
#> [1] TRUE
all.equal(result_loop, result_vec2)
#> [1] TRUE
all.equal(result_loop, result_vec3)
#> [1] TRUE
# Compare performance
library(microbenchmark)
microbenchmark(
loop = {
result <- numeric(length(x))
for (i in seq_along(x)) {
if (x[i] %% 2 == 0) result[i] <- x[i]^2
else result[i] <- x[i]^3
}
},
ifelse = ifelse(x %% 2 == 0, x^2, x^3),
case_when = case_when(x %% 2 == 0 ~ x^2, TRUE ~ x^3),
logical_index = {
result <- numeric(length(x))
even <- x %% 2 == 0
result[even] <- x[even]^2
result[!even] <- x[!even]^3
},
times = 100
)
#> Unit: microseconds
#> expr min lq mean median uq max
#> loop 4252.890 4729.1255 5265.68637 4931.9740 5408.7665 23887.811
#> ifelse 46.541 53.3485 59.63407 56.2125 60.6575 151.563
#> case_when 210.291 255.8740 319.65934 305.8085 372.7440 558.738
#> logical_index 33.349 39.0780 50.10506 43.2585 46.7335 488.479
#> neval
#> 100
#> 100
#> 100
#> 100Exercise 3:
find_first <- function(x, condition) {
# Validate inputs
if (!is.function(condition)) {
stop("condition must be a function")
}
if (length(x) == 0) {
return(list(index = NA, value = NA, found = FALSE))
}
# Search with early exit
for (i in seq_along(x)) {
if (condition(x[i])) {
return(list(
index = i,
value = x[i],
found = TRUE
))
}
}
# None found
list(index = NA, value = NA, found = FALSE)
}
# Test
data <- c(1, 3, 5, 8, 2, 9, 4)
# Find first > 5
find_first(data, function(x) x > 5)
#> $index
#> [1] 4
#>
#> $value
#> [1] 8
#>
#> $found
#> [1] TRUE
# Find first even
find_first(data, function(x) x %% 2 == 0)
#> $index
#> [1] 4
#>
#> $value
#> [1] 8
#>
#> $found
#> [1] TRUE
# Find first > 100 (none)
find_first(data, function(x) x > 100)
#> $index
#> [1] NA
#>
#> $value
#> [1] NA
#>
#> $found
#> [1] FALSE
# Empty vector
find_first(numeric(0), function(x) x > 5)
#> $index
#> [1] NA
#>
#> $value
#> [1] NA
#>
#> $found
#> [1] FALSE
# More complex condition
find_first(c("apple", "banana", "cherry"),
function(x) nchar(x) > 5)
#> $index
#> [1] 2
#>
#> $value
#> [1] "banana"
#>
#> $found
#> [1] TRUEExercise 4:
classify_grade <- function(score) {
# Validate input
if (!is.numeric(score)) {
stop("Score must be numeric")
}
# Vectorized function
sapply(score, function(s) {
# Check range
if (is.na(s)) {
return(NA_character_)
}
if (s < 0 || s > 100) {
warning("Score ", s, " is out of range (0-100)")
return("Invalid")
}
# Classify using switch on integer ranges
grade_num <- cut(s,
breaks = c(-Inf, 60, 70, 80, 90, Inf),
labels = FALSE)
switch(grade_num,
"F", # 1: 0-59
"D", # 2: 60-69
"C", # 3: 70-79
"B", # 4: 80-89
"A" # 5: 90-100
)
})
}
# Test
classify_grade(85)
#> [1] "B"
classify_grade(c(45, 65, 75, 85, 95))
#> [1] "F" "D" "C" "B" "A"
classify_grade(c(95, NA, 105, 65))
#> Warning in FUN(X[[i]], ...): Score 105 is out of range (0-100)
#> [1] "A" NA "Invalid" "D"
# More detailed version with +/-
classify_grade_detailed <- function(score) {
sapply(score, function(s) {
if (is.na(s)) return(NA_character_)
if (s < 0 || s > 100) return("Invalid")
if (s >= 97) return("A+")
if (s >= 93) return("A")
if (s >= 90) return("A-")
if (s >= 87) return("B+")
if (s >= 83) return("B")
if (s >= 80) return("B-")
if (s >= 77) return("C+")
if (s >= 73) return("C")
if (s >= 70) return("C-")
if (s >= 67) return("D+")
if (s >= 63) return("D")
if (s >= 60) return("D-")
return("F")
})
}
classify_grade_detailed(c(98, 88, 78, 68, 58))
#> [1] "A+" "B+" "C+" "D+" "F"