R Programming / Analysis interfaces and table integrity / Lesson 7
Iteration, vectorization, and the apply family
Choose a traversal whose input unit, output type, and empty-case behavior are explicit.
On this page
Lesson goals
- Distinguish elementwise operations from function iteration.
- Specify stable output types with vapply.
- Use safe indices and preallocated loop results.
Choose the unit that the function receives
A vectorized arithmetic operation works across a vector under its recycling and type rules. Applying a function over a list instead calls that function once for each component. These are different interfaces: a component might be one number, a whole group, or a structured record. Define the input unit and expected result before choosing a helper. A function designed for one complete group should not accidentally receive individual elements. Conversely, a scalar-only function may reject a group. The clarity of the traversal contract matters more than whether the code contains an explicit for keyword.
Specify the result shape instead of guessing it
lapply returns a list, preserving one result per input component. sapply may simplify results to a vector or matrix depending on what the function returns, so a later data change can alter the output structure. vapply requires a result template, making a scalar-summary contract explicit. The first fixture asks mean to return one numeric value for each group. Its empty case has length zero under the declared template. If one component returns a vector or incompatible type, treat the failure as evidence of a broken contract rather than replacing the template with a broader type without investigation.
groups <- list(a = c(2, 4), b = c(6, 8, 10))
means <- vapply(groups, mean, numeric(1))
cat("means=", paste(means, collapse = ","), "\n", sep = "")
empty <- vapply(list(), length, integer(1))
cat("empty length=", length(empty), "\n", sep = "")
means=3,8
empty length=0
vapply requires one compatible numeric summary per group; its declared result type remains explicit even for an empty input list.
An explicit loop can be a precise implementation
The second fixture allocates a numeric result of the required length and fills one position per input. seq_along produces exactly the valid indices, including no indices when the input is empty. The expression 1:length(x) does not share that property at length zero, because it constructs a sequence from one to zero. Preallocation also avoids repeatedly growing a vector with c inside the loop. This is a structural and allocation argument, not a measured speed claim. A well-specified loop can be clearer than a complicated chain of higher-order calls.
x <- c(1, 3, 5)
squares <- numeric(length(x))
for (i in seq_along(x)) {
squares[i] <- x[i]^2
}
cat("squares=", paste(squares, collapse = ","), "\n", sep = "")
cat("same=", identical(squares, x^2), "\n", sep = "")
cat("empty indices=", length(seq_along(numeric(0))), "\n", sep = "")
squares=1,9,25
same=TRUE
empty indices=0
Preallocation gives the loop a stable result size, while seq_along handles an empty vector without generating accidental indices.
Matrix traversal can coerce heterogeneous data
apply operates over margins of an array-like object and can coerce a data frame into a matrix first. If a character column forces the resulting matrix to character, a numeric summary may fail or receive the wrong representation. For heterogeneous columns, choose a list-oriented traversal and validate the selected columns. Row and column margins have different meanings, so check a non-square fixture and known values. Keep the names of the returned summaries aligned with the input components. Unlisting a structured result prematurely can erase boundaries that a later step needs.
Evaluate correctness and cost separately
Before optimizing traversal, compare values, type, length, names, missingness behavior, and empty cases against a simple reference. A helper named Vectorize provides function-call vectorization through iteration; its name does not promise a specialized fast numeric kernel. Large intermediate matrices can make a compact vectorized expression expensive in memory. Measure a realistic bounded case in the actual R environment before claiming a speed improvement, and preserve equivalent semantics. This course performs no R runtime or performance tests. The tiny fixtures provide derived expected behavior and make the chosen traversal independently reviewable.
Common errors and fixes
A summary changes from vector to matrix after a data update
- Why it happens
- Automatic simplification depended on returned lengths.
- How to fix it
- Use lapply for structured results or vapply with a justified template.
- Prevent it next time
- Test ordinary, singleton, empty, and inconsistent components.
A loop runs unexpectedly on empty input
- Why it happens
- 1:length(x) generated indices when length was zero.
- How to fix it
- Use seq_along or seq_len for the intended index contract.
- Prevent it next time
- Include zero-length input in traversal tests.
A numeric apply operation receives character values
- Why it happens
- A heterogeneous data frame was coerced to one matrix type.
- How to fix it
- Select numeric columns explicitly or use column-wise list traversal.
- Prevent it next time
- Check types before and after any matrix coercion.
Check your understanding
1. A list contains vectors of lengths two, one, and zero. Which helper expresses one integer length per component?
Show worked solution
Use vapply(x, length, integer(1)). Each call returns one integer length, so the expected result is 2,1,0. lapply would return a list of those scalars, while a permissive simplifier leaves more of the result contract implicit.
2. Why can a vectorized expression be slower or use more memory than a loop?
Show worked solution
It may allocate a large intermediate object or perform work that the loop avoids. The syntax alone does not describe the executed operations. Establish semantic equivalence, inspect allocations conceptually, and measure a bounded representative workload in the supported environment before choosing an optimization.
Key takeaways
- Traversal units and result templates are part of the API.
- Safe indices include the empty case.
- Concise syntax is not performance evidence.