02 · Variables & Types¶
Assignment¶
R's idiomatic assignment operator is <-, though = works identically at
the top level (they diverge in edge cases, e.g. inside function-call
arguments, where = sets a named argument rather than assigning a variable).
x <- 42 # idiomatic
y = 42 # also works, less idiomatic outside function arguments
42 -> z # right-to-left assignment also exists, rarely used
x
# [1] 42
Basic types¶
R has four core atomic types you'll use constantly, plus a fifth for missing data:
n <- 42L # integer (the L suffix forces integer, not double)
d <- 3.14 # double (the default for any plain number)
s <- "hello" # character (R has no separate single-char "char" type)
b <- TRUE # logical (TRUE/FALSE, or the shorthand T/F)
m <- NA # "Not Available" -- a missing value, valid in any type
class(n)
# [1] "integer"
class(d)
# [1] "numeric"
class(s)
# [1] "character"
class(b)
# [1] "logical"
class(m)
# [1] "logical" -- a bare NA defaults to logical unless typed otherwise
42 is a double, not an integer, by default
Typing x <- 42 makes x a double (numeric). To get a true
integer you must write 42L. This rarely matters for everyday scripts,
but it matters for class() checks and for interfacing with code that
expects a specific type.
Checking and converting types¶
x <- "123"
is.character(x) # TRUE
is.numeric(x) # FALSE
as.numeric(x) # 123 -- converts the string to a double
as.character(456) # "456" -- converts a number to a string
as.integer(3.9) # 3 -- truncates, does not round
as.numeric("abc")
# [1] NA
# Warning message: NAs introduced by coercion
Failed numeric conversions produce NA with a warning rather than crashing —
a common source of silent data-quality bugs, so it's worth checking for NA
after converting user-provided or file-read data.
NULL vs. NA¶
These are easy to confuse:
NAmeans "a value exists conceptually, but it's missing" — e.g. a survey respondent skipped a question. It has a type and takes up a slot in a vector.NULLmeans "nothing here at all" — an empty, zero-length object. It cannot be an element of a vector; assigningNULLto a list element removes that element.
x <- c(1, NA, 3)
length(x)
# [1] 3 -- NA still occupies a slot
y <- NULL
length(y)
# [1] 0 -- NULL is nothing
is.na(x)
# [1] FALSE TRUE FALSE
Vectors are the default container¶
Even a "single value" in R is technically a length-1 vector — there's no separate scalar type:
Combine values into a longer vector with c() ("combine"):
ages <- c(25, 31, 42, 19)
ages
# [1] 25 31 42 19
names_vec <- c("Alice", "Bob", "Carol")
names_vec
# [1] "Alice" "Bob" "Carol"
A vector holds a single type — mixing types forces R to coerce everything to the "widest" common type (logical → integer → double → character):
mixed <- c(1, "two", TRUE)
mixed
# [1] "1" "two" "TRUE" -- everything became character
class(mixed)
# [1] "character"
Operators¶
7 + 3 # 10
7 - 3 # 4
7 * 3 # 21
7 / 3 # 2.333333
7 %/% 3 # 2 -- integer division
7 %% 3 # 1 -- modulo (remainder)
7 ^ 2 # 49 -- exponentiation (** also works)
5 == 5 # TRUE
5 != 4 # TRUE
5 > 4 # TRUE
5 >= 5 # TRUE
TRUE && FALSE # FALSE -- logical AND (scalar, short-circuits)
TRUE || FALSE # TRUE -- logical OR (scalar, short-circuits)
!TRUE # FALSE -- logical NOT
c(TRUE, FALSE) & c(TRUE, TRUE) # element-wise AND: TRUE FALSE
&&/|| vs. &/|
Use && and || for single logical values (e.g. in if conditions) —
they short-circuit and only look at the first element. Use & and |
for element-wise operations across whole vectors, which is what you'll
do constantly once you're filtering data frames.
Naming conventions¶
R style guides (e.g. the tidyverse style guide) favor snake_case for
variable and function names:
student_age <- 21 # preferred
studentAge <- 21 # valid, but not idiomatic in R
StudentAge <- 21 # valid, but reads as a class/constructor name
Variable names must start with a letter or a dot (not followed by a digit),
and can contain letters, digits, ., and _. Reserved words like if,
function, and TRUE cannot be used as names.
Type cheat sheet¶
| Type | Example | class() |
|---|---|---|
| Double | 3.14, 42 |
"numeric" |
| Integer | 42L |
"integer" |
| Character | "hello" |
"character" |
| Logical | TRUE, FALSE |
"logical" |
| Missing | NA |
type of the containing vector |
| Nothing | NULL |
"NULL" |
How It Actually Works¶
<- doesn't "store a value in a box" the way many languages describe
assignment — R uses name-to-object binding with copy-on-modify
semantics. x <- 5 creates a numeric vector of length 1 somewhere in
memory and binds the name x (in the current environment, a hash table
under the hood) to point at it. y <- x does not copy that memory; y
and x both point at the same object, and R tracks a reference count on
it. Only when you modify y (e.g. y[1] <- 10) does R notice more than
one name points at the object, and then it duplicates the underlying
memory before writing — this is why the operation is called copy-on-modify
rather than copy-on-assign. You can see it with tracemem(x).
Every R value, no matter how small, is a SEXP (S-expression) — a C
struct with a type tag, reference/attribute metadata, and a pointer to the
actual data. This is why a "scalar" in R doesn't exist as a primitive the
way it does in C: 5 is actually a numeric vector of length 1. Type
coercion (e.g. TRUE + 1) works because R defines an implicit hierarchy —
logical < integer < double < character — and silently promotes the
"lower" type to the "higher" one before the operation runs, using
internal C coercion routines rather than any R-level if logic.
🔀 See this in another language¶
- MATLAB — Variables & Basic Data Types
- JavaScript — Variables, Data Types & Operators
- Rust — Variables, Types & Ownership Basics
Exercise¶
Create variables for a person's name (character), age (integer, using the
L suffix), height_m (double), and is_student (logical). Print each one's
value and its class(). Then create a vector mixed <- c(age, name) and
print it along with its class() — explain in a comment why the numeric
value got converted to a string.