Names and Values

Applying R to Lifestyle and Brain Health Research

Brian C. Helsel, PhD

University of Kansas Medical Center

August 12, 2026

Binding Basics

library(lobstr)
x <- c(1, 2, 3)
obj_addr(x)
#> 0x8d526b868

Behind the scenes R creates a vector object of values c(1, 2, 3) and binds it to a name x with an object address of 0x8d526b868.

# Creates another binding to the same object
y <- x
obj_addr(y)
#> 0x8d526b868

Copy-on-modify

y[[3]] <- 4
obj_addr(y)
#> 0x8d56e6968

Modifying y creates a new object that is a copy of 0x8d526b868 and has a new object address of 0x8d56e6968.

We can see when an object gets copied with base::tracemem()

y <- x
tracemem(x)
#> [1] "<0x8d526b868>"
y[[3]] <- 4
#> [1] "tracemem[0x8d526b868 -> 0x8d56e6968]:"
untracemem(x)

Function Calls

The same rules for copying apply to function calls.

f <- function(a) {
  a
}

tracemem(x)
#> "<0xb82219c28>"

z <- f(x)

untracemem(x)

Once f() completes, x and z will point to the same object.

obj_addr(x)
#> [1] 0xb82219c28

obj_addr(z)
#> [1] 0xb82219c28

Lists

Elements of lists can also point to objects, but copy-on-modify differs from variables in that a shallow copy is created.

l1 <- list(1, 2, 3)

A list stores references to the values rather than the values. This is important to know for understanding what happens when we modify a list.

l2 <- l1

l2[[3]] <- 4

Using the ref() function from the lobstr package shows the shared bindings across l1 and l2.

ref(l1, l2)

#> █ [1:0xb7b9c0c38] <list>
#> ├─[2:0xb7965c2e0] <dbl>
#> ├─[3:0xb7965c2a8] <dbl>
#> └─[4:0xb7965c270] <dbl>

#> █ [5:0xb7b99caf8] <list>
#> ├─[2:0xb7965c2e0]
#> ├─[3:0xb7965c2a8]
#> └─[6:0xb796a54d0] <dbl>

Data frames

Data frames are lists of vectors and copy-on-modify works differently for columns and rows.

d1 <- data.frame(x = c(1, 5, 6), y = c(2, 4, 3))

Columns

Only the modified column is changed.

d2 <- d1
d2[, 2] <- d2[, 2] * 2

Rows

Every column is modified and must be copied.

d3 <- d1
d3[1, ] <- d3[1, ] * 3

Character Vectors

R uses a global string pool where each element in a character vector points to a unique string in the pool.

x <- c("a", "a", "abc", "d")
ref(x, character = TRUE)

#> █ [1:0xb81432af8] <chr>
#> ├─[2:0xb86933428] <string: "a">
#> ├─[2:0xb86933428]
#> ├─[3:0xb784853b8] <string: "abc">
#> └─[4:0xb86b28040] <string: "d">

The global string pool has an impact on the amount of memory a character vector uses, but can otherwise be thought of similarly to numeric vectors.

Object Size

Use lobstr::obj_size to find out how much memory an object takes.

set.seed(123)
x <- rnorm(1000, mean = 35, sd = 5)
y <- list(x, x, x)

obj_size(x)
#> 8.05 kB

obj_size(y)
#> 8.13 kB

The elements of a list are references to the values. Storing x three times is the size of an empty list with three NULL elements.

obj_size(list(NULL, NULL, NULL)) + obj_size(x)
#> 8.13 kB

Character vectors also take up less memory than expected.

fruits <- c("apple", "apple", "apple")

# Repeating Apple 3 Times
obj_size(fruits)
#> 136 B

# Repeating Apple 30 Times
obj_size(rep(fruits, 10))
#> 344 B

# Repeating Apple 75 Times
obj_size(rep(fruits, 25))
#> 704 B

Object Size for Shared Values

References make it more challenging to think about the size of individual objects. The object size only equals the combined object size if there are no shared values.

fruits <- c("apple", "apple", "apple")
vegetables <- c("broccoli", "broccoli", "broccoli")
fruits_or_vegetables <- c("apple", "broccoli", "apple")

obj_size(fruits, vegetables)
#> 280 B
obj_size(fruits) + obj_size(vegetables)
#> 280 B
obj_size(fruits, fruits_or_vegetables)
#> 280 B
obj_size(fruits) + obj_size(fruits_or_vegetables)
#> 336 B

Alternative Representation

R has the ability to represent some vectors compactly. For example, using the : when providing a numeric range stores the first and last number only such that every sequence is always the same size.

print(1:10)
#> [1] 1 2 3 4 5 6 7 8 9 10
obj_size(1:10)
#> 680 B
obj_size(1:1000)
#> 680 B
obj_size(1:1000000)
#> 680 B

Environments

R environments are modified-in-place rather than copied-on-modify. When you modify an environment, all existing bindings to that environment continue to have the same reference.

e1 <- rlang::env(a = 1, b = 2, c = 3)
e2 <- e1

# e2$c changes with the modification of e1$c
e1$c <- 4

print(e2$c)
#> [1] 4

Unbinding and the garbage collector

# [1] Assigns x to numeric object
x <- 1:3

# [2] Reassigns x to new numeric object
x <- 2:4

# [3] Unbinds x from numeric objects
rm(x)
[1]

[2]

[3]

After running this code, we have two numeric objects not bound to a name. The garbage collector (GC) frees up memory by deleting R objects that are no longer used. The garbage collector runs manually using the gc function or automatically when R needs more memory to create new objects. You can also check the memory used using the lobstr::mem_used function.