R PROGRAMMING • CONTROL FLOW

For Loops — Use for loops over sequences and indices

Master iterative computation in R by looping over vectors, lists, and index sequences to automate repetitive tasks.

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
IBM releases FORTRAN, the first high-level language to feature the DO loop construct, introducing structured iteration over numeric ranges and establishing patterns that persist in modern languages.
1976
S Language at Bell Labs
John Chambers and colleagues create S, a language for statistical computing that includes for loops capable of iterating over arbitrary vector elements—a departure from purely index-based iteration.
1993
R Language Created
Ross Ihaka and Robert Gentleman develop R at the University of Auckland, inheriting S's for loop semantics along with its vectorized operations philosophy.
2000
R 1.0 Released
The stable release of R 1.0 brings a complete control flow toolkit—for, while, repeat—to the growing open-source statistics community, sparking widespread adoption in academia.
2014+
Tidyverse & Functional Alternatives
The purrr package introduces map functions as functional alternatives to for loops, prompting renewed discussion about when explicit iteration remains the clearest and most appropriate tool.

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.

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.

1

Element-Based Iteration

Using 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.
2

Index-Based Iteration

Using 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.
3

Sequence Generation

R provides multiple ways to generate sequences: the : operator, seq(), seq_len(), and seq_along(). Each has specific use cases and edge-case behaviors that matter in production code.
4

Pre-Allocation Pattern

Growing a vector inside a loop with 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).
5

Loop Control Keywords

The 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
Think of a for loop like an automated conveyor belt in a factory: the sequence is the belt carrying items, the loop variable is the workstation picking up one item at a time, and the loop body is the operation performed on each item. Whether you label items by their position on the belt (index-based) or simply grab whatever arrives (element-based) depends on whether your operation needs to know where in the sequence each item sits.

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.

The flowchart shows the three possible paths from the loop body: normal continuation returns to the element check, 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.

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
a:b → {a, a+1, a+2, …, b} when a ≤ b
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
seq_along(x) → {1, 2, …, length(x)} or integer(0) if length(x) = 0
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)
T(n) = Σᵢ₌₁ⁿ O(i) = O(n²/2) = O(n²)
Each call to result <- c(result, new_value) copies the entire vector of length i, yielding quadratic total time.
PRE-ALLOCATED INDEXING
T(n) = Σᵢ₌₁ⁿ O(1) = O(n)
Pre-allocating result <- numeric(n) and assigning via result[i] <- value performs constant-time writes, yielding linear total time.
⚠️ The 1:length() Trap
If your vector 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.

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.

Classification of the three primary for loop patterns in R: iterating over elements directly, over index positions via seq_along(), and over names of a named list or vector.
Summary of common for loop iteration patterns in R
PatternSyntaxUse WhenCaveat
Element-basedfor (x in vec)You only need values, not positionsCannot easily store results by position
Index-basedfor (i in seq_along(vec))You need position for output storage or neighbor accessSlightly more verbose
Name-basedfor (nm in names(lst))Accessing both the name and value of named elementsFails silently if names are NULL
Row-based (data frame)for (i in seq_len(nrow(df)))Processing data frame rows with side effectsSlower than vectorized or apply-family alternatives

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 OutputCreate 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_alongIterate 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 Iterationi=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 InterpretThe 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 position i - 1 of the output to compute position i, making element-based iteration insufficient.
Output verified: each element ≥ previous element ✓

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.

Comparison of iteration strategies in R
ApproachStrengthsLimitations
for loopExplicit control flow; handles side effects (file I/O, plotting); iteration i can depend on iteration i−1; easy to debug step-by-stepVerbose; requires manual pre-allocation; tempts copy-on-modify mistakes
Vectorized opsConcise; executed in optimized C code internally; no explicit loop variable managementOnly works when operations are element-wise and independent; cannot express sequential dependencies
apply / lapplyEliminates loop boilerplate; returns structured output; encourages functional styleNot inherently faster than well-written for loops; sapply can return unpredictable types
purrr::mapType-stable variants (map_dbl, map_chr); composable with pipes; consistent error messagesRequires tidyverse dependency; learning curve for users unfamiliar with functional paradigm
KEY TAKEAWAY
The advice to "never use for loops in R" is a persistent myth. In reality, a pre-allocated for loop runs at comparable speed to apply-family functions for most tasks. The real decision criterion is clarity: use vectorized operations when the transformation is element-wise, use for loops when iterations are sequentially dependent or involve side effects, and use functional alternatives like lapply or purrr::map when you want to express a clean input-to-output mapping without worrying about output container management.

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 concepts and their advanced counterparts
For Loop ConceptAdvanced ExtensionDescription
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 accumulationReduce(f, x, accumulate = TRUE)The Reduce function formalizes the fold/accumulate pattern, abstracting away the loop variable and index management entirely.
Nested for loopsouter(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 terminationCustom iterator objectsThe iterators package provides lazy iterator objects that generate values on demand, enabling for loops over data too large to fit in memory.

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.

Practice Problems

PROBLEM 1CONCEPTUAL
Explain why 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?
PROBLEM 2BASIC CALCULATION
Write an R for loop that computes the sum of all even numbers in the vector v <- c(3, 8, 15, 22, 7, 40). Use element-based iteration and the modulo operator %%.
PROBLEM 3INTERMEDIATE
Given a numeric vector 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.
PROBLEM 4APPLIED
You have a list of file paths: 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.
PROBLEM 5CRITICAL THINKING
Consider a function 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?

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().

Varsity Tutors • R Programming • For Loops — Use for loops over sequences and indices