SECTION 1
Historical Context & Motivation
The concept of a vector as a first-class data structure traces its lineage through decades of statistical computing language design. Unlike general-purpose languages such as C or Java, which treat arrays as contiguous memory blocks requiring explicit size declarations, R inherited a philosophy from its predecessors S and S-PLUS: every datum, even a single scalar, is fundamentally a vector. This design decision reflects the reality of statistical work, where operations on entire columns of data—means, standard deviations, regressions—are far more common than operations on individual numbers. Understanding how vectors are constructed is therefore not merely a syntactic exercise; it is the gateway to idiomatic, performant R programming.
1976 • S Language at Bell Labs
1988 • S Version 3 and c()
c() function for concatenating values into vectors, establishing the syntax that R would later adopt almost verbatim.1993 • R is Born
2000 • R 1.0.0 Release
c(), the : operator, and seq() as the canonical trio for vector creation, a convention that has remained unchanged for over two decades.The central question this lesson addresses is straightforward yet foundational: how do you efficiently construct vectors of arbitrary or patterned data in R? Whether you are assembling observed data points by hand, generating index sequences for subsetting, or producing evenly spaced grids for numerical analysis, the three tools—c(), :, and seq()—cover virtually every use case you will encounter.
SECTION 2
Core Principles & Definitions
Before examining syntax, it is essential to internalize several foundational principles that govern how R treats vectors. These principles explain not just how to create vectors but why certain behaviors—like automatic type coercion—occur during construction.
Atomic Homogeneity
logical, integer, double, character, or complex. Mixing types triggers implicit coercion following the hierarchy logical → integer → double → character.Scalars Are Length-1 Vectors
x <- 5 creates a numeric vector of length one. This uniformity means every vector creation tool works consistently regardless of the number of elements.c() Concatenates Recursively
c() function (short for "combine" or "concatenate") flattens nested calls, so c(1, c(2, 3)) yields [1] 1 2 3, not a nested structure.The Colon Operator Generates Integers
a:b generates a sequence from a to b incrementing (or decrementing) by 1. It has high operator precedence, which can produce surprising results in compound expressions if not parenthesized.seq() Provides Full Control
seq() function generalizes the colon operator by accepting from, to, by, and length.out parameters, enabling fractional steps and precise control over the number of generated elements.KEY TAKEAWAY
c() is the construction crew that installs them side by side. The colon operator and seq() are blueprints that tell the crew how many mailboxes to install and at what intervals to label them.SECTION 3
Visual Explanation — How Vectors Are Built
The following diagram illustrates the three primary mechanisms for constructing vectors in R. On the left, c() combines arbitrary values into a single flat vector. In the center, the colon operator produces a unit-step integer sequence. On the right, seq() offers configurable start, end, step size, and output length parameters.
c() (left, arbitrary values), : (center, unit-step sequences), and seq() (right, configurable sequences). Note the index labels [1], [2], … beneath each cell, reflecting R's 1-based indexing.Each panel in the diagram represents a distinct use case. The c() function is the most general tool—it accepts any combination of values and existing vectors, flattening them into a single one-dimensional structure. The colon operator is a syntactic shorthand optimized for generating consecutive integer-like sequences; it is particularly useful for loop counters and index ranges. Finally, seq() subsumes the colon operator's functionality while adding the ability to specify non-unit step sizes and to control the exact number of elements produced—an indispensable feature when constructing evaluation grids for numerical methods.
SECTION 4
How Vector Creation Works Internally
Under the hood, R allocates contiguous memory for vector storage, much like a C-style array. Understanding the semantics of each constructor clarifies edge cases—especially around type coercion and floating-point behavior—that can produce subtle bugs in production code.
c() — The Universal Concatenator
The function signature is c(..., recursive = FALSE). It accepts a variadic number of arguments and returns a single atomic vector. When elements of different types are mixed, R coerces all elements to the most general type in the hierarchy: logical < integer < double < character. For instance, c(TRUE, 3L, 2.5) coerces TRUE to 1L and then both integers to doubles, yielding c(1.0, 3.0, 2.5). Named elements can be specified inline: c(x = 1, y = 2) produces a named numeric vector.
TYPE COERCION HIERARCHY
Each arrow represents an implicit widening conversion. c() always coerces to the rightmost type present among its arguments.
The : Operator — Compact Integer Sequences
The expression a:b is syntactic sugar for seq.int(a, b). It generates a sequence starting at a, incrementing by +1 (or −1 if a > b), until it reaches or passes b. A critical caveat is operator precedence: the colon binds tighter than arithmetic operators, so 1:3 + 1 is parsed as (1:3) + 1, yielding 2 3 4 via vectorized addition, not 1:4. Always use parentheses when combining : with arithmetic.
COLON OPERATOR SEMANTICS
When a and b are both integers, the result is an integer vector. If either is a double (e.g., 1.5:4.5), the result is a double vector.
seq() — The General Sequence Generator
The full signature is seq(from, to, by, length.out, along.with). You may specify either by (step size) or length.out (desired number of elements), but not both simultaneously—R computes the missing parameter from the relationship: length.out = (to − from) / by + 1. The along.with argument is a convenience that sets length.out = length(along.with), producing an index vector matching the length of an existing object. Two lightweight variants—seq_len(n) and seq_along(x)—are preferred in production code because they handle edge cases (such as zero-length input) more safely than 1:length(x).
seq() RELATIONSHIP
When length.out is specified instead of by, R computes by = (to − from) / (length.out − 1), guaranteeing that both endpoints are included.
SECTION 5
Detailed Breakdown — Variants and Edge Cases
Beyond the three primary constructors, R offers several related functions and patterns for generating vectors. Understanding these variants—and the edge cases that arise when inputs are unusual—distinguishes a proficient R programmer from a novice.
c(), :, and seq() based on the properties of the desired vector. The bottom banner highlights the defensive-programming variants seq_len() and seq_along().Edge Cases and Common Pitfalls
| Expression | Result | Explanation |
|---|---|---|
c() | NULL | No arguments produces NULL, not an empty vector. Use integer(0) for an empty typed vector. |
1:0 | 1 0 | The colon generates a descending sequence when from > to. This is valid but surprising when used as 1:length(x) and x has length 0. |
seq_len(0) | integer(0) | Safely returns a zero-length integer vector, avoiding the 1:0 trap. |
c(1, "two") | "1" "two" | The numeric 1 is silently coerced to the character "1". This is a common source of downstream type errors. |
seq(0, 1, by=0.3) | 0.0 0.3 0.6 0.9 | The endpoint 1.0 is excluded because 0.9 + 0.3 = 1.2 > 1.0. Use length.out if both endpoints must appear. |
Common edge cases when constructing vectors in R
Defensive Programming Tip
for (i in seq_along(x)) instead of for (i in 1:length(x)). When x has length 0, 1:0 produces c(1, 0) and the loop body executes twice on invalid indices—a classic off-by-one bug.SECTION 6
Worked Example — Building a Simulation Grid
Suppose you are preparing a Monte Carlo simulation where you need to evaluate a function at 100 evenly spaced points between 0 and 2π, and you also need a vector of the first 10 positive integers for loop indexing plus a manually specified vector of sample sizes.
Building Vectors for a Simulation
- 1
Step 1 — Create the sample-size vector with c()
We manually specify five sample sizes that the simulation will iterate over. Since these are arbitrary, non-sequential values,c()is the appropriate tool.sample_sizes <- c(50, 100, 500, 1000, 5000) - 2
Step 2 — Create an index vector with the colon operator
For iterating over 10 replications, we need the integers 1 through 10. The colon operator is ideal for this unit-step integer sequence.reps <- 1:10→[1] 1 2 3 4 5 6 7 8 9 10 - 3
Step 3 — Create the evaluation grid with seq()
We need exactly 100 points spanning the interval [0, 2π]. Since 2π/99 is an irrational step size, we specify the desired number of elements rather than a step. This guarantees that both 0 and 2π appear in the output.theta <- seq(from = 0, to = 2 * pi, length.out = 100) - 4
Step 4 — Verify the constructed vectors
We uselength(),typeof(), andhead()to inspect each vector and confirm correct construction. This is good practice before passing vectors into expensive computations.length(theta) # 100 typeof(theta) # "double" head(theta, 4) # 0.000 0.0635 0.1269 0.1904 - 5
Step 5 — Combine results using c()
After the simulation, suppose we want to concatenate partial result vectors from two runs. Sincec()flattens its arguments, this is straightforward.all_results <- c(results_run1, results_run2)
SECTION 7
Strengths, Limitations & When to Use Each
Each vector constructor occupies a distinct niche in terms of expressiveness, performance, and safety. The following comparison table summarizes their characteristics to aid in choosing the right tool for a given task.
| Feature | c() | : (colon) | seq() |
|---|---|---|---|
| Arbitrary values | ✓ Yes | ✗ No | ✗ No |
| Custom step size | N/A | ✗ Fixed ±1 | ✓ via by |
| Exact output length | Manual (count args) | Implicit from range | ✓ via length.out |
| Named elements | ✓ Yes | ✗ No | ✗ No |
| Safe for 0-length | Returns NULL | ✗ 1:0 bug | ✓ seq_len(0) → integer(0) |
| Typical use case | Assembling data, merging vectors | Quick index ranges, loop counters | Evaluation grids, linspace-like sequences |
Feature comparison of R's three primary vector constructors
KEY TAKEAWAY
c() as manually placing books on a shelf in any order, the colon operator as telling someone to number shelves 1 through n, and seq() as a CNC machine that cuts shelves at precisely specified intervals. When precision and edge-case safety matter—as they do in research-quality code—seq() and its variants (seq_len, seq_along) are the safest choice.SECTION 8
Connection to Advanced Data Structures
Vectors are not just a standalone concept—they are the atomic building blocks from which every complex data structure in R is assembled. Matrices and arrays are vectors with a dim attribute, data frames are lists of equal-length vectors, and even factors are integer vectors with a levels attribute. Mastering vector creation thus directly transfers to constructing and manipulating these higher-order structures.
| Concept from This Lesson | Advanced Extension | Example |
|---|---|---|
c() for flat vectors | list() for heterogeneous collections | list(name="Ada", age=36, scores=c(95,87)) |
seq() for 1D grids | expand.grid() for multi-dimensional grids | expand.grid(x=seq(0,1,0.1), y=seq(0,1,0.1)) |
Coercion in c() | Factor encoding via factor() | factor(c("low","med","high"), ordered=TRUE) |
Vectorized creation with rep() | Matrix construction via matrix() | matrix(1:12, nrow=3, ncol=4) |
How vector concepts scale to advanced R data structures
As you progress to working with the tidyverse and data.table ecosystems, the fluency you develop now with atomic vector creation will pay dividends. Column operations in dplyr's mutate(), filtering with logical vectors, and constructing model matrices all rely on the same principles of type homogeneity, coercion, and sequence generation covered in this lesson.
SECTION 9
Practice Problems
Question 1: CONCEPTUAL
c(TRUE, 3L, 2.5, "hello") produces a character vector rather than a numeric or logical one. What is the coercion hierarchy that R follows, and why does it exist?Show answer
"hello" is a character value—the most general type—all other elements are coerced to character: TRUE becomes "TRUE", 3L becomes "3", and 2.5 becomes "2.5". This hierarchy exists because every value can be represented as a string without loss of identity (though numeric semantics are lost), making character the "widest" type.Question 2: BASIC CALCULATION
seq() to generate the sequence 2, 4, 6, 8, 10. Then write an equivalent expression using the colon operator and vectorized arithmetic.Show answer
seq(from = 2, to = 10, by = 2). Using the colon operator with vectorized multiplication: 2 * (1:5). Both produce the numeric vector [1] 2 4 6 8 10. The parentheses around 1:5 are technically unnecessary here (since : binds tighter than *), but they improve readability and guard against precedence surprises.Question 3: INTERMEDIATE
Show answer
x <- seq(from = -pi, to = pi, length.out = 50). When length.out is specified, R computes by = (to − from) / (length.out − 1) = (π − (−π)) / 49 = 2π / 49 ≈ 0.1282. This formula ensures that both endpoints are included in the output. The resulting vector has exactly 50 elements uniformly partitioning the interval [−π, π].Question 4: APPLIED
x and applies a transformation to each element. Your loop currently uses for (i in 1:length(x)). A colleague passes x <- c() (an empty input). Describe the bug that occurs and rewrite the loop header to fix it.Show answer
x is empty, length(x) returns 0, so 1:length(x) evaluates to 1:0, which is the descending sequence c(1, 0). The loop body executes twice, attempting to access x[1] and x[0], both of which return unexpected results (NA or a zero-length vector). The fix: for (i in seq_along(x)). When x has length 0, seq_along(x) returns integer(0) and the loop body never executes.Question 5: CRITICAL THINKING
x <- seq(0, 1, by = 0.1); 0.3 %in% x. This returns TRUE on most systems. Now consider y <- seq(0, 1, by = 0.01); 0.29 %in% y. Under what circumstances might this return FALSE despite 0.29 appearing to be in the sequence? Propose a robust solution.Show answer
seq() may produce values like 0.29000000000000004 instead of exactly 0.29. The %in% operator uses exact equality (==), which fails for values that differ by even a single ULP (unit in the last place). A robust solution is to use any(abs(y - 0.29) < .Machine$double.eps^0.5), which checks for near-equality within machine tolerance. Alternatively, when exact membership testing is critical, construct the sequence using integer arithmetic and scale: y <- (0:100) / 100.SUMMARY
Summary — Creating Vectors in R
Summary
Vectors are the fundamental atomic data structure in R, and three primary tools exist for creating them. The c() function concatenates arbitrary values into a single flat vector, applying implicit type coercion when mixed types are present (following the hierarchy logical → integer → double → character). The colon operator (:) provides a concise way to generate unit-step sequences, though its high precedence demands careful parenthesization in compound expressions. The seq() function generalizes sequence creation with by and length.out parameters for full control over step size and output length.
For defensive programming, prefer seq_len(n) over 1:n and seq_along(x) over 1:length(x) to avoid the classic 1:0 bug on zero-length inputs. These vector creation tools are the foundation upon which matrices, data frames, lists, and all higher-order R structures are built.