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.

A statistical scatter plot with code braces and a variable symbol for R programming.
Inside this lesson 10 sections 5 questions 9 figures

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

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.

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.

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.

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.

Execution Flow of an R For Loopfor (var in sequence)More elements?NOLoop endsYESBind var ← next elementExecute loop body { ... }next / break?nextbreakExit loopnormal
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.

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

a:b→a,a+1,a+2,…,bwhena≤ba: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

seqalong(x)→1,2,…,length(x)orinteger(0)iflength(x)=0seq_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)=Σi=1nO(i)=O(n2/2)=O(n2)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)=Σi=1nO(1)=O(n)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.

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.

For Loop Iteration Patterns in Rfor (var in ...)Over ElementsOver IndicesOver NamesVectorfor (x in vec)print(x)Listfor (el in lst)process(el)seq_along + Accessfor (i in seq_along(v))out[i] <- f(v[i])Named Accessfor (nm in names(lst))cat(nm, lst[[nm]])Best Practices• Pre-allocate output • Use seq_along() for index loops • Prefer element-based when indices not needed
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.
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

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. 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. 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. 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. 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 position i - 1 of the output to compute position i, 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.

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

Comparison of iteration strategies in R

✦

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.

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

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

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?
Show answer
When 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

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 %%.
Show answer
Initialize an accumulator 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

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.
Show answer
Use 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

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.
Show answer
Pre-allocate with 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

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?
Show answer
Approach (a) is O(n²) because each 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().

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