Atomic Vectors and Types

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

Checked against R 4.6.1 (2026-06-24) on 2026-10-20; R 4.6.2 is scheduled for 2026-10-29. This page is about the one data structure that everything else in R is made of: the vector. It starts with the six atomic types and the four functions that report what a value is (typeof(), mode(), storage.mode() and class()), goes through the is.*() tests and the as.*() conversions with the exact warning texts they produce, explains the coercion hierarchy that decides the type of c(), shows every way of creating a vector (c(), vector() and its typed shortcuts, the colon operator, seq(), seq_len(), seq_along() and rep()), and then covers recycling, length(x) <- n, zero-length vectors, raw, character and complex vectors, the way attributes turn a vector into a matrix, a factor, a date or a data frame, the change of is.atomic(NULL) in R 4.4.0, and long vectors. Everything on this page is base R, so no add-on package version applies; the lintr 3.4.0 default linters were used only to check the scripts.

Every example below is a complete script that was run with Rscript on R 4.6.1 with warnings printed as they happen (options(warn = 1)), and the lines starting with #> are what it printed. The scripts also pass the default lintr linters; where an idiom being shown is exactly what a default linter dislikes (1:length(x), a single-quoted string, NA_character_ == "one") the line carries a # nolint: comment so that the linter does not hide the lesson. A warning or an error that a script demonstrates on purpose is caught with tryCatch() or withCallingHandlers() and printed as its conditionMessage(), so the text shown is exactly what R says. The names books, loans, shelf_today and loan_days come from the library-shelf scenario described on the section index; the scripts that use them start with source("scenario.R"), which reads books.csv and loans.csv. The page deliberately stops at the edge of the neighbouring topics: NA, NaN, Inf and NULL as values are in Missing and Special Values, floating-point arithmetic and integer overflow are in Numbers and Arithmetic, and the full rules of [, [[ and $ are in Subsetting and Indexing.

The vector family at a glance

A programmer coming from C, Java or Python looks for a scalar type and does not find one. The R language definition says it plainly: single numbers such as 4.2 and strings such as "four point two" are still vectors of length one, there are no more basic types, and vectors of length zero are possible and useful (The R Language Definition: Vectors). The manual calls a vector made of one of the six basic types an atomic vector, and a second kind of vector, the list (a "generic vector"), can hold elements of any type, including other lists. A list is therefore called recursive, and the atomic types are called atomic where it is necessary to exclude lists (The R Language Definition: Lists). Everything that looks like a richer data structure (a matrix, a factor, a date, a data frame) is one of these two kinds of vector with attributes attached, which the figure summarises.

Diagram of the R vector family. A top panel shows the six atomic types raw, logical, integer, double, complex and character with a sample value and the mode of each, and an arrow for implicit coercion towards the highest type. A middle panel shows NULL (type NULL, length 0, not atomic since R 4.4.0) and the list as a recursive vector. A bottom panel shows attributes turning an integer vector into a matrix with dim, an integer vector into a factor with levels and class, a double vector into a Date with class, and a list of equal-length columns into a data frame with names, row.names and class.

The next table lists each structure with what it is underneath and the attribute that makes it so; the other pages of this part of the reference take the rows one by one.

Structure What it is underneath (typeof()) What makes it that structure Where it is covered

Atomic vector

one of "logical", "integer", "double", "complex", "character", "raw"

nothing: no attribute is needed

this page

NULL

its own type, "NULL", length 0

nothing: there is exactly one NULL

Missing and Special Values

List

"list", a vector of references to other objects

nothing for a plain list; names for a named one

Lists

Matrix and array

an atomic vector (or a list)

the dim attribute, and dimnames for labels

Matrices and Arrays

Factor

"integer" codes

the levels and class attributes

Factors

Date and date-time

"double" (days or seconds)

the class attribute ("Date", "POSIXct")

Dates and Times

Data frame

"list" of equal-length columns

names, row.names and class = "data.frame"

Data Frames

The manual for beginners states the same split from the other side: vectors are atomic structures because all of their components are of one type, or mode (numeric, complex, logical, character and raw), and a vector must be unambiguously one of them; lists are recursive structures because their components can themselves be lists (An Introduction to R: Intrinsic attributes: mode and length). The one apparent exception to "all elements have the same type" is the special value NA, which turns out to come in one typed variant per atomic type; it is described in Missing and Special Values.

The six atomic types

The language definition names the six basic vector types as logical, integer, real, complex, character and raw. The word "real" is the old name of what typeof() now calls "double": the name of the type is double, while numeric is the name of the mode and of the implicit class. The help page of numeric calls this a historical anomaly and explains the consequence that matters in practice: is.numeric() tests the mode, so it is true for integers and doubles alike, but as.numeric() is identical to as.double() and always produces a double (R documentation: numeric).

Type typeof() Literals Create Test Convert Missing value

logical

"logical"

TRUE, FALSE

logical(n)

is.logical()

as.logical()

NA

integer

"integer"

1L, 0x10L, 1e3L

integer(n)

is.integer()

as.integer()

NA_integer_

double

"double"

1, 1.5, 1e3, 0x10

double(n) or numeric(n)

is.double() (and is.numeric(), which also accepts integers)

as.double() or as.numeric()

NA_real_

complex

"complex"

2i (and 3+2i, which is a sum)

complex(n)

is.complex()

as.complex()

NA_complex_

character

"character"

"a", 'a', r"(a)"

character(n)

is.character()

as.character()

NA_character_

raw

"raw"

none: use as.raw(1)

raw(n)

is.raw()

as.raw()

none

The functions in the table live on a few shared help pages: as.numeric() and is.numeric() on numeric, as.integer() and is.integer() on integer, as.logical() on logical, as.character() on character, as.complex() on complex and as.raw() on raw. Looking up a function by its name with ?as.integer opens the right page, as Getting Help and Documentation explains.

Literals and what the parser makes of them

A number written in a script is a double unless it carries the suffix L, which asks for an integer. The suffix works on decimal, hexadecimal and exponent forms. When the number cannot be an integer (it has a fractional part or it is outside the 32-bit range), the parser keeps the double and warns, which the script below catches with parse() so that the exact texts can be shown. The raw string r"(...)" was added in R 4.0.0 (R NEWS: Changes in R 4.0.0) and is a character constant like any other. The syntax of numeric and string constants in general is in Syntax and Lexical Structure.

# How the parser reads constants: the suffix L makes an integer.
typeof(42)
#> [1] "double"
typeof(42L)
#> [1] "integer"
typeof(0x10L)
#> [1] "integer"
typeof(1e3)
#> [1] "double"
typeof(1e3L)
#> [1] "integer"
typeof(0x10)
#> [1] "double"
typeof(TRUE)
#> [1] "logical"
typeof(2i)
#> [1] "complex"
typeof("text")
#> [1] "character"
typeof(r"(C:\no\escapes)")
#> [1] "character"
typeof(NA)
#> [1] "logical"
typeof(NA_integer_)
#> [1] "integer"
typeof(NA_real_)
#> [1] "double"
typeof(NA_character_)
#> [1] "character"

# A value that cannot be an integer keeps its double type and the parser
# says so (the warning is caught here so that the script exits cleanly).
parse_message <- function(code) {
  tryCatch(
    {
      parse(text = code)
      "no warning"
    },
    warning = \(w) conditionMessage(w)
  )
}
parse_message("1.5L")
#> [1] "integer literal 1.5L contains decimal; using numeric value"
parse_message("3000000000L")
#> [1] "non-integer value 3000000000L qualified with L; using numeric value"
parse_message("12L")
#> [1] "no warning"

Single values are vectors, and empty vectors have a type

Because there is no scalar, length(1.5) is 1 and a string is one element of a character vector, whatever its number of characters. nchar() counts characters; length() counts elements. A vector can have length zero and still have a type: character(0) and numeric(0) are different objects, and the type is what lets sum(numeric(0)) return 0 (the identity of addition) instead of failing. The next section looks at the types with four different functions.

typeof(), mode(), storage.mode() and class()

R has four functions that answer "what is this value?", and they answer different questions. They agree for most atomic vectors, which is why many tutorials treat them as synonyms, and they disagree in exactly the cases that matter.

Function What it reports

typeof(x)

The internal storage type of the object: what the interpreter and C code see. For atomic vectors it is one of "logical", "integer", "double", "complex", "character", "raw"; for other objects it includes "list", "NULL", "closure", "special", "builtin", "environment", "symbol", "language" and a few that are rarely seen at user level.

mode(x)

A coarser classification inherited from the S language: integers and doubles are both "numeric", the three function types are "function", the type "symbol" is called "name" and "language" is "call" (or "(").

storage.mode(x)

Like typeof() for data, except that every function type is reported as "function". It is what storage.mode(x) <- value changes.

class(x)

The class used by S3 dispatch and by print(): the class attribute when there is one, otherwise an implicit class derived from the type and the dimensions ("matrix" and "array" for objects with dim, "integer", "numeric", "function", …​).

The language definition gives the table that relates the first three for the six atomic types, and the script below builds the same table with real output and adds the fourth column. Note the integer and double rows: both have mode "numeric", so only typeof() and storage.mode() tell them apart, and the class of a double is "numeric", not "double".

# One value of each atomic type, and what four functions say about it.
examples <- list(
  logical = TRUE,
  integer = 1L,
  double = 1,
  complex = 1i,
  character = "a",
  raw = as.raw(1)
)
what_is <- vapply(examples, \(x) {
  c(
    typeof = typeof(x), mode = mode(x),
    storage.mode = storage.mode(x), class = class(x)
  )
}, character(4L))
print(t(what_is), quote = FALSE)
#>           typeof    mode      storage.mode class
#> logical   logical   logical   logical      logical
#> integer   integer   numeric   integer      integer
#> double    double    numeric   double       numeric
#> complex   complex   complex   complex      complex
#> character character character character    character
#> raw       raw       raw       raw          raw

# Single values are vectors of length 1; there is no scalar type.
length(1.5)
#> [1] 1
is.vector("four point two")
#> [1] TRUE
length("four point two")
#> [1] 1
nchar("four point two")
#> [1] 14

# A vector can be empty and still have a type.
c(typeof(character(0)), typeof(numeric(0)), typeof(logical(0)))
#> [1] "character" "double"    "logical"
length(integer(0))
#> [1] 0

The same functions on the other objects of the vector family show where they diverge. A matrix has the type of its elements and a class of length two; a factor is an integer underneath and its mode is "numeric"; a Date is a double; a data frame is a list; a function has three possible types and one mode; a formula is a call with class "formula"; a symbol is "name" to mode() and class() but "symbol" to typeof().

# typeof() is the storage type; class() is what S3 dispatch and print see.
things <- list(
  integer = 1:3,
  double = c(1.5, 2),
  matrix = matrix(1:6, nrow = 2),
  array = array(0, dim = c(2, 2, 2)),
  factor = factor(c("a", "b")),
  Date = as.Date("2026-06-30"),
  POSIXct = as.POSIXct("2026-06-30 09:30:00", tz = "UTC"),
  list = list(1, "a"),
  data.frame = data.frame(x = 1:2),
  closure = function(x) x,
  builtin = sum,
  special = `if`,
  formula = y ~ x,
  symbol = quote(x),
  NULL = NULL
)
summary_row <- \(x) {
  c(
    typeof = typeof(x), mode = mode(x),
    class = paste(class(x), collapse = " ")
  )
}
print(t(vapply(things, summary_row, character(3L))), quote = FALSE)
#>            typeof   mode     class
#> integer    integer  numeric  integer
#> double     double   numeric  numeric
#> matrix     integer  numeric  matrix array
#> array      double   numeric  array
#> factor     integer  numeric  factor
#> Date       double   numeric  Date
#> POSIXct    double   numeric  POSIXct POSIXt
#> list       list     list     list
#> data.frame list     list     data.frame
#> closure    closure  function function
#> builtin    builtin  function function
#> special    special  function function
#> formula    language call     formula
#> symbol     symbol   name     name
#> NULL       NULL     NULL     NULL

The length of class(x) is not always one. R 4.0.0 made every matrix inherit from the class "array", so class(matrix(1:6, nrow = 2)) is c("matrix", "array"), and NEWS said at the time that this invalidates code that wrongly assumes class(matrix_obj) has length one (R NEWS: Changes in R 4.0.0). A comparison such as class(m) == "matrix" then returns a vector of length two, and as the condition of an if() it is an error since R 4.2.0. Write inherits(m, "matrix") or is.matrix(m) instead; both give a single TRUE or FALSE. How dispatch uses the class is the subject of S3 Classes and Generics.

The implicit class

An object without a class attribute still has a class, computed from the type and the dimensions, and S3 dispatch uses a longer implicit class than class() prints: for an integer it is "integer" then "numeric", for a double "double" then "numeric", and for a matrix "matrix", "array" and then the type’s classes. The internal helper .class2() shows the dispatch vector. The practical consequence is that a method written for "numeric" is found for both integers and doubles, and that inherits(1L, "numeric") is FALSE although is.numeric(1L) is TRUE: inherits() looks at class(), is.numeric() at the mode.

# class() reports one thing; dispatch uses the implicit class, which is longer.
class(1L)
#> [1] "integer"
class(1)
#> [1] "numeric"
.class2(1L)
#> [1] "integer" "numeric"
.class2(1)
#> [1] "double"  "numeric"
.class2(matrix(1:4, nrow = 2))
#> [1] "matrix"  "array"   "integer" "numeric"
inherits(1L, "integer")
#> [1] TRUE
inherits(1L, "numeric")
#> [1] FALSE
is.numeric(1L)
#> [1] TRUE
inherits(1, "numeric")
#> [1] TRUE
inherits(1, "double")
#> [1] FALSE
mode(1L) == "numeric"
#> [1] TRUE

Changing the storage mode

storage.mode(x) <- "integer" and mode(x) <- "character" convert the values and keep the attributes, which is the difference from as.integer() and as.character(): those are as.vector()-style conversions that strip every attribute, including dim and names (the help page of as.integer says so and recommends storage.mode(x) <- "integer" to change the type without stripping attributes). mode<- only works where an as.<mode> function exists, while storage.mode<- is the more efficient primitive form for the internal types.

# storage.mode<- changes the type and keeps the attributes;
# as.integer() changes the type and drops them.
m <- matrix(c(1, 2, 3, 4), nrow = 2)
typeof(m)
#> [1] "double"
storage.mode(m) <- "integer"
typeof(m)
#> [1] "integer"
m
#>      [,1] [,2]
#> [1,]    1    3
#> [2,]    2    4
as.integer(m)
#> [1] 1 2 3 4

# mode<- is the same operation with the mode vocabulary.
x <- c(2.5, 3.5)
mode(x) <- "character"
x
#> [1] "2.5" "3.5"
mode(x) <- "numeric"
x
#> [1] 2.5 3.5

Testing the type: the is.* family

Each atomic type has an is.*() predicate: is.logical(), is.integer(), is.double(), is.complex(), is.character() and is.raw(), plus the less strict is.numeric(), is.atomic() and is.vector(). The first six are exact type tests. The last three are the ones that surprise, because each answers a slightly different question, and the next script puts them all on the same values. Read the table column by column.

# The is.*() predicates answer different questions, so they disagree.
values <- list(
  integer = 1:3,
  double = c(1.5, 2),
  factor = factor(c("a", "b")),
  Date = as.Date("2026-06-30"),
  logical = TRUE,
  character = "a",
  list = list(1),
  NULL = NULL,
  fn = sum
)
tests <- list(
  numeric = is.numeric, integer = is.integer, double = is.double,
  character = is.character, logical = is.logical, atomic = is.atomic,
  vector = is.vector
)
answers <- vapply(values, \(x) {
  vapply(tests, \(f) f(x), logical(1L))
}, logical(length(tests)))
print(t(answers))
#>           numeric integer double character logical atomic vector
#> integer      TRUE    TRUE  FALSE     FALSE   FALSE   TRUE   TRUE
#> double       TRUE   FALSE   TRUE     FALSE   FALSE   TRUE   TRUE
#> factor      FALSE   FALSE  FALSE     FALSE   FALSE   TRUE  FALSE
#> Date        FALSE   FALSE   TRUE     FALSE   FALSE   TRUE  FALSE
#> logical     FALSE   FALSE  FALSE     FALSE    TRUE   TRUE   TRUE
#> character   FALSE   FALSE  FALSE      TRUE   FALSE   TRUE   TRUE
#> list        FALSE   FALSE  FALSE     FALSE   FALSE  FALSE   TRUE
#> NULL        FALSE   FALSE  FALSE     FALSE   FALSE  FALSE  FALSE
#> fn          FALSE   FALSE  FALSE     FALSE   FALSE  FALSE  FALSE

is.numeric() on integers, doubles and factors

is.numeric() is true for integers and doubles because it tests the mode and not the type, but it is false for a factor, although a factor is an integer vector underneath and its mode() is "numeric". The help page explains that is.numeric() is an internal generic function: the default method returns true when the mode is numeric and the object is not a factor, and R ships methods for "Date", "POSIXt", "difftime" and "complex" that all return false, because arithmetic on a date is not arithmetic on a number. The rule for authors of new classes is that a method should return true only if the base type is double or integer and the values can reasonably be regarded as numbers. The consequence for code is that is.numeric(x) is the right check for "can I do arithmetic on this?", and is.double(x) or typeof(x) == "double" is the right check for "is this stored as a double?". Likewise is.integer(x) tests the type, not whether the values are whole numbers: is.integer(5) is FALSE, and the help page of integer points to a tolerance-based is.wholenumber() for the other question.

is.atomic(NULL) is FALSE since R 4.4.0

Before R 4.4.0 is.atomic(NULL) returned TRUE. The NEWS entry for R 4.4.0 says that is.atomic(NULL) now returns FALSE because NULL is not an atomic vector, and that strict back-compatibility would replace is.atomic(foo) by (is.null(foo) || is.atomic(foo)), a change that "should happen only sparingly" (R NEWS: Changes in R 4.4.0). The help page of is.atomic repeats the date and spells the old behaviour as a one-line function, which the script below uses. Code written for R 4.3 or older that tests is.atomic(x) to mean "x is a plain vector or nothing" will now treat NULL as the other case; code that tests is.atomic(x) and then calls length(x) or indexes it works either way, because NULL has length zero.

# is.atomic(NULL) is FALSE since R 4.4.0 (it was TRUE before).
is.atomic(NULL)
#> [1] FALSE
is.null(NULL)
#> [1] TRUE
is.vector(NULL)
#> [1] FALSE
typeof(NULL)
#> [1] "NULL"
length(NULL)
#> [1] 0

# The help page of is.atomic() gives the way to get the old answer back.
is_atomic_or_null <- function(x) is.atomic(x) || is.null(x)
is_atomic_or_null(NULL)
#> [1] TRUE
is_atomic_or_null(1:3)
#> [1] TRUE
is_atomic_or_null(list())
#> [1] FALSE

# Atomic and recursive are not complementary: NULL and symbols are neither.
atomic_or_recursive <- function(x) {
  c(is_atomic = is.atomic(x), is_recursive = is.recursive(x))
}
atomic_or_recursive(c(a = 1, b = 3))
#>    is_atomic is_recursive
#>         TRUE        FALSE
atomic_or_recursive(list(2))
#>    is_atomic is_recursive
#>        FALSE         TRUE
atomic_or_recursive(sum)
#>    is_atomic is_recursive
#>        FALSE         TRUE
atomic_or_recursive(quote(x))
#>    is_atomic is_recursive
#>        FALSE        FALSE
atomic_or_recursive(NULL)
#>    is_atomic is_recursive
#>        FALSE        FALSE

The last block of the script shows why "atomic" and "recursive" are not complementary: NULL and symbols are neither, and, a surprise at first sight, functions are recursive. A named vector is atomic and not recursive, a list is recursive and not atomic, and any code that branches on is.atomic(x) should decide what it wants to do with NULL, symbols and functions.

is.vector() and attributes

is.vector() has a narrower meaning than its name suggests. It returns TRUE only for a vector of the requested mode that has no attributes other than names. A plain vector, a named vector and a list pass; a matrix, a factor, a Date and any object with a class, dim or a custom attribute do not, although they are atomic. The mode argument defaults to "any"; mode = "numeric" accepts integers and doubles, while mode = "double" accepts only doubles, so is.vector(x, mode = "double") is close to typeof(x) == "double" plus the attribute rule. as.vector() is the other half: it removes all attributes, names included, from an atomic result, and it converts a factor to a character vector. A list passed to as.vector() is returned with its names; as.vector(x, mode = "list") turns an atomic vector into a list, one element per item, keeping the names.

# is.vector() is TRUE only when the names are the only attribute.
is.vector(1:3)
#> [1] TRUE
is.vector(c(a = 1, b = 2))
#> [1] TRUE
is.vector(structure(1:3, note = "a comment"))
#> [1] FALSE
is.vector(matrix(1:4, nrow = 2))
#> [1] FALSE
is.vector(factor("a"))
#> [1] FALSE
is.vector(list(1))
#> [1] TRUE
is.vector(expression(x + 1))
#> [1] TRUE
is.vector(quote(x))
#> [1] FALSE
is.vector(1:3, mode = "numeric")
#> [1] TRUE
is.vector(1:3, mode = "double")
#> [1] FALSE
is.vector(1:3, mode = "integer")
#> [1] TRUE
is.vector(1:3, mode = "list")
#> [1] FALSE

# is.atomic() does not care about attributes.
is.atomic(matrix(1:4, nrow = 2))
#> [1] TRUE
is.atomic(factor("a"))
#> [1] TRUE

# as.vector() strips every attribute of an atomic result, even the names.
as.vector(c(a = 1, b = 2))
#> [1] 1 2
as.vector(matrix(1:4, nrow = 2))
#> [1] 1 2 3 4
as.vector(factor(c("x", "y", "x")))
#> [1] "x" "y" "x"
as.vector(list(a = 1, b = 2))
#> $a
#> [1] 1
#>
#> $b
#> [1] 2
#>
as.vector(c(a = 1, b = 2), mode = "list")
#> $a
#> [1] 1
#>
#> $b
#> [1] 2
#>

Converting: the as.* family

Every type test has a converter: as.logical(), as.integer(), as.numeric() (which is as.double()), as.complex(), as.character() and as.raw(). The manual for beginners introduces them with the same idea: R changes the mode almost anywhere it could be considered sensible to do so, and a large collection of as.something() functions performs the coercion from one mode to another (An Introduction to R: Intrinsic attributes: mode and length). Two rules hold for all of them. First, a conversion that fails does not stop the program: it returns NA for the element that could not be converted, and for most failures it also signals a warning. Second, as.integer(), as.numeric() and the other converters strip all attributes, names included; to keep the attributes use storage.mode<- (see the previous section).

as.integer() truncates, it does not round

Converting a double to an integer drops the fractional part towards zero, so as.integer(3.9) is 3L and as.integer(-3.9) is -3L. The help page of integer says that non-integral values are truncated towards zero (as.integer(x) equals trunc(x) there) and that real values larger in modulus than the largest integer become NA, unlike the S language, which returned the most extreme integer of the same sign. A string is read first as a number and then truncated, so as.integer("3.9") is also 3L. Logical values map to 1 and 0, and NA stays NA. If you want rounding, call round(), ceiling() or floor() first, as the next script shows beside the truncated results.

The strings that as.numeric() accepts

A character string is converted if it consists of optional white space around either a decimal number or a hexadecimal number starting with 0x or 0X (the help page of integer adds "as well as any allowed by the platform for real numbers", which is why "inf", "Inf" and "NaN" work). An exponent is fine ("1e3"), a sign is fine ("+5" and "-.5"), and the empty string gives NA without a warning. Everything else gives NA with the warning NAs introduced by coercion: a decimal comma, a unit, a thousands separator, a word, and also the text "NA" itself, which is a four-character string and not a missing value. A genuine NA_character_ converts to NA silently. When a real data file is read with read.csv() or read.table() the readers use their na.strings argument for the literal "NA"; the converters do not.

# A helper that prints a value and, if there was one, the warning raised.
report <- function(expr) {
  warning_text <- NULL
  value <- withCallingHandlers(expr, warning = \(w) {
    warning_text <<- conditionMessage(w)
    invokeRestart("muffleWarning")
  })
  print(value)
  if (!is.null(warning_text)) cat("warning: ", warning_text, "\n", sep = "")
  invisible(value)
}

# as.integer() truncates towards zero; it does not round.
as.integer(c(3.9, 3.1, -3.9, -3.1))
#> [1]  3  3 -3 -3
round(c(3.9, 3.1, -3.9, -3.1))
#> [1]  4  3 -4 -3
as.integer("3.9")
#> [1] 3
as.integer(TRUE)
#> [1] 1
as.integer(c(TRUE, FALSE, NA))
#> [1]  1  0 NA

# Strings are read as numbers: blanks around the number are fine, so are
# exponents, hexadecimal, Inf and NaN; the empty string gives NA quietly.
as.numeric(c(" 12 ", "1e3", "0x1A", "-.5", "+5", "inf", "NaN", ""))
#> [1]   12.0 1000.0   26.0   -0.5    5.0    Inf    NaN     NA
# A comma, a unit, a word or the text "NA" are not numbers.
report(as.numeric("1,5"))
#> [1] NA
#> warning: NAs introduced by coercion
report(as.numeric("12 pages"))
#> [1] NA
#> warning: NAs introduced by coercion
report(as.numeric(c("7", "seven", "8")))
#> [1]  7 NA  8
#> warning: NAs introduced by coercion
report(as.integer("seven"))
#> [1] NA
#> warning: NAs introduced by coercion
report(as.numeric("NA"))
#> [1] NA
#> warning: NAs introduced by coercion
as.numeric(NA_character_)
#> [1] NA

# as.character() and as.complex().
as.character(c(1L, 25L))
#> [1] "1"  "25"
as.character(c(1.5, 100000, 0.1 + 0.2, 1 / 3))
#> [1] "1.5"               "1e+05"             "0.3"
#> [4] "0.333333333333333"
as.character(c(TRUE, NA))
#> [1] "TRUE" NA
as.complex("1+2i")
#> [1] 1+2i
report(as.numeric(1 + 2i))
#> [1] 1
#> warning: imaginary parts discarded in coercion

The warning NAs introduced by coercion

The warning has exactly one text for strings that are not numbers, NAs introduced by coercion, and a second text when the number is valid but does not fit in an integer: NAs introduced by coercion to integer range. Both are warnings, not errors, so a script that ignores them carries on with NA values and a message at the end. Both are also real signals: the first means that some text was not a number, the second means that the value was a number and was too large for 32 bits. The shelf data has both cases at hand. The isbn column is character, and most ISBNs contain hyphens, so as.numeric() turns them into NA; the bare 13-digit ones are valid doubles (about 9.78e12), but as integers they are far outside the range up to 2147483647, so as.integer() fails on them with the second text. An ISBN is an identifier, not a quantity, and the right fix is to keep it as text and to remove the hyphens with gsub() only when a check digit has to be computed (the scenario’s normalise_isbn() does that).

# The same ISBN column read as text, then forced to numbers.
report <- function(expr) {
  warning_text <- NULL
  value <- withCallingHandlers(expr, warning = \(w) {
    warning_text <<- conditionMessage(w)
    invokeRestart("muffleWarning")
  })
  print(value)
  if (!is.null(warning_text)) cat("warning: ", warning_text, "\n", sep = "")
  invisible(value)
}
books <- read.csv("books.csv")
books$isbn[c(1, 3, 9)]
#> [1] "978-0-306-40615-7" "9780001000100"     ""
report(as.numeric(books$isbn[c(1, 3, 9)]))
#> [1]           NA 9.780001e+12           NA
#> warning: NAs introduced by coercion
report(as.numeric(books$isbn[c(3, 11)]))
#> [1] 9.780001e+12 9.780009e+12
report(as.integer(books$isbn[c(3, 11)]))
#> [1] NA NA
#> warning: NAs introduced by coercion to integer range

# Hyphens are the problem, not the length: remove them first.
digits <- gsub("-", "", books$isbn, fixed = TRUE)
report(as.numeric(digits[1:3]))
#> [1] 9.780306e+12 9.780141e+12 9.780001e+12

# Numbers outside the integer range have the same message.
report(as.integer(2^31))
#> [1] NA
#> warning: NAs introduced by coercion to integer range
report(as.integer(-2^31))
#> [1] NA
#> warning: NAs introduced by coercion to integer range
report(as.integer(.Machine$integer.max))
#> [1] 2147483647
report(as.integer("2147483648"))
#> [1] NA
#> warning: NAs introduced by coercion to integer range
report(as.integer(c(1e10, 5)))
#> [1] NA  5
#> warning: NAs introduced by coercion to integer range

# A character ISBN is an identifier, not a quantity: keep it as text.
class(books$isbn)
#> [1] "character"

The same second warning appears for a double that is out of range (as.integer(2^31)), for the smallest integer written as a double (as.integer(-2^31), because that bit pattern is the one R uses for NA_integer_), and for a string that spells a too-large integer. At the top level of an interactive session R prints the warning after the statement finishes, and suppressWarnings() hides it; hiding it without looking is the most common way to ship a bug, so the usual pattern is to convert, count the NA values that were not there before, and decide what to do with them (see Defensive Programming and Input Validation).

as.logical() and the strings it knows

as.logical() accepts a short fixed list of strings: "T", "TRUE", "True" and "true" are true; "F", "FALSE", "False" and "false" are false; every other string, including "yes", "no", "1", "0", "Y", a lower-case "t" and a string with a trailing blank, is NA, and unlike as.numeric() it does not warn. In numeric and complex vectors a zero is FALSE and every non-zero value is TRUE; NaN and NA give NA; raw bytes follow the same rule. For a factor the labels are used, not the integer codes. Counting with a logical vector is the same coercion running the other way: sum() and mean() of a logical vector count the TRUE values and compute the proportion.

# as.logical() accepts a short list of spellings and gives NA for the rest,
# without a warning.
words <- c("TRUE", "true", "True", "T", "FALSE", "false", "False", "F")
as.logical(words)
#> [1]  TRUE  TRUE  TRUE  TRUE FALSE FALSE FALSE FALSE
others <- c("yes", "no", "Y", "1", "0", "t", "TRUE ", "")
as.logical(others)
#> [1] NA NA NA NA NA NA NA NA
as.logical(c(0, 1, 2, -1, 0.5, NA, NaN))
#> [1] FALSE  TRUE  TRUE  TRUE  TRUE    NA    NA
as.logical(c(0L, 5L))
#> [1] FALSE  TRUE
as.logical(0 + 0i)
#> [1] FALSE
as.logical(as.raw(c(0, 1, 2)))
#> [1] FALSE  TRUE  TRUE

# Counting with a logical vector uses the same coercion in reverse.
answers <- c("yes", "no", "yes", "maybe")
sum(answers == "yes")
#> [1] 2
mean(answers == "yes")
#> [1] 0.5
answers %in% c("yes", "no")
#> [1]  TRUE  TRUE  TRUE FALSE

Converting a factor to numbers

A factor is stored as integer codes with a levels attribute, so as.numeric() and as.integer() return the codes, not the numbers the labels spell. The FAQ has had this entry for a long time (R FAQ 7.10: How do I convert factors to numeric?): convert the labels with as.numeric(as.character(f)), or, more efficiently for many values, index the converted levels with the factor, as.numeric(levels(f))[f]. The help page of numeric carries the same warning. Since R 4.0.0 read.csv() no longer creates factors by default, so this trap now appears mostly with older data or with explicit factor() calls; see Factors.

# A factor is an integer vector with levels: as.numeric() gives the codes.
sizes <- factor(c("20", "10", "20", "5"))
typeof(sizes)
#> [1] "integer"
as.integer(sizes)
#> [1] 2 1 2 3
as.numeric(sizes)
#> [1] 2 1 2 3
levels(sizes)
#> [1] "10" "20" "5"

# Go through the labels to get the numbers the labels spell.
as.numeric(as.character(sizes))
#> [1] 20 10 20  5
as.numeric(levels(sizes))[sizes]
#> [1] 20 10 20  5
is.numeric(sizes)
#> [1] FALSE
sum(as.numeric(as.character(sizes)))
#> [1] 55

The coercion hierarchy

R combines values of different types by promoting them to the highest type involved. The help page of c() gives the order: NULL < raw < logical < integer < double < complex < character < list < expression (R documentation: c). The first types are the narrowest: a raw byte can become a logical, a logical a 0 or 1 integer, an integer an exact double, and a double a complex number with a zero imaginary part. Character is the catch-all for atomic data, because everything has a text form; the list is the only type that can hold anything, which is why c(1, list("a")) is a list. Nothing in the order goes down: to go from character to numeric you have to call as.numeric() yourself, and it may fail.

The next script builds the whole table by applying c() to every pair of types, then shows concrete results. The table is symmetric because the winner does not depend on the argument order, and the diagonal is each type with itself. Three entries are worth reading closely: raw is below logical (c(as.raw(1), TRUE) is TRUE TRUE), NA among other values takes the type of the others, and a list always wins, because the other elements are then wrapped as list elements.

# c() returns the highest type among its arguments, in the order
# raw < logical < integer < double < complex < character < list.
types <- list(
  raw = as.raw(1), logical = TRUE, integer = 1L,
  double = 1, complex = 1i, character = "a", list = list(1)
)
result <- vapply(types, \(a) {
  vapply(types, \(b) typeof(c(a, b)), character(1L))
}, character(length(types)))
print(result, quote = FALSE)
#>           raw       logical   integer   double    complex   character list
#> raw       raw       logical   integer   double    complex   character list
#> logical   logical   logical   integer   double    complex   character list
#> integer   integer   integer   integer   double    complex   character list
#> double    double    double    double    double    complex   character list
#> complex   complex   complex   complex   complex   complex   character list
#> character character character character character character character list
#> list      list      list      list      list      list      list      list

# A few concrete cases.
c(TRUE, 2L)
#> [1] 1 2
c(1L, 2.5)
#> [1] 1.0 2.5
c(1.5, 2i)
#> [1] 1.5+0i 0.0+2i
c(1, "a")
#> [1] "1" "a"
c(TRUE, "a")
#> [1] "TRUE" "a"
c(1, NA, "a")
#> [1] "1" NA  "a"
c(as.raw(1), 2L)
#> [1] 1 2
c(1, list("a"))
#> [[1]]
#> [1] 1
#>
#> [[2]]
#> [1] "a"
#>

Implicit and explicit coercion

Coercion is implicit when R does it for you and explicit when you call an as.*() function. Implicit coercion happens in four places that you meet all the time.

  • c(), unlist() and vector subassignment (x[i] <- value) promote to the highest type, as in the table above.

  • Arithmetic coerces logicals to integers, and integers to doubles when the other operand is a double. TRUE + TRUE is the integer 2L; 5L / 2L is a double because division always returns one; 5L %/% 2L stays an integer.

  • Comparison coerces to the highest type, and a string wins: 1 == "1" is TRUE, and "10" < 9 compares the strings "10" and "9" and is TRUE because "1" sorts before "9". This is the usual reason a column that was read as text sorts and compares in a surprising order.

  • A condition in if() or while() is coerced to a single logical; the string "TRUE" and any non-zero number are accepted, anything else is an error such as argument is not interpretable as logical (see Control Flow).

# Logical values take part in arithmetic as 0 and 1.
TRUE + TRUE
#> [1] 2
typeof(TRUE + TRUE)
#> [1] "integer"
sum(c(TRUE, FALSE, TRUE, TRUE))
#> [1] 3
mean(c(TRUE, FALSE, TRUE, TRUE))
#> [1] 0.75

# Integer arithmetic stays integer, except for division and powers.
typeof(5L + 2L)
#> [1] "integer"
typeof(5L * 2L)
#> [1] "integer"
typeof(5L / 2L)
#> [1] "double"
typeof(5L %/% 2L)
#> [1] "integer"
typeof(5L^2L)
#> [1] "double"
typeof(5L + 2)
#> [1] "double"

# Comparisons coerce to the common type, and a string wins.
1 == "1"
#> [1] TRUE
TRUE == "TRUE"
#> [1] TRUE
"10" < 9
#> [1] TRUE
"10" < "9"
#> [1] TRUE

# Arithmetic does not do that; it refuses text.
tryCatch("1" + 1, error = \(e) conditionMessage(e))
#> [1] "non-numeric argument to binary operator"

# if() accepts what can be read as a single logical value.
if ("TRUE") "string TRUE is accepted"
#> [1] "string TRUE is accepted"
if (2L) "a non-zero number is TRUE"
#> [1] "a non-zero number is TRUE"
tryCatch(if ("yes") 1, error = \(e) conditionMessage(e))
#> [1] "argument is not interpretable as logical"
tryCatch(if (NA) 1, error = \(e) conditionMessage(e))
#> [1] "missing value where TRUE/FALSE needed"

Arithmetic is stricter than comparison: "1" + 1 is the error non-numeric argument to binary operator, because arithmetic does not coerce text. R chooses the implicit rules to be permissive where the result is unambiguous and to fail where it would have to guess.

Implicit coercion is also the reason a subassignment can silently change the type of a whole vector. Storing a double in an integer vector turns the vector into a double, and storing a string turns it into a character vector, with every other element converted by its text form. A column that has been polluted by one bad value becomes character without any message. The next script also shows the difference between == and identical(): equality compares values after coercion, while identical() compares type and attributes too, and all.equal() reports a type difference in words.

# Equal values of different types: == compares values, identical() types too.
1L == 1
#> [1] TRUE
identical(1L, 1)
#> [1] FALSE
identical(c(a = 1), 1)
#> [1] FALSE
identical(as.integer(c(1, 2, 3)), 1:3)
#> [1] TRUE
identical(TRUE, 1L)
#> [1] FALSE
all.equal(1L, 1)
#> [1] TRUE
all.equal(1, "1")
#> [1] "Modes: numeric, character"
#> [2] "target is numeric, current is character"
identical(NA, NA_integer_)
#> [1] FALSE
is.na(NA) && is.na(NA_integer_)
#> [1] TRUE

# The type of a result matters when it is stored back into a column.
ids <- c(101L, 102L, 103L)
ids[2] <- 999
typeof(ids)
#> [1] "double"
ids[2] <- "ten"
typeof(ids)
#> [1] "character"
ids
#> [1] "101" "ten" "103"

The types of the shelf columns

The library scenario is a good test of types: read.csv() chooses the type of every column from its contents. The title, author, ISBN and genre are character, the year and pages are integers (an empty field becomes NA and does not spoil the integer type), and the loan dates are Date objects after as.Date(). str() prints the types in a compact form with the abbreviations int, num, chr, logi, cplx and raw. Comparing an integer column with a double produces a double, which matters when the result is stored back or compared with identical().

source("scenario.R")

# What type did read.csv() give each column of the shelf?
vapply(books, typeof, character(1L))
#>       title      author        isbn        year       genre       pages
#> "character" "character" "character"   "integer" "character"   "integer"
vapply(books, \(column) class(column)[1L], character(1L))
#>       title      author        isbn        year       genre       pages
#> "character" "character" "character"   "integer" "character"   "integer"
vapply(loans, \(column) class(column)[1L], character(1L))
#>     loan_id        isbn      member   loan_date return_date
#> "character" "character" "character"      "Date"      "Date"

# str() abbreviates the types: int, num, chr, logi, cplx, raw.
str(books$year)
#>  int [1:16] 1974 1813 2019 2021 2016 2008 NA 1999 2012 2023 ...
str(books$title)
#>  chr [1:16] "The Dispossessed" "Pride and Prejudice" "  the quiet streets " ...
str(1.5)
#>  num 1.5
str(c(TRUE, NA))
#>  logi [1:2] TRUE NA
str(2i)
#>  cplx 0+2i
str(as.raw(1:3))
#>  raw [1:3] 01 02 03
str(shelf_today)
#>  Date[1:1], format: "2026-06-30"

# The year column is integer, so adding a double changes its type.
typeof(books$year)
#> [1] "integer"
typeof(books$year + 1L)
#> [1] "integer"
typeof(books$year + 1)
#> [1] "double"
typeof(books$pages > 300)
#> [1] "logical"
sum(books$pages > 300, na.rm = TRUE)
#> [1] 6

Creating vectors

There are six everyday ways to make a vector: c() joins values, vector() and the typed constructors make a vector of a given length, the colon operator and seq() generate arithmetic sequences, rep() repeats values, and the readers and the apply family produce vectors from data and from functions. The first five are covered here.

c(): combining values and names

c() is short for "combine". Its arguments are coerced to a common type by the hierarchy above, nested vectors are flattened, NULL arguments disappear, and c() with no arguments returns NULL. All attributes except names are removed. Names given as name = value arguments become the names attribute, and an element without a name gets the empty string; when the value of a named argument is itself a vector, the result gets a prefix (c(x = c(a = 1, b = 2)) has names x.a and x.b, and an unnamed vector of length two gives x1 and x2). Two argument names are reserved by c() itself: recursive and use.names. A call such as c(recursive = 1, value = 2) does not create an element called recursive; it sets the argument, which is easy to hit by accident when a value is called recursive or use.names in the data. The vector of values should then be built with setNames() or names<- instead.

source("scenario.R")

# c() joins its arguments into one flat vector.
c(1, 2, 3)
#> [1] 1 2 3
c(c(1, 2), c(3, c(4, 5)))
#> [1] 1 2 3 4 5
c(books$year[1:3], 2030L)
#> [1] 1974 1813 2019 2030
c()
#> NULL
c(NULL, 1, NULL)
#> [1] 1

# Names given in c() become the names attribute; unnamed parts get "".
founders <- c(first = books$year[1], second = books$year[2], 2019L)
founders
#>  first second
#>   1974   1813   2019
names(founders)
#> [1] "first"  "second" ""

# A named argument that is itself a vector gets a prefix.
c(x = c(a = 1, b = 2))
#> x.a x.b
#>   1   2
c(x = 1:2)
#> x1 x2
#>  1  2
c(x = c(a = 1, b = 2), y = 3)
#> x.a x.b   y
#>   1   2   3

# use.names = FALSE drops all of them.
c(first = 1, second = 2, use.names = FALSE)
#> [1] 1 2

# Two arguments of c() are not element names: recursive and use.names.
c(recursive = 1, value = 2)
#> value
#>     2
c(value = 1, 2)
#> value
#>     1     2

# Duplicated names are allowed; lookup by name takes the first.
dup <- c(a = 1, a = 2)
dup["a"]
#> a
#> 1
dup[["a"]]
#> [1] 1

Names are not required to be unique, and nothing checks them. Lookup by a name returns the first match. Names are an attribute, so they are lost by as.vector(), kept by arithmetic with the rules in the section on vectorised arithmetic, and kept or padded by length<-. The full treatment of names, setNames() and unname() is in Attributes and Names.

vector() and the typed constructors

vector(mode, length) creates a vector of any atomic mode, "list" or "expression", filled with the "empty" value of the type: FALSE, 0L, 0, 0+0i, "", a zero byte, or NULL for a list. logical(n), integer(n), numeric(n), double(n), complex(n), character(n) and raw(n) are the same call with the mode in the name; numeric() and double() are identical. Their main use is to preallocate the result of a loop with the right type and size (see the section on zero-length vectors and preallocation below). The length argument has to be a single number; a vector of other length is an error, and for more than .Machine$integer.max elements it has to be a double. The help page also says that vector creates a "simple" vector with no attributes. An unknown mode is an error: vector: cannot make a vector of mode 'foo'.

# vector(mode, length) makes a vector of "empty" values of any mode.
vector("logical", 3)
#> [1] FALSE FALSE FALSE
vector("integer", 3)
#> [1] 0 0 0
vector("numeric", 3)
#> [1] 0 0 0
vector("double", 3)
#> [1] 0 0 0
vector("complex", 2)
#> [1] 0+0i 0+0i
vector("character", 2)
#> [1] "" ""
vector("raw", 3)
#> [1] 00 00 00
vector("list", 3)
#> [[1]]
#> NULL
#>
#> [[2]]
#> NULL
#>
#> [[3]]
#> NULL
#>

# The typed shortcuts are the same call with the mode in the name.
identical(numeric(3), vector("numeric", 3))
#> [1] TRUE
identical(character(0), vector("character", 0))
#> [1] TRUE
identical(double(2), numeric(2))
#> [1] TRUE
integer(0)
#> integer(0)
character(0)
#> character(0)
logical(0)
#> logical(0)

# The "empty" value of each type.
typeof(numeric(1))
#> [1] "double"
numeric(1) == 0
#> [1] TRUE
character(1) == ""
#> [1] TRUE
logical(1)
#> [1] FALSE
as.integer(raw(1))
#> [1] 0
vapply(vector("list", 2), is.null, logical(1))
#> [1] TRUE TRUE

The colon operator

from:to creates a sequence of integers when from is a whole number, in steps of one, and counts down when to is smaller than from. If from is not whole (1.5:4), the result is a double sequence 1.5, 2.5, 3.5. The colon binds tighter than the arithmetic operators and looser than ^ and unary minus, so 1:n - 1 is (1:n) - 1, -1:3 is (-1):3, and 2:3^2 is 2:(3^2). The precedence table is in Operators and Precedence.

The colon has one famous trap: 1:0 is not empty, it is the sequence 1, 0. Code that writes for (i in 1:length(x)) runs the loop body twice for an empty x, with i equal to 1 and then 0, and indexing with those values gives surprising results. The fix is seq_along(x), which is empty for an empty vector, or seq_len(n) when the count is a number. lintr flags the first form (the seq_linter), which is why the script marks the deliberate uses with # nolint: seq_linter.. The colon also needs a non-missing number: 1:NA is the error NA/NaN argument, and a vector of length greater than one on either side gives a warning that only the first element is used (numerical expression has 2 elements: only the first used).

# The colon operator builds an integer sequence when it can.
1:5
#> [1] 1 2 3 4 5
5:1
#> [1] 5 4 3 2 1
0:3
#> [1] 0 1 2 3
-1:3
#> [1] -1  0  1  2  3
-(1:3)
#> [1] -1 -2 -3
typeof(1:5)
#> [1] "integer"
typeof(1.5:4)
#> [1] "double"
1.5:4
#> [1] 1.5 2.5 3.5
typeof(0:3 + 0.5)
#> [1] "double"

# The colon binds tighter than + - * / but looser than ^ and unary minus.
n <- 4
1:n - 1
#> [1] 0 1 2 3
1:(n - 1)
#> [1] 1 2 3
2:3^2
#> [1] 2 3 4 5 6 7 8 9
(2:3)^2
#> [1] 4 9

# The empty-sequence trap: 1:0 is c(1, 0), not an empty vector.
x <- character(0)
1:length(x) # nolint: seq_linter.
#> [1] 1 0
seq_len(length(x)) # nolint: seq_linter.
#> integer(0)
seq_along(x)
#> integer(0)
for (i in 1:length(x)) { # nolint: seq_linter.
  cat("iteration", i, "\n")
}
#> iteration 1
#> iteration 0
for (i in seq_along(x)) {
  cat("never printed", i, "\n")
}
tryCatch(1:NA, error = \(e) conditionMessage(e))
#> [1] "NA/NaN argument"
tryCatch(1:c(3, 4), warning = \(w) conditionMessage(w))
#> [1] "numerical expression has 2 elements: only the first used"

seq(), seq_len() and seq_along()

seq() is the general sequence generator and has several forms, selected by which arguments are given. seq(from, to, by) steps by a fixed amount (a negative by with from greater than to counts down, and the wrong sign is the error wrong sign in 'by' argument), seq(from, to, length.out = n) spreads n points evenly, and seq(from, by = b, length.out = n) or seq(to = t, by = b, length.out = n) build the sequence from one end. With one argument, seq(n) is 1:n and seq(v) is seq_along(v), which is the reason seq(x) is not safe when x might be a single number: seq(c(10, 20, 30)) is 1:3 but seq(5) is 1:5. The help page recommends seq_len() and seq_along() for loops: seq_len(n) is 1:n for positive n and empty for zero, an error for a negative n, and seq_along(x) is seq_len(length(x)); both return integers (or doubles for a long vector) and are primitive and fast. seq.int() is the faster internal version for the same arguments.

The type of the result follows the arguments: seq(1, 10) is an integer sequence, like 1:10, seq(1, 10, by = 3) is a double sequence because by is a double (even though all the values are whole), seq(1L, 10L, by = 3L) is an integer sequence, and anything with a fractional step is a double. A double sequence with a decimal step accumulates floating-point error, so compare its elements with a tolerance (see Numbers and Arithmetic).

source("scenario.R")

# seq() with from, to and by.
seq(1, 10, by = 3)
#> [1]  1  4  7 10
seq(0, 1, by = 0.25)
#> [1] 0.00 0.25 0.50 0.75 1.00
seq(10, 1)
#>  [1] 10  9  8  7  6  5  4  3  2  1
seq(10, 1, by = -3)
#> [1] 10  7  4  1
tryCatch(seq(1, 10, by = -1), error = \(e) conditionMessage(e))
#> [1] "wrong sign in 'by' argument"

# seq() with length.out spreads points evenly.
seq(1, 10, length.out = 4)
#> [1]  1  4  7 10
seq(0, 1, length.out = 5)
#> [1] 0.00 0.25 0.50 0.75 1.00
seq(5, by = 2, length.out = 4)
#> [1]  5  7  9 11
seq(to = 100, by = 25, length.out = 3)
#> [1]  50  75 100

# One argument: seq(n) is 1:n, seq(v) is seq_along(v).
seq(5)
#> [1] 1 2 3 4 5
seq(c(10, 20, 30))
#> [1] 1 2 3

# The result type: integer when everything is whole, double otherwise.
typeof(seq(1, 10))
#> [1] "integer"
typeof(seq(1, 10, by = 3))
#> [1] "double"
typeof(seq(1L, 10L, by = 3L))
#> [1] "integer"
typeof(seq(1, 10, length.out = 4))
#> [1] "double"
typeof(seq_len(3))
#> [1] "integer"
typeof(seq_along(letters))
#> [1] "integer"

# seq_len() and seq_along() are the safe loop helpers.
seq_len(0)
#> integer(0)
seq_len(3)
#> [1] 1 2 3
seq_along(books$title)[1:5]
#> [1] 1 2 3 4 5
seq_len(nrow(books))
#>  [1]  1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16
seq(1950, 2020, by = 10)
#> [1] 1950 1960 1970 1980 1990 2000 2010 2020
tryCatch(seq_len(-1), error = \(e) conditionMessage(e))
#> [1] "argument must be coercible to non-negative integer"
rev(seq_len(4))
#> [1] 4 3 2 1

rep() and rep_len()

rep(x, times, each, length.out) repeats a vector. times repeats the whole vector (a single number) or each element a different number of times (a vector of the length of x), each repeats each element in turn, and length.out cuts or recycles the result to an exact length; when each is combined with another argument the each replication happens first, and when length.out and times are both given length.out wins. rep_len(x, n) and rep.int(x, times) are faster, less flexible versions. rep() keeps the names of its input, while rep_len() and rep.int() drop them. A negative or NA count is an error (invalid 'times' argument), and so is a vector of counts of the wrong length. The most common use is to preallocate a typed vector filled with NA (rep(NA_character_, n)), because NA alone is logical and would set the type of a column to logical.

source("scenario.R")

# times repeats the whole vector, each repeats every element.
rep(c("a", "b"), times = 3)
#> [1] "a" "b" "a" "b" "a" "b"
rep(c("a", "b"), each = 3)
#> [1] "a" "a" "a" "b" "b" "b"
rep(c("a", "b"), times = 2, each = 2)
#> [1] "a" "a" "b" "b" "a" "a" "b" "b"

# A vector of times gives one count per element.
rep(1:3, times = c(3, 1, 2))
#> [1] 1 1 1 2 3 3

# length.out stops (or recycles) at an exact length.
rep(c(1, 2, 3), length.out = 7)
#> [1] 1 2 3 1 2 3 1
rep(c("x", "y"), length.out = 5)
#> [1] "x" "y" "x" "y" "x"
rep_len(1:3, 8)
#> [1] 1 2 3 1 2 3 1 2
rep.int(c(0, 1), 3)
#> [1] 0 1 0 1 0 1

# Names survive rep().
rep(c(a = 1, b = 2), 2)
#> a b a b
#> 1 2 1 2

# Preallocating a typed vector of NA values for a loop or a column.
rep(NA_character_, 3)
#> [1] NA NA NA
rep(NA_integer_, 4)
#> [1] NA NA NA NA
pages_filled <- rep(NA_integer_, nrow(books))
length(pages_filled)
#> [1] 16

# A divider line, and the first title three times.
paste(rep("-", 20), collapse = "")
#> [1] "--------------------"
rep(books$title[1], 3)
#> [1] "The Dispossessed" "The Dispossessed" "The Dispossessed"

# Invalid counts are errors.
tryCatch(rep(1:2, times = -1), error = \(e) conditionMessage(e))
#> [1] "invalid 'times' argument"
tryCatch(rep(1:3, times = c(1, 2)), error = \(e) conditionMessage(e))
#> [1] "invalid 'times' argument"

Recycling and vectorised arithmetic

The arithmetic, comparison and logical operators and most mathematical functions work on whole vectors, element by element, without a loop; this is what "vectorised" means in R. When the operands have different lengths the shorter one is recycled: the language definition says that if the longer length is not a multiple of the shorter one a warning is given, and that any arithmetic operation involving a zero-length vector has a zero-length result (The R Language Definition: Recycling rules). The warning text is longer object length is not a multiple of shorter object length, and it belongs to the arithmetic, comparison and logical operators alike.

Recycling is what makes x * 2 and x - mean(x) work, so it is a feature; the problem is that the exact-multiple case is silent. Comparing a length-16 column with a vector of length two repeats the vector eight times without any message, which is almost never what was meant, and the script shows the intended %in% next to it. Other functions have their own messages for the same situation: assigning a replacement that does not fit gives number of items to replace is not a multiple of replacement length, matrix() complains that the data length is not a multiple of the number of rows, and data.frame() refuses columns of different lengths with an error (arguments imply differing number of rows: 3, 2) because a data frame does not recycle by default, except where the lengths are an exact multiple.

source("scenario.R")

# The shorter operand is repeated; no warning when it divides evenly.
1:6 + 1:2
#> [1] 2 4 4 6 6 8
1:6 + 10
#> [1] 11 12 13 14 15 16
1:6 * c(1, 10, 100)
#> [1]   1  20 300   4  50 600

# When it does not divide evenly the result is still computed, with a warning.
tryCatch(1:5 + 1:2, warning = \(w) conditionMessage(w))
#> [1] "longer object length is not a multiple of shorter object length"
suppressWarnings(1:5 + 1:2)
#> [1] 2 4 4 6 6
tryCatch(c(TRUE, FALSE) & c(TRUE, TRUE, FALSE), warning = \(w) {
  conditionMessage(w)
})
#> [1] "longer object length is not a multiple of shorter object length"

# A zero-length operand gives a zero-length result, with no warning.
1:3 + integer(0)
#> integer(0)
numeric(0) * 5
#> numeric(0)
sum(numeric(0))
#> [1] 0

# Comparisons recycle too, which makes a silent bug possible.
books$genre[1:6]
#> [1] "Science fiction" "Classic"         "Fiction"         "fiction"
#> [5] "Mystery"         "Non-fiction"
books$genre[1:6] == c("Fiction", "Poetry")
#> [1] FALSE FALSE  TRUE FALSE FALSE FALSE
books$genre[1:6] %in% c("Fiction", "Poetry")
#> [1] FALSE FALSE  TRUE FALSE FALSE FALSE

# Shelf arithmetic: one scalar against a whole column.
head(books$pages / 100, 5)
#> [1] 3.87 4.32 2.14 1.98 3.01
head(books$year - 1900L, 5)
#> [1]  74 -87 119 121 116

# Sub-assignment and matrix() recycle with their own messages.
x <- 1:6
tryCatch(x[1:4] <- 1:3, warning = \(w) conditionMessage(w))
#> [1] "number of items to replace is not a multiple of replacement length"
tryCatch(matrix(1:5, nrow = 2, ncol = 3), warning = \(w) conditionMessage(w))
#> [1] "data length [5] is not a sub-multiple or multiple of the number of rows [2]"
tryCatch(data.frame(id = 1:3, n = 1:2), error = \(e) conditionMessage(e))
#> [1] "arguments imply differing number of rows: 3, 2"

An upcoming change is worth knowing about. The R-devel NEWS (the future 4.7.0) says that subsetting with a logical index of a length that is not a multiple of the length of the vector now gives a warning; this is prose only here because the final text may change before the release (R NEWS: Changes in R-devel). The rules of logical indexing are in Subsetting and Indexing.

Vectorised arithmetic and the attributes of the result

Vectorisation is not limited to the operators. sqrt(), round(), %/%, %%, cumsum() and the comparison operators all return a vector of the same length as their input; the summary functions sum(), max(), range() and mean() reduce a vector to one number (or two for range()), and pmax() and pmin() are the element-by-element versions of max() and min(). A vector that contains NA makes the result NA unless na.rm = TRUE is given (see Missing and Special Values); the pages column of the shelf has two NA, so the examples below pass na.rm = TRUE to the summaries.

Binary operations copy most attributes from the longer operand, and from both operands with the first one preferred when the lengths are equal; the exceptions are names, dim and dimnames, which are set by the operator’s own code (The R Language Definition: Elementary arithmetic operations). For names the practical rule, checked in the script, is that they come from an operand whose length equals the length of the result, the first one preferred: c(a = 1, b = 2) + c(x = 10, y = 20) is named a and b, c(1, 2) + c(x = 10, y = 20) takes the names of the second operand, and a named vector of length two added to an unnamed vector of length four gives an unnamed result.

source("scenario.R")

# Arithmetic, comparison and maths functions work on whole vectors.
pages <- books$pages
head(pages * 2L)
#> [1] 774 864 428 396 602  NA
head(sqrt(pages))
#> [1] 19.67232 20.78461 14.62874 14.07125 17.34935       NA
head(pages %/% 100L)
#> [1]  3  4  2  1  3 NA
head(pages %% 100L)
#> [1] 87 32 14 98  1 NA
head(pages > 300)
#> [1]  TRUE  TRUE FALSE FALSE  TRUE    NA
cumsum(1:5)
#> [1]  1  3  6 10 15

# One value out of many: summaries return a single number.
sum(pages, na.rm = TRUE)
#> [1] 3755
range(pages, na.rm = TRUE)
#> [1]  88 432
max(pages, na.rm = TRUE)
#> [1] 432

# max() versus pmax(): pmax() works element by element.
max(c(1, 5, 3), c(4, 2, 6))
#> [1] 6
pmax(c(1, 5, 3), c(4, 2, 6))
#> [1] 4 5 6
pmin(c(1, 5, 3), 4)
#> [1] 1 4 3

# Names follow the first operand (or the longer one); other attributes too.
c(a = 1, b = 2) + c(x = 10, y = 20)
#>  a  b
#> 11 22
c(a = 1, b = 2) + 10
#>  a  b
#> 11 12
c(1, 2) + c(x = 10, y = 20)
#>  x  y
#> 11 22
c(a = 1, b = 2, c = 3, d = 4) + c(x = 10, y = 20)
#>  a  b  c  d
#> 11 22 13 24

# Logic: & and | work element by element; all() and any() reduce.
c(TRUE, FALSE, NA) & c(TRUE, TRUE, TRUE)
#> [1]  TRUE FALSE    NA
c(TRUE, FALSE, NA) | c(FALSE, FALSE, FALSE)
#> [1]  TRUE FALSE    NA
all(pages > 50, na.rm = TRUE)
#> [1] TRUE
any(pages > 400, na.rm = TRUE)
#> [1] TRUE

Changing the length of a vector

length(x) returns the number of elements, and the replacement form length(x) <- n changes it: shortening discards the extra elements, and lengthening pads at the end. The padding is NA for logical, integer, double, complex and character vectors, a zero byte for raw vectors, and NULL for lists and expression vectors, the same values that vector(mode, n) creates, as the help page says. The replacement removes all attributes except names, which are adjusted and extended by empty strings (""); a matrix loses its dim and becomes a plain vector. The introduction manual shows the same pair of operations, adding elements by assigning to an index beyond the end (e[3] <- 17 on an empty vector) and truncating with length(alpha) <- 3 (An Introduction to R: Changing the length of an object).

length<- rejects a negative or NA value with the error invalid value. It cannot lengthen NULL, which stays NULL with the warning length of NULL cannot be changed, so a variable that starts as NULL has to be created as vector("list", 0) or character(0) first. Reading past the end with [[ is an error (subscript out of bounds), while assigning past the end with [<- or [[<- quietly extends the vector with NA between the old end and the new element. head() and tail() from the utils package are the non-destructive way to take the first or last elements, and append() inserts values at a position without changing the original.

# length(x) <- n pads with NA at the end, or truncates.
x <- c(10L, 20L, 30L)
length(x) <- 5
x
#> [1] 10 20 30 NA NA
length(x) <- 2
x
#> [1] 10 20
length(x) <- 0
x
#> integer(0)

# The padding has the type of the vector.
words <- c("a", "b")
length(words) <- 4
words
#> [1] "a" "b" NA  NA
flags <- c(TRUE, FALSE)
length(flags) <- 3
flags
#> [1]  TRUE FALSE    NA
raws <- as.raw(1:2)
length(raws) <- 4
raws
#> [1] 01 02 00 00

# Names are padded with empty strings.
named <- c(a = 1, b = 2)
length(named) <- 4
named
#>  a  b
#>  1  2 NA NA
names(named)
#> [1] "a" "b" ""  ""

# Other attributes are dropped: a matrix becomes a plain vector.
m <- matrix(1:6, nrow = 2)
length(m) <- 4
m
#> [1] 1 2 3 4

# A list is padded with NULL.
items <- list(1, "a")
length(items) <- 3
str(items)
#> List of 3
#>  $ : num 1
#>  $ : chr "a"
#>  $ : NULL

# Assigning past the end extends the vector the same way.
y <- 1:3
y[6] <- 60L
y
#> [1]  1  2  3 NA NA 60
tryCatch(y[[9]], error = \(e) conditionMessage(e))
#> [1] "subscript out of bounds"
tryCatch(length(y) <- -1, error = \(e) conditionMessage(e))
#> [1] "invalid value"
tryCatch(length(y) <- NA, error = \(e) conditionMessage(e))
#> [1] "invalid value"
tryCatch({
  z <- NULL
  length(z) <- 2
}, warning = \(w) conditionMessage(w))
#> [1] "length of NULL cannot be changed"

Zero-length vectors and preallocation

A zero-length vector is a normal value, and it is what most functions return when there is nothing to return: integer(0) from grep() or which() with no match, character(0) from grep(value = TRUE), numeric(0) from a subset that selects nothing. Its type is kept. Arithmetic with it gives a zero-length result (the language definition’s rule, shown in the recycling script), the summary functions have defined values for it (sum(numeric(0)) is 0, prod(numeric(0)) is 1, while max(numeric(0)) is -Inf with the warning no non-missing arguments to max; returning -Inf), and a loop over seq_along() of it does not run at all. The things that go wrong are 1:length(x) (see the colon operator) and conditions such as if (x == 1), where a zero-length x makes the condition logical(0) and the error is argument is of length zero, a topic of Missing and Special Values and Control Flow.

The same constructors that make empty vectors make the containers that loops fill. Growing a vector one element at a time with x[length(x) + 1] <- value or c(x, value) works, but each step may allocate a new vector, and the type of the vector can change halfway; creating integer(n), character(n) or vector("list", n) once and assigning into it by index states the type and the length up front. When each result is a single value, vapply() does both in one call: its third argument, FUN.VALUE, is a template of the type and length each call must return, and a mismatch is an error with a clear message instead of a silent change of type. sapply() guesses, so it returns a list for empty input and an inconsistent shape whenever the results differ in length, which is why the recommended form in code that other people run is vapply() (the apply family is covered in The apply Family and Functionals).

source("scenario.R")

# Preallocate a result of the right type and length, then fill it.
title_chars <- integer(nrow(books))
for (i in seq_along(books$title)) {
  title_chars[i] <- nchar(trimws(books$title[i]))
}
title_chars
#>  [1] 16 19 17 10 15 13 21 16 15 14 11 16 13 14 11 15

# vapply() states the type and length of every result and checks them.
vapply(books$title, \(t) nchar(trimws(t)), integer(1L), USE.NAMES = FALSE)
#>  [1] 16 19 17 10 15 13 21 16 15 14 11 16 13 14 11 15
vapply(books$pages, \(p) p > 300L, logical(1L))[1:6]
#> [1]  TRUE  TRUE FALSE FALSE  TRUE    NA

# A wrong promise is an error, not a silent change of type.
tryCatch(
  vapply(1:3, \(i) "a", numeric(1L)),
  error = \(e) conditionMessage(e)
)
#> [1] "values must be type 'double',\n but FUN(X[[1]]) result is type 'character'"
tryCatch(
  vapply(1:3, \(i) c(i, i), numeric(1L)),
  error = \(e) conditionMessage(e)
)
#> [1] "values must be length 1,\n but FUN(X[[1]]) result is length 2"

# On empty input vapply() still returns the promised type; sapply() cannot.
vapply(character(0), nchar, integer(1L))
#> named integer(0)
sapply(character(0), nchar)
#> named list()
vapply(character(0), nchar, integer(1L), USE.NAMES = FALSE)
#> integer(0)

Raw, character and complex vectors

Three of the types need a few more words.

Raw vectors

A raw vector holds bytes, each printed as two hexadecimal digits, and is the type that binary data, hashes and byte-level text work with. as.raw() converts numbers 0 to 255; a value outside the range becomes a zero byte with the warning out-of-range values treated as 0 in coercion to raw. There is no missing byte, so no NA exists for raw, and a raw vector lengthened with length<- is padded with zero bytes. charToRaw() shows the bytes of a string in its encoding: "A" is the single byte 41, and "é" is the two bytes c3 a9 in UTF-8, so nchar("é") is 1 and nchar("é", type = "bytes") is 2 (the Encodings, Locales and Internationalization page continues with this). rawToChar() goes back, utf8ToInt() and intToUtf8() convert between characters and Unicode code points, and rawToBits() and packBits() convert between bytes and their bits. The logical operators &, |, ! and xor() are bitwise on raw vectors, rawShift() shifts bits, and arithmetic such as + is not defined and gives non-numeric argument to binary operator.

source("scenario.R")

# Raw vectors hold bytes, shown as two hexadecimal digits.
as.raw(c(1, 15, 16, 255))
#> [1] 01 0f 10 ff
raw(3)
#> [1] 00 00 00
as.integer(as.raw(c(1, 15, 16, 255)))
#> [1]   1  15  16 255
tryCatch(as.raw(256), warning = \(w) conditionMessage(w))
#> [1] "out-of-range values treated as 0 in coercion to raw"
tryCatch(as.raw(-1), warning = \(w) conditionMessage(w))
#> [1] "out-of-range values treated as 0 in coercion to raw"

# Text is bytes in some encoding.
charToRaw("A")
#> [1] 41
charToRaw("\u00e9")
#> [1] c3 a9
nchar("\u00e9")
#> [1] 1
nchar("\u00e9", type = "bytes")
#> [1] 2
bytes <- charToRaw(books$title[1])
length(bytes)
#> [1] 16
rawToChar(rev(bytes))
#> [1] "dessessopsiD ehT"
utf8ToInt("A")
#> [1] 65
intToUtf8(c(82, 33))
#> [1] "R!"

# Logic on raw vectors is bitwise; arithmetic is not defined.
as.raw(12) & as.raw(10)
#> [1] 08
as.raw(12) | as.raw(10)
#> [1] 0e
xor(as.raw(12), as.raw(10))
#> [1] 06
!as.raw(1)
#> [1] fe
rawShift(as.raw(1), 3)
#> [1] 08
tryCatch(as.raw(1) + as.raw(1), error = \(e) conditionMessage(e))
#> [1] "non-numeric argument to binary operator"
rawToBits(as.raw(5))
#> [1] 01 00 01 00 00 00 00 00
packBits(rawToBits(as.raw(5)))
#> [1] 05

Character vectors

A character vector is a vector of strings. A string is one element, however many characters it has, so length() counts the strings and nchar() counts the characters; the number of characters of a missing string is NA_integer_, not zero. Strings can be written with double or single quotes, with backslash escapes, and, since R 4.0.0, as raw strings r"(...)" in which backslashes mean nothing (R documentation: Quotes). A comparison that involves NA_character_ gives NA, not FALSE; NA_character_ == "one" is a comparison with a missing value and is.na() is the test. Everything about searching, splitting and replacing is in Strings and Regular Expressions.

Complex vectors

Complex numbers are the sixth type and the least used. complex(real = 3, imaginary = 4) builds 3+4i, and Re(), Im(), Mod(), Arg() and Conj() take it apart. sqrt(-1) is NaN with the warning NaNs produced, because the real square root of a negative number is not defined; sqrt(as.complex(-1)) is 0+1i. Converting a complex vector to a real type discards the imaginary part with the warning imaginary parts discarded in coercion. Complex arithmetic is part of Numbers and Arithmetic.

# A string is an element, so a character vector is a vector of strings.
x <- c("one", "two", NA)
length(x)
#> [1] 3
nchar(x)
#> [1]  3  3 NA
is.na(x)
#> [1] FALSE FALSE  TRUE
x == "one"
#> [1]  TRUE FALSE    NA
NA_character_ == "one" # nolint: equals_na_linter.
#> [1] NA

# Quotes: either kind, escapes with a backslash, raw strings since R 4.0.0.
'single' # nolint: quotes_linter.
#> [1] "single"
"say \"hi\""
#> [1] "say \"hi\""
r"(C:\temp\file)"
#> [1] "C:\\temp\\file"
nchar(r"(C:\temp\file)")
#> [1] 12

# Complex numbers have a real and an imaginary part.
z <- complex(real = 3, imaginary = 4)
z
#> [1] 3+4i
Re(z)
#> [1] 3
Im(z)
#> [1] 4
Mod(z)
#> [1] 5
Conj(z)
#> [1] 3-4i
typeof(z)
#> [1] "complex"
tryCatch(sqrt(-1), warning = \(w) conditionMessage(w))
#> [1] "NaNs produced"
sqrt(as.complex(-1))
#> [1] 0+1i

Lists, attributes and the rest of the family

A list is the other kind of vector. It has a length, it is created by list() or vector("list", n), and typeof() reports "list"; is.vector() is true for it, is.atomic() is false, is.recursive() is true. c() combines lists and atomic vectors into a list when any argument is a list, and unlist() flattens a list by the same coercion ranking, so a list of an integer, a double and a logical becomes a double vector, and one that contains a string becomes a character vector. lengths() returns the length of each element at once, which is the fast replacement for sapply(x, length). The container itself and its operations are in Lists.

source("scenario.R")

# A list is a vector too, but not an atomic one: its elements can be anything.
shelf_row <- list(title = books$title[1], year = books$year[1], pages = 387L)
typeof(shelf_row)
#> [1] "list"
is.vector(shelf_row)
#> [1] TRUE
is.atomic(shelf_row)
#> [1] FALSE
is.recursive(shelf_row)
#> [1] TRUE
length(shelf_row)
#> [1] 3
vapply(shelf_row, typeof, character(1L))
#>       title        year       pages
#> "character"   "integer"   "integer"

# c() with a list returns a list, and unlist() flattens by the same ranking.
typeof(c(1, list("a")))
#> [1] "list"
unlist(list(1L, 2.5, TRUE))
#> [1] 1.0 2.5 1.0
unlist(list(1L, "a", TRUE))
#> [1] "1"    "a"    "TRUE"
typeof(unlist(list(1L, 2.5, TRUE)))
#> [1] "double"
unlist(list(a = 1, b = list(c = 2, d = 3)))
#>   a b.c b.d
#>   1   2   3

# lists nest; lengths() counts the elements of each.
nested <- list(1:3, letters, NULL, list(1, 2))
lengths(nested)
#> [1]  3 26  0  2
length(nested)
#> [1] 4

# vector("list", n) is the empty container to fill.
slots <- vector("list", 3)
slots[[2]] <- "filled"
vapply(slots, is.null, logical(1L))
#> [1]  TRUE FALSE  TRUE

# A data frame is a list of equal-length columns.
typeof(books)
#> [1] "list"
class(books)
#> [1] "data.frame"
length(books)
#> [1] 6
is.list(books)
#> [1] TRUE
lengths(books)[1:3]
#>  title author   isbn
#>     16     16     16

The remaining structures are made with attributes and do not change typeof(). dim(v) <- c(2, 3) turns an integer vector into a matrix, a class of "factor" together with levels turns integer codes into a factor, a class of "Date" turns a double into a date, and a list with equal-length elements, names, row.names and the class "data.frame" is a data frame. Removing the attributes (attributes(v) <- NULL, or unclass() for the class) gives back the base vector, and structure() sets several attributes in one expression. What happens to each attribute when a vector is subsetted, combined or computed with is the subject of Attributes and Names; the short rules relevant here are that coercion drops all attributes, subsetting drops all but names, dim and dimnames, and sub-assignment preserves them (The R Language Definition: Copying of attributes).

# Attributes turn a plain vector into richer objects without changing the type.
v <- 1:6
dim(v) <- c(2, 3)
v
#>      [,1] [,2] [,3]
#> [1,]    1    3    5
#> [2,]    2    4    6
class(v)
#> [1] "matrix" "array"
typeof(v)
#> [1] "integer"
attributes(v)
#> $dim
#> [1] 2 3
#>

f <- factor(c("low", "high", "low"), levels = c("low", "high"))
typeof(f)
#> [1] "integer"
class(f)
#> [1] "factor"
attributes(f)
#> $levels
#> [1] "low"  "high"
#>
#> $class
#> [1] "factor"
#>
unclass(f)
#> [1] 1 2 1
#> attr(,"levels")
#> [1] "low"  "high"

d <- as.Date("2026-06-30")
typeof(d)
#> [1] "double"
class(d)
#> [1] "Date"
unclass(d)
#> [1] 20634

df <- data.frame(id = 1:2, ok = c(TRUE, FALSE))
typeof(df)
#> [1] "list"
class(df)
#> [1] "data.frame"
names(attributes(df))
#> [1] "names"     "class"     "row.names"

# Removing the attributes gives back the underlying vector.
attributes(v) <- NULL
v
#> [1] 1 2 3 4 5 6
is.null(attributes(v))
#> [1] TRUE
# structure() sets several attributes in one call.
structure(c(2.5, 3), class = "tolerance", unit = "mm")
#> [1] 2.5 3.0
#> attr(,"class")
#> [1] "tolerance"
#> attr(,"unit")
#> [1] "mm"

Long vectors

Vectors in R 2.x were limited to 2^31 - 1 (about two billion) elements, because the length was stored in a C int. R 3.0.0 added support for longer vectors on 64-bit platforms, a feature described in the "Long vectors" section of the R internals manual and in the 3.0.0 NEWS (R Internals: Long vectors and R NEWS: Changes in R 3.0.0). The support covers raw, logical, integer, double, complex and character vectors and lists; the elements of character vectors (single strings) are still limited to 2^31 - 1 bytes. The length is recorded as a 64-bit value. In theory lengths could have 63 bits, but there is currently a 52-bit limit, which exceeds what current operating systems offer and ensures that a length can be stored exactly in a double.

Four practical consequences follow.

  • length() returns an integer for ordinary vectors and a double when the length exceeds .Machine$integer.max, which is 2147483647. Code that calls as.integer(length(x)) before passing it to C or Fortran has to check for the NA result.

  • A single long vector of 2^31 or more elements takes at least 8 GB when it is integer or logical and 16 GB when it is double or character, so you will meet them on large machines only. The length argument of vector() has to be a double for such sizes, and a request that is too large is an error: vector size specified is too large beyond the theoretical limit, or cannot allocate vector of size ... when the memory is simply not there.

  • Indexing is done with doubles: a double index above 2^31 - 1 reaches elements that no integer could address, and seq_len() and seq_along() return doubles when the vector is long. Most functions work with long vectors. The 3.0.0 NEWS said that some others report long vectors not supported yet, mostly because they work with integer indices, and named anyDuplicated() and match() as examples; later releases lifted several of these limits (R 3.6.0, for example, made match(x, table) work for a long character x), so check the NEWS of your version rather than older advice.

  • Arithmetic sequences created by 1:n, seq_along() and the like have used a compact representation since R 3.5.0 (the ALTREP framework), which defers the expansion: 1:3e9 is a long vector that occupies almost no memory until a function touches every element. That makes it a safe way to demonstrate the type and length rules without allocating 24 GB. In R 4.6.1 object.size() reports the size the expanded vector would need, not the memory in use, so do not read it as a measurement for such a sequence.

# The largest integer, and so the largest value length() returns as integer.
.Machine$integer.max
#> [1] 2147483647
2^31 - 1
#> [1] 2147483647
typeof(length(1:10))
#> [1] "integer"

# A compact sequence is a long vector that costs almost no memory.
x <- 1:3e9
length(x)
#> [1] 3e+09
typeof(length(x))
#> [1] "double"
length(x) > .Machine$integer.max
#> [1] TRUE
typeof(x)
#> [1] "double"
x[3e9]
#> [1] 3e+09
identical(length(x), 3e9)
#> [1] TRUE

# Code that needs an integer length has to check for the overflow.
tryCatch(as.integer(length(x)), warning = \(w) conditionMessage(w))
#> [1] "NAs introduced by coercion to integer range"

# lengths() on a list gives the integer length of every element.
lengths(list(1:3, letters, character(0)))
#> [1]  3 26  0

# Asking for more than the machine can ever hold is an error.
tryCatch(numeric(2^53), error = \(e) conditionMessage(e))
#> [1] "vector size specified is too large"

The bit64 package (not installed for this page) adds a 64-bit integer type for values beyond 2^31 - 1; plain R has none, and a double represents every integer exactly up to 2^53. The numeric side of this limit, integer overflow to NA with the warning NAs produced by integer overflow, is in Numbers and Arithmetic, and memory and copying for large objects are in Memory and Copy Semantics.

Common errors and warnings

Most of the conditions in this part of the language come from a handful of rules: coercion that fails, recycling that does not fit, a length or count that is invalid, and an operator applied to the wrong type. The script runs each snippet, catches the first condition it signals, and prints it, so the texts below are the real ones from R 4.6.1.

# Each snippet is evaluated and the first condition it raises is reported.
condition_text <- function(expr) {
  tryCatch(
    {
      expr
      "no condition"
    },
    warning = \(w) paste("warning:", conditionMessage(w)),
    error = \(e) paste("error:", conditionMessage(e))
  )
}
cases <- list(
  quote(as.numeric("seven")),
  quote(as.integer(3e9)),
  quote(as.raw(300)),
  quote(1:3 + 1:2),
  quote("a" + 1),
  quote(-"a"),
  quote(!"a"),
  quote(sqrt("a")),
  quote(sum("a")),
  quote(mean("a")),
  quote(max(numeric(0))),
  quote(numeric(-1)),
  quote(vector("foo", 2)),
  quote(rep(1:2, times = -1)),
  quote(seq(1, 10, by = -1)),
  quote(seq_len(-1)),
  quote(1:NA),
  quote(seq_len(NA)),
  quote((1:3)[[5]]),
  quote(list(a = 1)[[2]]),
  quote(if (c(TRUE, FALSE)) 1),
  quote(if (NA) 1),
  quote(if (logical(0)) 1),
  quote(.Machine$integer.max + 1L)
)
for (case in cases) {
  cat(deparse(case), "\n  ->", condition_text(eval(case)), "\n")
}
#> as.numeric("seven")
#>   -> warning: NAs introduced by coercion
#> as.integer(3e+09)
#>   -> warning: NAs introduced by coercion to integer range
#> as.raw(300)
#>   -> warning: out-of-range values treated as 0 in coercion to raw
#> 1:3 + 1:2
#>   -> warning: longer object length is not a multiple of shorter object length
#> "a" + 1
#>   -> error: non-numeric argument to binary operator
#> -"a"
#>   -> error: invalid argument to unary operator
#> !"a"
#>   -> error: invalid argument type
#> sqrt("a")
#>   -> error: non-numeric argument to mathematical function
#> sum("a")
#>   -> error: invalid 'type' (character) of argument
#> mean("a")
#>   -> warning: argument is not numeric or logical: returning NA
#> max(numeric(0))
#>   -> warning: no non-missing arguments to max; returning -Inf
#> numeric(-1)
#>   -> error: invalid 'length' argument
#> vector("foo", 2)
#>   -> error: vector: cannot make a vector of mode 'foo'.
#> rep(1:2, times = -1)
#>   -> error: invalid 'times' argument
#> seq(1, 10, by = -1)
#>   -> error: wrong sign in 'by' argument
#> seq_len(-1)
#>   -> error: argument must be coercible to non-negative integer
#> 1:NA
#>   -> error: NA/NaN argument
#> seq_len(NA)
#>   -> error: argument must be coercible to non-negative integer
#> (1:3)[[5]]
#>   -> error: subscript out of bounds
#> list(a = 1)[[2]]
#>   -> error: subscript out of bounds
#> if (c(TRUE, FALSE)) 1
#>   -> error: the condition has length > 1
#> if (NA) 1
#>   -> error: missing value where TRUE/FALSE needed
#> if (logical(0)) 1
#>   -> error: argument is of length zero
#> .Machine$integer.max + 1L
#>   -> warning: NAs produced by integer overflow
Condition When it appears What to do

NAs introduced by coercion (warning)

as.numeric(), as.integer() or as.double() on a string that is not a number, including the string "NA"

look at the strings that failed (x[is.na(as.numeric(x)) & !is.na(x)]) before deciding to keep, clean or reject them

NAs introduced by coercion to integer range (warning)

as.integer() on a number or string outside +/-2147483647

use a double, keep the value as text if it is an identifier, or use a package with 64-bit integers

NAs produced by integer overflow (warning)

integer arithmetic whose result does not fit in 32 bits

use doubles (1 instead of 1L)

longer object length is not a multiple of shorter object length (warning)

arithmetic, comparison or logic with lengths that do not divide

make the lengths equal, or use rep_len() so the recycling is explicit

number of items to replace is not a multiple of replacement length (warning)

x[i] <- value with a value that does not fit

assign a value of length one, or of the same length as the selection

non-numeric argument to binary operator (error)

arithmetic with a string, a raw vector or a list

convert with as.numeric() (after checking), or compute on the right column

invalid 'type' (character) of argument (error)

sum() and similar on text

the same

invalid 'times' argument, invalid 'length' argument (errors)

negative, NA or non-numeric counts in rep(), numeric() and the other constructors

validate the count first

wrong sign in 'by' argument (error)

seq(1, 10, by = -1)

use a positive by for an increasing sequence, or rev()

NA/NaN argument (error)

1:NA

filter the missing value before building the range

argument must be coercible to non-negative integer (error)

seq_len(-1) or seq_len(NA)

check the count before building the sequence

subscript out of bounds (error)

x[[i]] with i beyond the length

check with length(), or use [ which returns NA

the condition has length > 1 (error, R 4.2.0 and later)

if (x == 1) with a vector x

any(), all() or isTRUE() make the intent explicit

argument is of length zero (error)

if (x == 1) with a zero-length x

test length(x) first

out-of-range values treated as 0 in coercion to raw (warning)

as.raw(256) or as.raw(-1)

keep values in 0 to 255

imaginary parts discarded in coercion (warning)

converting a complex vector to a real type

take Re() explicitly

vector: cannot make a vector of mode 'foo'. (error)

vector("foo", 2)

use one of the atomic modes, "list" or "expression"

The conditions system itself, with tryCatch(), withCallingHandlers() and custom conditions, is in Conditions and Error Handling.

Pitfalls

  • Treating a warning as noise. NAs introduced by coercion is the only sign that text was not a number. Convert, count the new NA values, and look at the offending strings.

  • 1:length(x) and 1:0. An empty vector turns the loop into two iterations. Use seq_along() and seq_len().

  • as.numeric() on a factor. The result is the codes (R FAQ 7.10: How do I convert factors to numeric?). Go through as.character() or the levels.

  • is.numeric() is not is.double(). It is true for integers and false for factors and dates.

  • class(x) == "matrix". Since R 4.0.0 the class of a matrix has length two. Use inherits() or is.matrix().

  • Silent recycling. A comparison with a vector whose length divides the length of the other one is recycled without a word; %in% is usually what was meant.

  • A named argument that c() keeps for itself. recursive and use.names are arguments, not names.

  • Mixed types in one column. One string turns a numeric vector into a character vector, and a double turns an integer vector into a double. Check typeof() after reading data and after assigning into a vector.

  • length(x) <- n on NULL. It does nothing and warns; create the right empty vector first.

  • as.integer(length(x)) for a long vector. The result is NA above 2147483647.

  • is.atomic(NULL) in old code. It was TRUE before R 4.4.0; code that relied on it needs is.null(x) || is.atomic(x).

  • Using T and F. They are ordinary variables that can be reassigned (R FAQ 3.3.3); write TRUE and FALSE.

Version notes

Version Change

R 3.0.0

Support for vectors longer than 2^31 - 1 elements on 64-bit platforms; length() returns a double for them (R NEWS: Changes in R 3.0.0)

R 3.5.0

ALTREP: sequences such as 1:n and seq_along() use a compact representation, and coercing integer and double vectors to character is deferred until first use (R NEWS: Changes in R 3.5.0)

R 4.0.0

Matrices inherit from class "array", so class(matrix) has length two; raw string constants r"(...)"; stringsAsFactors = FALSE is the default in data.frame() and read.table() (R NEWS: Changes in R 4.0.0)

R 4.2.0

if() and while() with a condition of length greater than one is an error (R NEWS: Changes in R 4.2.0)

R 4.4.0

is.atomic(NULL) returns FALSE (R NEWS: Changes in R 4.4.0)

R-devel (4.7.0)

x[logi] warns when the length of x is not a multiple of the length of the logical index (R-devel NEWS, not yet released)

Where older tutorials differ

Material written for R 3.x and earlier, and tutorials that translate habits from other languages, differ from current practice in these places.

Older tutorial Current practice Why Version

is.atomic(x) is TRUE for NULL

is.atomic(NULL) is FALSE; test is.null(x) as well when NULL must pass

NULL is not an atomic vector

R 4.4.0

class(x) == "matrix" to detect a matrix

is.matrix(x) or inherits(x, "matrix")

matrices have class c("matrix", "array")

R 4.0.0

Vectors cannot have more than 2147483647 elements

long vectors up to 2^52 elements exist on 64-bit builds; length() may return a double

the length is stored as a 64-bit value

R 3.0.0

1:n allocates n numbers

1:n is a compact sequence that is expanded lazily

ALTREP

R 3.5.0

for (i in 1:length(x))

for (i in seq_along(x))

1:0 is c(1, 0)

always

sapply() to build a vector of known type

vapply() with a FUN.VALUE template

sapply() returns a list for empty input and guesses the shape

always

T and F for TRUE and FALSE

TRUE and FALSE

T and F can be reassigned (R FAQ 3.3.3)

always

as.numeric(f) to get numbers from a factor of numbers

as.numeric(as.character(f)) or as.numeric(levels(f))[f]

the codes are returned (R FAQ 7.10)

always

read.csv() and data.frame() turn text columns into factors

text columns stay character; stringsAsFactors = FALSE is the default

the default changed (R FAQ 7.10 still applies to explicit factor() columns)

R 4.0.0

5 and 5L are interchangeable

identical(5L, 5) is FALSE: they differ in type, and 5L / 2L is a double

integer and double are separate types, and == hides the difference

always

mode(x) as the way to get the type

typeof(x) for the type, class(x) for dispatch, inherits() for tests

mode() merges integer and double

always

seq(1, n) or seq(x) to iterate

seq_len(n) and seq_along(x)

seq(x) depends on the length of x

always

Functions used on this page

Function Help page

typeof()

typeof

mode(), storage.mode() and their replacement forms

mode

class(), inherits(), unclass(), oldClass()

class

is.numeric(), as.numeric(), numeric()

numeric

is.double(), as.double(), double()

double

is.integer(), as.integer(), integer()

integer

is.logical(), as.logical(), logical()

logical

is.character(), as.character(), character()

character

is.complex(), as.complex(), complex(), Re(), Im(), Mod(), Conj()

complex

is.raw(), as.raw(), raw()

raw

charToRaw(), rawToChar(), rawToBits(), packBits(), rawShift()

rawConversion

utf8ToInt(), intToUtf8()

utf8Conversion

is.atomic(), is.recursive()

is.atomic

is.vector(), as.vector(), vector()

vector

c()

c

:

Colon

seq(), seq_len(), seq_along(), seq.int()

seq

rep(), rep_len(), rep.int()

rep

length(), length<-

length

lengths()

lengths

vapply(), sapply(), lapply()

lapply

identical()

identical

all.equal()

all.equal

unlist()

unlist

attributes(), structure(), attr()

attributes and structure

pmax(), pmin(), max(), min()

Extremes

round(), trunc(), ceiling(), floor()

Round

str()

str

head(), tail()

head

.Machine

.Machine

Where to go next