R PROGRAMMING • SYNTAX AND CORE TYPES

R Data Types — Work with numeric, integer, character, logical types

Understanding R's atomic type system is foundational to writing correct, efficient statistical programs.

Historical Context & Motivation

Every programming language must decide how to represent and manipulate data in memory, and the choices a language's designers make about type systems profoundly shape its expressiveness, safety, and performance characteristics. R's type system descends from a lineage of statistical computing languages that prioritized interactive data analysis over systems-level concerns like manual memory management. Unlike C or Java, where a programmer explicitly declares every variable's type before use, R employs dynamic typing — the interpreter infers a value's type at runtime, and a single variable can be reassigned to hold a different type entirely. This design choice emerged directly from R's heritage: a language built by statisticians for statisticians, where rapid prototyping mattered more than compile-time guarantees.

1976
S Language Created at Bell Labs
John Chambers and colleagues at Bell Labs created the S language for statistical computing. S introduced the concept of treating all data as vectors by default, with basic types including numeric, character, and logical — a design that R would later inherit wholesale.
1993
R Development Begins
Ross Ihaka and Robert Gentleman at the University of Auckland began developing R as a free, open-source implementation of S. They preserved S's atomic type system but implemented it atop a Lisp-like interpreter with garbage collection, formalizing the six atomic vector types that persist today.
2000
R 1.0.0 Released
The first stable release of R codified the type hierarchy: logical, integer, double (numeric), complex, character, and raw. The CRAN package repository launched the same year, and the type system's consistency across packages became a critical interoperability guarantee.
2011–present
Tidyverse and Type-Aware Tooling
Hadley Wickham's tidyverse ecosystem introduced stricter conventions around types — for instance, tibbles enforce column types more predictably than base data frames. Packages like vctrs formalized type coercion rules, reflecting the community's growing attention to type safety in production R code.

The central question this lesson addresses is deceptively simple: when you assign x <- 42 in R, what exactly is x? Is it an integer, a floating-point number, something else? Understanding how R categorizes, stores, and implicitly converts between its atomic types is essential for debugging subtle errors — the kind where TRUE + 1 yields 2 and "3" + 1 throws an error, even though both seem like they should work.

Core Principles & Definitions

R's type system rests on a few foundational ideas that distinguish it from most general-purpose languages. The most important is that R has no true scalar types — even a single value like 5L is actually a vector of length one. Every atomic value in R belongs to one of six atomic vector types, four of which are the most commonly encountered in day-to-day programming: numeric (double), integer, character, and logical. The remaining two — complex and raw — serve specialized purposes and are less frequently used in typical data analysis workflows.

1

Everything Is a Vector

R has no scalar types. The expression x <- 3.14 creates a numeric vector of length 1. This vectorization pervades the language: operations automatically broadcast across elements, eliminating explicit loops for elementwise computation.
2

Atomic Homogeneity

An atomic vector can hold elements of only one type. If you attempt to combine different types in c(), R silently coerces all elements to the most general type. This implicit coercion follows a strict hierarchy: logical → integer → double → character.
3

Dynamic Typing, Strong Semantics

Variables are not declared with types; R infers them at runtime. However, R is not weakly typed in the JavaScript sense — adding a character to a number produces an error, not silent concatenation. Type discipline is enforced at the operator level.
4

typeof() vs. class()

The function typeof() returns the internal storage mode (e.g., "double", "integer"), while class() returns the object-oriented class (e.g., "numeric", "factor"). Distinguishing these two queries is critical for debugging type-related issues.
KEY TAKEAWAY
Think of R's type coercion hierarchy like a series of increasingly capacious containers. A logical value (TRUE/FALSE) fits inside an integer container (1/0), which fits inside a double container (1.0/0.0), which fits inside a character container ("1"/"0"). Mixing types in a vector is like pouring liquids into the smallest container that holds them all — R always upsizes, never downsizes, and the result is always homogeneous.

Visual Explanation — The Type Hierarchy

The top row shows the three numeric-compatible atomic types (logical, integer, double) with solid arrows indicating explicit coercion functions. Dashed arrows show that any type can be coerced to character. The spectrum bar at the bottom illustrates implicit coercion: when types are mixed in a single vector via c(), R always coerces to the widest (rightmost) type present.

The diagram above captures R's single most important typing rule: implicit coercion always moves toward the most general type. A logical value can be losslessly represented as an integer (TRUE becomes 1, FALSE becomes 0), an integer can be losslessly represented as a double (42L becomes 42.0), and any value can be losslessly represented as a character string. The reverse is not true — as.integer("hello") produces NA with a warning, because the string has no sensible integer representation. This asymmetry is precisely why coercion is one-directional: information can be preserved when widening, but may be lost when narrowing.

How R Stores Types Internally

Understanding R's type system at a deeper level requires examining how each atomic type maps to underlying C representations in R's interpreter. When you create a variable, R allocates a SEXP (S-expression pointer) — a C-level structure that contains a type tag (called SEXPTYPE), metadata (length, attributes), and a pointer to the data payload. The four primary SEXPTYPE values for our atomic types are LGLSXP (logical), INTSXP (integer), REALSXP (double/numeric), and STRSXP (character).

MEMORY LAYOUT — NUMERIC VECTOR
SEXP → [ SEXPTYPE=REALSXP | length=n | attributes | data: double[n] ]
Each element in a numeric (double) vector occupies 8 bytes (64-bit IEEE 754). An integer vector uses 4 bytes per element (32-bit signed). Logical vectors also use 4 bytes per element internally (stored as int). Character vectors store pointers to a global string pool (CHARSXP cache), so identical strings share memory.
MEMORY SIZE FORMULA
bytes(v) = header_overhead + n × sizeof(element_type)
Where n is the vector length, sizeof(double) = 8, sizeof(integer) = 4, sizeof(logical) = 4. You can verify with object.size() in R. For a 1-million-element vector, using integer instead of double saves approximately 4 MB.
⚠️ Why 42 is a double, not an integer
A common source of confusion: by default, R stores bare numeric literals like 42 as doubles (REALSXP), not integers. To create an integer, you must append the L suffix: 42L. This is a deliberate design choice — since R is primarily used for statistical computing, and most statistical operations produce real-valued results, defaulting to double avoids unnecessary integer-to-double coercions in the common case. Verify: typeof(42) returns "double", while typeof(42L) returns "integer".

The practical implication of these storage details becomes significant at scale. If you are working with a dataset containing millions of rows and a column of small whole numbers (e.g., counts from 0 to 1000), storing them as integer rather than double halves the memory footprint for that column. Furthermore, integer arithmetic can be marginally faster on some architectures because the CPU can use integer-specific instructions. The object.size() function and the lobstr::obj_size() function from the lobstr package are your primary tools for profiling these memory differences.

Detailed Breakdown of Each Type

Comparison of R's four primary atomic vector types
Propertylogicalintegerdouble (numeric)character
typeof()"logical""integer""double""character"
class()"logical""integer""numeric""character"
Literal syntaxTRUE, FALSE, NA1L, 0L, NA_integer_3.14, 1e−3, Inf, NaN"abc", 'xyz', NA_character_
Storage (bytes/elem)4 (int internally)4 (32-bit signed)8 (64-bit IEEE 754)8 (pointer to CHARSXP)
NA variantNANA_integer_NA_real_NA_character_
Test functionis.logical()is.integer()is.double()is.character()
This diagram traces the internal representation of c(TRUE, 3L, 2.5, "R"). Because character is the widest type present, all four elements are coerced to character strings before the vector is assembled. The bottom row shows the resulting SEXP structure: a STRSXP header followed by four pointers into R's global string cache.

The detailed type comparison table above highlights several subtleties worth internalizing. First, note the asymmetry between typeof() and class() for doubles: typeof(3.14) returns "double" but class(3.14) returns "numeric". This is because "numeric" is an S3 class name that encompasses both doubles and integers in certain contexts, while "double" is the precise internal storage type. Second, each type has a typed NA variant: using NA_integer_ or NA_character_ ensures that a missing value does not inadvertently trigger coercion when inserted into a typed vector.

Worked Example — Type Inspection and Coercion

Suppose you are cleaning a dataset imported from a CSV file and need to verify that each column has the expected type before performing computations. The following worked example demonstrates a realistic type-checking and coercion workflow.

Diagnosing and fixing types in an imported dataset column
1
Step 1 — Create sample data and inspect typesAfter importing a CSV, numeric columns are sometimes read as character because of stray non-numeric entries. Let's simulate this scenario: ages <- c("21", "34", "28", "N/A", "45") Now inspect the type:typeof(ages) # "character" class(ages) # "character" is.character(ages) # TRUE
The vector is character — we cannot compute mean(ages) without coercion.
2
Step 2 — Attempt numeric coercionWe use as.numeric() (equivalent to as.double()) to convert: ages_num <- as.numeric(ages) # Warning: NAs introduced by coercion ages_num # 21 34 28 NA 45 The string "N/A" has no valid numeric representation, so R converts it to NA and emits a warning.
typeof(ages_num) returns "double". The fourth element is now NA_real_.
3
Step 3 — Convert to integer for storage efficiencyAges are whole numbers, so integer storage is appropriate: ages_int <- as.integer(ages_num) ages_int # 21 34 28 NA 45 typeof(ages_int) # "integer" We verify memory savings:object.size(ages_num) # 96 bytes object.size(ages_int) # 72 bytes
For this small vector the savings are modest, but at 10⁶ elements the difference is ≈ 4 MB.
4
Step 4 — Create a logical mask for missing dataWe often need to identify which entries are missing: mask <- is.na(ages_int) mask # FALSE FALSE FALSE TRUE FALSE typeof(mask) # "logical" Now leverage logical-to-integer coercion to count NAs: sum(mask) # 1 The sum() function coerces TRUE → 1 and FALSE → 0, then sums.
There is exactly 1 missing value in the vector. The idiom sum(is.na(x)) is one of the most common uses of logical-to-integer coercion in practice.
5
Step 5 — Compute the mean, excluding NAsmean(ages_int, na.rm = TRUE) # 32 The na.rm = TRUE argument removes NA values before computation. Without it, mean(ages_int) returns NA because R propagates missingness by default — a principled design choice that forces the analyst to make explicit decisions about missing data.
The mean age is 32, computed as (21 + 34 + 28 + 45) / 4 = 128 / 4 = 32.

Strengths, Pitfalls, and Common Gotchas

Strengths and pitfalls of R's type system design choices
FeatureStrengthPitfall
Implicit coercionEnables elegant idioms like sum(x > 0) to count TRUE values without explicit casting.Silently converts types when combining vectors, potentially producing unexpected character vectors from mixed inputs.
Default doubleAvoids integer overflow (R integers are 32-bit, max ≈ 2.1 × 10⁹) in most statistical operations.Floating-point comparison issues: 0.1 + 0.2 == 0.3 returns FALSE. Use all.equal() instead.
NA propagationForces explicit handling of missing data, preventing silent computation on incomplete datasets.Forgetting na.rm = TRUE causes entire summaries to return NA. Beginners often spend debugging time on this.
Dynamic typingRapid prototyping — no type declarations, fast iterative analysis at the REPL.Type errors surface at runtime, not compile time. A function expecting numeric may silently receive character and fail deep in a pipeline.
Character string poolIdentical strings share memory via CHARSXP caching, making character vectors of repeated values very memory-efficient.Unique strings (e.g., UUIDs) each allocate separate cache entries, which can cause unexpected memory bloat with millions of distinct strings.
KEY TAKEAWAY
R's type system is optimized for the interactive data analysis use case — it prioritizes convenience and expressiveness over compile-time safety. This is analogous to the engineering trade-off between a dynamically typed scripting language like Python and a statically typed systems language like Rust. In R, the equivalent of type annotations is defensive use of stopifnot(is.numeric(x)) assertions at function boundaries — a practice that becomes increasingly important as your R code moves from exploratory analysis to production pipelines.

Connection to Advanced Type Constructs

The four atomic types covered in this lesson are the foundation upon which R builds its more complex data structures. Understanding this foundation makes the higher-level abstractions — factors, dates, data frames, tibbles — far less mysterious, because each of these is ultimately an atomic vector with attributes attached. A factor, for instance, is an integer vector with a levels attribute and a class attribute set to "factor". A Date is a double vector with class = "Date", storing the number of days since 1970-01-01.

Mapping atomic type concepts to advanced R constructs
ConceptThis Lesson (Atomic Types)Advanced Topic
Homogeneous collectionAtomic vectors — all elements same typeMatrices & arrays — multi-dimensional atomic vectors
Heterogeneous collectionNot possible in a single atomic vectorLists — each element can be any type; data frames are lists of equal-length vectors
Categorical dataStored as character vectorsFactors — integer-backed with level labels, enabling ordered categories and efficient storage
Type safetyRuntime checks via is.*() functionsThe vctrs package provides formal type hierarchies; R7/S7 classes add method dispatch with type contracts
Missing dataTyped NAs: NA_integer_, etc.tidyr and data.table provide structured missing-data imputation workflows built on typed NAs

As you move into subsequent lessons on lists, data frames, and the S3/S4 object systems, you will see that mastering atomic types is not merely an academic exercise — it is the prerequisite for understanding why df$column behaves the way it does, why certain joins fail in dplyr, and why Rcpp interfaces require you to declare exact SEXP types when passing data between R and C++.

Practice Problems

PROBLEM 1CONCEPTUAL
Explain why typeof(42) returns "double" rather than "integer", even though 42 is a whole number. What design rationale justifies this default, and how would you create an integer 42 instead?
PROBLEM 2BASIC CALCULATION
Predict the output type and value of each of the following expressions. Verify your predictions by stating what typeof() would return for each: (a) TRUE + FALSE + TRUE (b) c(1L, 2.5) (c) paste(42, "is the answer")
PROBLEM 3INTERMEDIATE
Consider the vector v <- c(TRUE, 3L, 4.5, "hello"). (a) What is typeof(v)? (b) What are the exact string values stored in v? (c) Now suppose you run as.numeric(v). What result do you get and why?
PROBLEM 4APPLIED
You import a CSV with 500,000 rows. One column, status, should contain only TRUE or FALSE but was imported as character ("TRUE", "FALSE", and some "Yes"/"No" entries). Write R code that: (1) converts the column to logical, correctly handling "Yes" → TRUE and "No" → FALSE, (2) reports how many entries could not be converted, and (3) verifies the final type.
PROBLEM 5CRITICAL THINKING
R's coercion hierarchy is logical → integer → double → character. Propose and justify an alternative design where the hierarchy were different — for example, what if R did not implicitly coerce logical to integer? Discuss at least two idioms from real R code that would break and how the language would need to compensate. Conversely, identify one class of bug that such a change would prevent.

Summary — R Data Types

R's type system is built on six atomic vector types, four of which are central to everyday programming: logical (TRUE/FALSE), integer (suffixed with L), double (the default for all numeric literals), and character (strings). R has no true scalars — every value is a vector of length one. When types are mixed in a single vector, R applies implicit coercion following the hierarchy logical → integer → double → character, always widening to the most general type present.

Use typeof() to query internal storage mode and class() for the S3 class; these can differ (e.g., typeof returns "double" while class returns "numeric"). Each type has a typed NA variant (NA_integer_, NA_real_, NA_character_) to prevent unintended coercion. Explicit coercion functions — as.integer(), as.double(), as.character(), as.logical() — give you precise control. Mastering these atomic types is the prerequisite for understanding every higher-level R data structure: factors, matrices, lists, and data frames are all built on this atomic vector foundation.

Varsity Tutors • R Programming • R Data Types — Work with numeric, integer, character, logical types