SECTION 1
Historical Context & Motivation
The concept of iterative computation predates modern programming languages by centuries, but the for loop as a formal control structure crystallized during the development of high-level programming languages in the mid-twentieth century. Early programmers working on machines like the ENIAC had to manually rewire circuits to repeat operations; the introduction of stored-program architectures and languages like FORTRAN made it possible to express iteration declaratively. R, designed as a statistical computing environment in the 1990s, inherited its looping constructs from the S language developed at Bell Laboratories in the 1970s and 1980s. Understanding this lineage is essential because R's for loop syntax—iterating directly over elements of a vector rather than requiring explicit counter management—reflects a philosophy of treating data objects as first-class citizens. This design choice has profound implications for how statisticians and data scientists structure their analyses.
1957 • FORTRAN DO Loops
1976 • S Language at Bell Labs
1993 • R Language Created
2000 • R 1.0 Released
2014+ • Tidyverse & Functional Alternatives
The central question this lesson addresses is deceptively simple: how do you instruct R to perform the same operation across every element of a data structure? While R's vectorized functions handle many such cases implicitly, explicit for loops remain indispensable when operations have side effects, when each iteration depends on the previous result, or when the logic within each iteration is complex enough that a loop body is the clearest expression of intent.
SECTION 2
Core Principles & Definitions
At its core, R's for loop is a definite iteration construct that traverses every element of an iterable object exactly once. Unlike while loops, which continue until a Boolean condition evaluates to FALSE, a for loop's number of iterations is determined at entry by the length of the sequence provided. The syntax follows the pattern for (variable in sequence) { body }, where the loop variable takes on each value of the sequence in order. Understanding the distinction between iterating over elements directly versus iterating over index positions is the foundational insight that unlocks effective loop-based programming in R.
Element-Based Iteration
for (x in vec) assigns each element of vec to x in turn. This is idiomatic when you only need the values, not their positions.Index-Based Iteration
for (i in seq_along(vec)) or for (i in 1:length(vec)) gives access to both position and value via vec[i]. Prefer seq_along() for safety with zero-length vectors.Sequence Generation
: operator, seq(), seq_len(), and seq_along(). Each has specific use cases and edge-case behaviors that matter in production code.Pre-Allocation Pattern
c() is O(n²) because R copies the entire vector each iteration. Pre-allocating with vector("numeric", n) or numeric(n) reduces this to O(n).Loop Control Keywords
next (skip to the next iteration) and break (exit the loop entirely) provide fine-grained control over execution flow within the loop body.KEY TAKEAWAY
SECTION 3
Visual Explanation — Anatomy of a For Loop
The following diagram illustrates the execution flow of an R for loop. The loop begins by evaluating the sequence; on each pass, the loop variable is bound to the next element in that sequence, the body executes, and control returns to the top. When all elements have been consumed, the loop terminates and execution continues with the statement after the closing brace.
next skips the remainder of the body and jumps to the next element, and break terminates the loop immediately.Several details of this flow deserve emphasis. First, the sequence expression is evaluated exactly once, before the first iteration begins. Modifying the sequence object inside the loop body does not change the set of values iterated over—R captures the sequence at entry. Second, the loop variable persists in the enclosing environment after the loop completes, retaining the value from the final iteration. This behavior differs from languages with block-scoped loop variables and can be a source of subtle bugs if the variable name collides with other identifiers.
SECTION 4
How For Loops Work Under the Hood
While R's for loop does not involve mathematical equations in the traditional sense, understanding its computational complexity requires a precise model of how R manages memory during iteration. The two dominant patterns—element-based and index-based—have identical time complexity for simple read operations, but diverge sharply when the loop modifies a growing data structure.
Sequence Generation Functions
COLON OPERATOR
Generates an integer sequence from a to b inclusive. Caution: 1:0 yields c(1, 0), not an empty sequence—a common source of bugs.
SEQ_ALONG SAFETY
Unlike 1:length(x), seq_along(x) returns an empty integer vector when x has length zero, correctly producing zero iterations.
Complexity of Loop-Based Growth Patterns
NAIVE GROWTH (ANTI-PATTERN)
Each call to result <- c(result, new_value) copies the entire vector of length i, yielding quadratic total time.
PRE-ALLOCATED INDEXING
Pre-allocating result <- numeric(n) and assigning via result[i] <- value performs constant-time writes, yielding linear total time.
The 1:length() Trap
x might be empty, never write for (i in 1:length(x)). When length(x) is 0, 1:0 evaluates to c(1, 0), causing the loop to execute twice with invalid indices. Use seq_along(x) or seq_len(length(x)) instead.SECTION 5
Common For Loop Patterns in R
R programmers encounter several recurring loop patterns depending on whether they iterate over atomic vectors, lists, data frame rows, or nested structures. The diagram below classifies these patterns by showing the relationship between the iterable type, the loop variable binding, and the access mechanism used inside the body.
seq_along(), and over names of a named list or vector.| Pattern | Syntax | Use When | Caveat |
|---|---|---|---|
| Element-based | for (x in vec) | You only need values, not positions | Cannot easily store results by position |
| Index-based | for (i in seq_along(vec)) | You need position for output storage or neighbor access | Slightly more verbose |
| Name-based | for (nm in names(lst)) | Accessing both the name and value of named elements | Fails silently if names are NULL |
| Row-based (data frame) | for (i in seq_len(nrow(df))) | Processing data frame rows with side effects | Slower than vectorized or apply-family alternatives |
Summary of common for loop iteration patterns in R
SECTION 6
Worked Example — Computing a Running Maximum
Suppose you have a numeric vector of daily stock prices and want to compute the running maximum—the highest price observed up to and including each day. This is a classic case where a for loop is natural because each output depends on the previous computed value.
Running Maximum via Index-Based For Loop
- 1
Step 1 — Define Input and Pre-Allocate Output
Create the input vector and pre-allocate the output vector to the same length. Set the first element of the output equal to the first element of the input, since the running maximum at day 1 is simply the day-1 price.prices <- c(102, 98, 105, 103, 110, 107)run_max <- numeric(length(prices))run_max[1] <- prices[1]run_max = (102, 0, 0, 0, 0, 0) - 2
Step 2 — Write the For Loop with seq_along
Iterate starting from index 2 through the end of the vector. At each position, compare the current price to the previous running maximum and take the larger value.for (i in 2:length(prices)) {run_max[i] <- max(prices[i], run_max[i - 1])}Loop executes for i = 2, 3, 4, 5, 6 - 3
Step 3 — Trace Iteration by Iteration
i=2: max(98, 102) = 102. i=3: max(105, 102) = 105. i=4: max(103, 105) = 105. i=5: max(110, 105) = 110. i=6: max(107, 110) = 110.run_max = (102, 102, 105, 105, 110, 110) - 4
Step 4 — Verify and Interpret
The result is a monotonically non-decreasing vector where each position stores the highest price seen so far. This pattern exemplifies why index-based loops with pre-allocation are preferred: we need positioni - 1of the output to compute positioni, making element-based iteration insufficient.Output verified: each element ≥ previous element ✓
SECTION 7
For Loops vs. Vectorized & Functional Alternatives
R's ecosystem provides several alternatives to explicit for loops, including vectorized operations, the apply family of functions (sapply, lapply, vapply), and the purrr::map functions from the tidyverse. Choosing among these is less about performance in modern R (where well-written loops are fast) and more about readability, composability, and the nature of the side effects involved.
| Approach | Strengths | Limitations |
|---|---|---|
| for loop | Explicit control flow; handles side effects (file I/O, plotting); iteration i can depend on iteration i−1; easy to debug step-by-step | Verbose; requires manual pre-allocation; tempts copy-on-modify mistakes |
| Vectorized ops | Concise; executed in optimized C code internally; no explicit loop variable management | Only works when operations are element-wise and independent; cannot express sequential dependencies |
| apply / lapply | Eliminates loop boilerplate; returns structured output; encourages functional style | Not inherently faster than well-written for loops; sapply can return unpredictable types |
| purrr::map | Type-stable variants (map_dbl, map_chr); composable with pipes; consistent error messages | Requires tidyverse dependency; learning curve for users unfamiliar with functional paradigm |
Comparison of iteration strategies in R
KEY TAKEAWAY
SECTION 8
Connection to Advanced Iteration Concepts
The for loop serves as the conceptual foundation for several more advanced iteration paradigms in R and in computing generally. Understanding how explicit for loops map onto these abstractions will prepare you for parallel computing, functional programming, and the design of domain-specific iterators.
| For Loop Concept | Advanced Extension | Description |
|---|---|---|
for (i in 1:n) | foreach(i = 1:n) %dopar% | The foreach package parallelizes independent iterations across CPU cores, turning sequential loops into concurrent computations. |
| Sequential accumulation | Reduce(f, x, accumulate = TRUE) | The Reduce function formalizes the fold/accumulate pattern, abstracting away the loop variable and index management entirely. |
| Nested for loops | outer(x, y, FUN) | The outer() function replaces double for loops that compute a function over all pairs of elements from two vectors. |
| Loop with early termination | Custom iterator objects | The iterators package provides lazy iterator objects that generate values on demand, enabling for loops over data too large to fit in memory. |
For loop concepts and their advanced counterparts
As you progress into high-performance computing with R, you will find that many optimization strategies—such as byte-compilation via compiler::cmpfun() or rewriting hot loops in C++ via Rcpp—begin with a correctly structured for loop that is then profiled and selectively replaced. The ability to write clear, correct for loops is therefore not a beginner's crutch but a professional skill that anchors more sophisticated engineering work.
SECTION 9
Practice Problems
Question 1: CONCEPTUAL
for (i in 1:length(x)) can produce incorrect behavior when x is an empty vector (i.e., length(x) == 0). What would you use instead, and why?Show answer
length(x) is 0, the expression 1:0 evaluates to the vector c(1, 0), causing the loop to execute twice with invalid indices rather than zero times. This occurs because the colon operator always produces a non-empty sequence by counting down when the left operand exceeds the right. The safe alternative is seq_along(x), which returns integer(0) for zero-length input, correctly resulting in zero iterations. Alternatively, seq_len(length(x)) also handles this edge case correctly.Question 2: BASIC CALCULATION
v <- c(3, 8, 15, 22, 7, 40). Use element-based iteration and the modulo operator %%.Show answer
total <- 0, then write for (x in v) { if (x %% 2 == 0) total <- total + x }. The loop visits each element: 3 is odd (skip), 8 is even (total=8), 15 is odd (skip), 22 is even (total=30), 7 is odd (skip), 40 is even (total=70). The final result is 70. Note that this is an element-based loop because we only need values, not positions—we never write to a specific index.Question 3: INTERMEDIATE
temps <- c(72, 68, NA, 75, NA, 80, 77) representing daily temperatures with missing values, write a for loop that replaces each NA with the most recent non-NA value ("last observation carried forward"). Use index-based iteration.Show answer
for (i in 2:length(temps)) { if (is.na(temps[i])) temps[i] <- temps[i - 1] }. Starting from index 2, at i=3 we find NA and replace it with temps[2]=68. At i=5 we find NA and replace it with temps[4]=75. The final vector is c(72, 68, 68, 75, 75, 80, 77). This problem requires index-based iteration because each replacement depends on the element at position i−1 in the (already modified) vector. Note that this assumes the first element is not NA; a robust implementation would check for that edge case.Question 4: APPLIED
files <- list.files("data/", pattern = "\\.csv$", full.names = TRUE). Write a for loop that reads each CSV file, appends a column indicating the source file name, and combines all data frames into one. Pre-allocate a list for the results.Show answer
dfs <- vector("list", length(files)). Then use for (i in seq_along(files)) { dfs[[i]] <- read.csv(files[i]); dfs[[i]]$source <- basename(files[i]) }. Finally, combine with result <- do.call(rbind, dfs). The key design decisions are: (1) use index-based iteration so results go into pre-allocated list positions, (2) use seq_along() to handle the edge case of zero files, and (3) defer row-binding until after the loop to avoid quadratic copy overhead.Question 5: CRITICAL THINKING
f(x) that you want to apply to each element of a vector of length n, storing results in an output vector. Analyze the asymptotic time complexity of three approaches: (a) growing the output with c() in a for loop, (b) pre-allocating and indexing in a for loop, and (c) using vapply(). Under what conditions might approach (a) appear to work fine during development but fail in production?Show answer
c(result, new_value) copies the entire result vector, yielding total copy operations proportional to 1+2+…+n = n(n+1)/2. Approach (b) is O(n×T(f)) where T(f) is the cost of one call to f, because each indexed assignment is O(1). Approach (c) has essentially the same O(n×T(f)) complexity as (b) since vapply pre-allocates internally. The insidious nature of approach (a) is that with small development datasets (n ≈ 100–1000), the quadratic cost is imperceptible. In production with n ≈ 10⁶, runtime explodes: a 1-second task at n=1000 becomes approximately 1,000,000 seconds (~11.6 days) at n=10⁶ under the O(n²) pattern, versus ~1000 seconds under O(n). This is a textbook example of why algorithmic complexity analysis matters even in a high-level language.SUMMARY
Summary
Summary
R's for loop iterates over every element of a sequence using the syntax for (variable in sequence) { body }. You can iterate directly over element values when you only need the data, or over index positions via seq_along() when you need to store results by position or access neighboring elements. Always prefer seq_along() over 1:length() to safely handle zero-length inputs.
Performance hinges on pre-allocation: growing a vector with c() inside a loop incurs O(n²) cost, while pre-allocating and assigning by index is O(n). The loop control keywords next and break provide fine-grained flow control. For loops remain the clearest choice when iterations have sequential dependencies or side effects; when the operation is element-wise and independent, prefer vectorized operations or functional alternatives like vapply() and purrr::map().