Chapter 8 $ and [[ Operator Errors

What You’ll Learn:

  • Differences between $, [[]], and []
  • When each operator works (and fails)
  • Recursive indexing in lists
  • Partial matching pitfalls
  • Atomic vector vs list extraction

Key Errors Covered: 12+ operator-specific errors

Difficulty: ⭐⭐ Intermediate

8.1 Introduction

R has three main extraction operators that confuse everyone:

my_list <- list(name = "Alice", age = 25, scores = c(85, 90, 95))

# Three ways to get the same thing:
my_list$name
#> [1] "Alice"
my_list[["name"]]
#> [1] "Alice"
my_list["name"]  # Wait, this is different!
#> $name
#> [1] "Alice"
# Check the types
class(my_list$name)        # "character"
#> [1] "character"
class(my_list[["name"]])   # "character"
#> [1] "character"
class(my_list["name"])     # "list" - different!
#> [1] "list"

Understanding these differences prevents endless frustration.

💡 Key Insight: The Three Operators

lst <- list(x = 1:3, y = "text", z = list(a = 10))

# $ - Extract by name (partial matching!)
lst$x        # c(1, 2, 3)
#> [1] 1 2 3
lst$x        # Same as lst[["x"]]
#> [1] 1 2 3

# [[ ]] - Extract single element (no partial matching)
lst[["x"]]   # c(1, 2, 3)
#> [1] 1 2 3
lst[[1]]     # Can use position too
#> [1] 1 2 3

# [ ] - Extract sub-list (keeps structure)
lst["x"]     # list(x = 1:3)
#> $x
#> [1] 1 2 3
lst[1]       # list(x = 1:3)
#> $x
#> [1] 1 2 3
lst[1:2]     # list(x = 1:3, y = "text")
#> $x
#> [1] 1 2 3
#> 
#> $y
#> [1] "text"

Rule of thumb: - $ : Quick named access (interactive use) - [[]] : Safe programmatic access - [] : When you need to keep list structure

8.2 Error #1: $ operator is invalid for atomic vectors

⭐ BEGINNER 🔢 TYPE

8.2.1 The Error

x <- c(a = 1, b = 2, c = 3)  # Named vector
x$a  # $ doesn't work on vectors!
#> Error in x$a: $ operator is invalid for atomic vectors

🔴 ERROR

Error in x$a : $ operator is invalid for atomic vectors

8.2.2 What It Means

The $ operator only works on recursive objects (lists, data frames, environments). It doesn’t work on atomic vectors.

8.2.3 Understanding the Difference

# Atomic vector (1D, all same type)
vec <- c(a = 1, b = 2, c = 3)
is.atomic(vec)     # TRUE
#> [1] TRUE
is.recursive(vec)  # FALSE
#> [1] FALSE

# Use names indexing instead
vec["a"]
#> a 
#> 1
vec[["a"]]
#> [1] 1

# List (recursive)
lst <- list(a = 1, b = 2, c = 3)
is.atomic(lst)     # FALSE
#> [1] FALSE
is.recursive(lst)  # TRUE
#> [1] TRUE

# $ works here
lst$a
#> [1] 1

8.2.4 Common Causes

8.2.4.1 Cause 1: Confusion Between Vector and List

# Created a vector
values <- c(x = 10, y = 20)

# Treating it like a list
values$x  # Error!
#> Error in values$x: $ operator is invalid for atomic vectors

8.2.4.2 Cause 2: After Extracting from Data Frame

df <- data.frame(x = 1:3, y = 4:6)

# Extract column (becomes vector!)
col <- df$x
class(col)  # "integer" (atomic vector)
#> [1] "integer"

# Can't use $ on result
col$something  # Error!
#> Error in col$something: $ operator is invalid for atomic vectors

8.2.4.3 Cause 3: Function Returns Vector

get_values <- function() {
  c(a = 1, b = 2)  # Returns atomic vector
}

result <- get_values()
result$a  # Error!
#> Error in result$a: $ operator is invalid for atomic vectors

8.2.5 Solutions

SOLUTION 1: Use Correct Indexing for Vectors

vec <- c(a = 1, b = 2, c = 3)

# Right way for vectors:
vec["a"]     # Single bracket
#> a 
#> 1
vec[["a"]]   # Double bracket
#> [1] 1

# Get multiple elements
vec[c("a", "c")]
#> a c 
#> 1 3

# All names
names(vec)
#> [1] "a" "b" "c"

SOLUTION 2: Convert to List if Needed

vec <- c(a = 1, b = 2, c = 3)

# Convert to list
lst <- as.list(vec)

# Now $ works
lst$a
#> [1] 1

SOLUTION 3: Check Object Type First

safe_dollar <- function(x, name) {
  if (is.atomic(x) && !is.null(names(x))) {
    message("Using [[ ]] for atomic vector")
    return(x[[name]])
  } else if (is.recursive(x)) {
    return(x[[name]])
  } else {
    stop("Cannot extract '", name, "' from ", class(x)[1])
  }
}

# Test
vec <- c(a = 1, b = 2)
safe_dollar(vec, "a")
#> Using [[ ]] for atomic vector
#> [1] 1

lst <- list(a = 1, b = 2)
safe_dollar(lst, "a")
#> [1] 1

⚠️ Common Pitfall: Data Frame Columns

df <- data.frame(x = 1:3, y = 4:6, z = 7:9)

# Extracting a column
col <- df$x         # Vector (atomic)
col$something       # Error!
#> Error in col$something: $ operator is invalid for atomic vectors

# Selecting columns keeps data frame
subset <- df["x"]   # Data frame (recursive)
subset$x            # Works!
#> [1] 1 2 3

# Rule: Single column with $ or [[ ]] → vector
#       One or more columns with [ ] → data frame

8.3 Error #2: recursive indexing failed at level X

⭐⭐ INTERMEDIATE 📏 DIMENSION

8.3.1 The Error

my_list <- list(a = list(b = 1))
my_list[[c("a", "b", "c")]]  # Too deep!
#> Error in my_list[[c("a", "b", "c")]]: subscript out of bounds

🔴 ERROR

Error in my_list[[c("a", "b", "c")]] : 
  recursive indexing failed at level 3

8.3.2 What It Means

You’re trying to index deeper into a nested list than it actually goes.

8.3.3 Recursive Indexing

# Nested list
nested <- list(
  a = list(
    b = list(
      c = 42
    )
  )
)

# Recursive indexing with vector
nested[[c("a", "b", "c")]]
#> [1] 42

# Equivalent to:
nested[["a"]][["b"]][["c"]]
#> [1] 42

# But this fails (only 3 levels deep):
nested[[c("a", "b", "c", "d")]]  # Level 4 doesn't exist!
#> Error in nested[[c("a", "b", "c", "d")]]: subscript out of bounds

8.3.4 Common Causes

8.3.4.1 Cause 1: Wrong Path

data <- list(
  user = list(
    name = "Alice",
    age = 25
  )
)

# Typo in path
data[[c("user", "email")]]  # "email" doesn't exist
#> NULL

8.3.4.2 Cause 2: Mixed Types

data <- list(
  values = c(10, 20, 30)  # Atomic vector, not list!
)

# Can't recurse into atomic vector
data[[c("values", "1")]]  # Error at level 2
#> Error in data[[c("values", "1")]]: subscript out of bounds

8.3.4.3 Cause 3: Dynamic Path Too Long

data <- list(a = list(b = 1))
path <- c("a", "b", "c")  # Path too long

data[[path]]  # Error
#> Error in data[[path]]: subscript out of bounds

8.3.5 Solutions

SOLUTION 1: Check Path Exists

nested <- list(a = list(b = list(c = 42)))

safe_deep_extract <- function(x, path) {
  for (i in seq_along(path)) {
    if (!is.list(x) && !is.environment(x)) {
      stop("Cannot recurse at level ", i, 
           ": object is ", class(x)[1], ", not list")
    }
    
    if (!path[i] %in% names(x)) {
      stop("Name '", path[i], "' not found at level ", i)
    }
    
    x <- x[[path[i]]]
  }
  return(x)
}

# Test
safe_deep_extract(nested, c("a", "b", "c"))  # Works
#> [1] 42
safe_deep_extract(nested, c("a", "b", "c", "d"))  # Informative error
#> Error in safe_deep_extract(nested, c("a", "b", "c", "d")): Cannot recurse at level 4: object is numeric, not list

SOLUTION 2: Use purrr::pluck()

library(purrr)

nested <- list(a = list(b = list(c = 42)))

# Safe extraction with default
pluck(nested, "a", "b", "c")
#> [1] 42
pluck(nested, "a", "b", "c", "d")  # Returns NULL, not error
#> NULL
pluck(nested, "a", "b", "c", "d", .default = NA)
#> [1] NA

SOLUTION 3: Step-by-Step Extraction

nested <- list(a = list(b = list(c = 42)))
path <- c("a", "b", "c")

# Extract step by step with checking
result <- nested
for (step in path) {
  if (is.null(result)) {
    message("Path ended at NULL")
    break
  }
  
  if (!step %in% names(result)) {
    message("'", step, "' not found")
    result <- NULL
    break
  }
  
  result <- result[[step]]
}

result
#> [1] 42

8.4 Error #3: attempt to select less than one element

⭐⭐ INTERMEDIATE 📏 DIMENSION

8.4.1 The Error

my_list <- list(a = 1, b = 2)
my_list[[integer(0)]]  # Empty index!
#> Error in my_list[[integer(0)]]: attempt to select less than one element in get1index

🔴 ERROR

Error in my_list[[integer(0)]] : 
  attempt to select less than one element in integerOneIndex

8.4.2 What It Means

[[]] must select exactly one element, but you provided an empty index.

8.4.3 Common Causes

8.4.3.1 Cause 1: Empty Which() Result

my_list <- list(a = 1, b = 2, c = 3)

# Find elements meeting condition
indices <- which(sapply(my_list, function(x) x > 10))
length(indices)  # 0
#> [1] 0

# Try to extract
my_list[[indices]]  # Error!
#> Error in my_list[[indices]]: attempt to select less than one element in get1index

8.4.3.2 Cause 2: Filtered Index

values <- list(a = 5, b = 10, c = 15)

# Filter for values > 20
big_ones <- which(sapply(values, function(x) x > 20))

# Try to get first
values[[big_ones[1]]]  # NA[1] → error!
#> NULL

8.4.3.3 Cause 3: Off-by-One with Subtraction

my_list <- list(a = 1, b = 2)
index <- 1 - 1  # 0

my_list[[index]]  # Can't select 0th element
#> Error in my_list[[index]]: attempt to select less than one element in get1index <real>

8.4.4 Solutions

SOLUTION 1: Check Before Extracting

my_list <- list(a = 1, b = 2, c = 3)
indices <- which(sapply(my_list, function(x) x > 10))

# Check first
if (length(indices) > 0) {
  my_list[[indices[1]]]
} else {
  message("No elements found")
  NULL
}
#> No elements found
#> NULL

SOLUTION 2: Use [ ] for Multiple/Zero Elements

my_list <- list(a = 1, b = 2, c = 3)
indices <- which(sapply(my_list, function(x) x > 10))

# [ ] handles empty gracefully
my_list[indices]  # Returns empty list
#> named list()

SOLUTION 3: Safe Extraction Function

safe_extract_one <- function(x, i, default = NULL) {
  if (length(i) == 0) {
    message("No index provided")
    return(default)
  }
  
  if (is.na(i)) {
    message("Index is NA")
    return(default)
  }
  
  if (i < 1 || i > length(x)) {
    message("Index out of bounds: ", i)
    return(default)
  }
  
  return(x[[i]])
}

# Test
my_list <- list(a = 1, b = 2)
safe_extract_one(my_list, integer(0))
#> No index provided
#> NULL
safe_extract_one(my_list, 1)
#> [1] 1
safe_extract_one(my_list, 10)
#> Index out of bounds: 10
#> NULL

8.5 Error #4: attempt to select more than one element

⭐ BEGINNER 📏 DIMENSION

8.5.1 The Error

my_list <- list(a = 1, b = 2, c = 3)
my_list[[c(1, 2)]]  # Multiple indices!
#> Error in my_list[[c(1, 2)]]: subscript out of bounds

🔴 ERROR

Error in my_list[[c(1, 2)]] : 
  attempt to select more than one element in integerOneIndex

8.5.2 What It Means

[[]] extracts exactly one element. For multiple elements, use [].

8.5.3 Single vs Multiple Selection

my_list <- list(a = 1, b = 2, c = 3)

# [[ ]] - One element
my_list[[1]]      # Element 1
#> [1] 1
my_list[["a"]]    # By name
#> [1] 1

# [ ] - Multiple elements (returns list)
my_list[1]        # List with element 1
#> $a
#> [1] 1
my_list[1:2]      # List with elements 1 and 2
#> $a
#> [1] 1
#> 
#> $b
#> [1] 2
my_list[c("a", "c")]  # By names
#> $a
#> [1] 1
#> 
#> $c
#> [1] 3

8.5.4 Common Causes

8.5.4.1 Cause 1: Meant to Use [ ]

data <- list(x = 1:5, y = 6:10, z = 11:15)

# Want first two elements
data[[1:2]]  # Error!
#> [1] 2
# Use [ ] instead
data[1:2]
#> $x
#> [1] 1 2 3 4 5
#> 
#> $y
#> [1]  6  7  8  9 10

8.5.4.2 Cause 2: Vector of Names

data <- list(name = "Alice", age = 25, city = "NYC")

# Try to get multiple by name
cols <- c("name", "age")
data[[cols]]  # Error!
#> Error in data[[cols]]: subscript out of bounds
# Use [ ] for multiple
data[cols]
#> $name
#> [1] "Alice"
#> 
#> $age
#> [1] 25

# Or extract separately
lapply(cols, function(col) data[[col]])
#> [[1]]
#> [1] "Alice"
#> 
#> [[2]]
#> [1] 25

8.5.4.3 Cause 3: Recursive Indexing Confusion

nested <- list(a = list(b = 1, c = 2))

# This works (recursive indexing)
nested[[c("a", "b")]]  # Goes to nested$a$b
#> [1] 1

# But not multiple at one level
nested[[c("a"), c("b", "c")]]  # Error!
#> Error in nested[[c("a"), c("b", "c")]]: incorrect number of subscripts

8.5.5 Solutions

SOLUTION 1: Use [ ] for Multiple Elements

my_list <- list(a = 1, b = 2, c = 3, d = 4)

# Multiple elements - use single bracket
my_list[c(1, 3)]
#> $a
#> [1] 1
#> 
#> $c
#> [1] 3
my_list[c("a", "c")]
#> $a
#> [1] 1
#> 
#> $c
#> [1] 3

# Single element - use double bracket
my_list[[1]]
#> [1] 1
my_list[["a"]]
#> [1] 1

SOLUTION 2: Loop or Apply for Multiple

data <- list(x = 1:3, y = 4:6, z = 7:9)
elements_wanted <- c("x", "z")

# Extract each separately
result <- lapply(elements_wanted, function(name) data[[name]])
names(result) <- elements_wanted
result
#> $x
#> [1] 1 2 3
#> 
#> $z
#> [1] 7 8 9

# Or use [ ] and unlist if needed
data[elements_wanted]
#> $x
#> [1] 1 2 3
#> 
#> $z
#> [1] 7 8 9

8.6 Partial Matching with $

⚠️ Dangerous Pitfall: Partial Matching

The $ operator does partial matching by default:

my_list <- list(name = "Alice", age = 25)

# Exact match
my_list$name
#> [1] "Alice"

# Partial match (DANGEROUS!)
my_list$n     # Matches "name"
#> Warning in my_list$n: partial match of 'n' to 'name'
#> [1] "Alice"
my_list$na    # Matches "name"
#> Warning in my_list$na: partial match of 'na' to 'name'
#> [1] "Alice"
my_list$nam   # Matches "name"
#> Warning in my_list$nam: partial match of 'nam' to 'name'
#> [1] "Alice"

# Ambiguous partial match returns NULL
my_list$a     # Could be "age" - but only one letter, returns NULL
#> Warning in my_list$a: partial match of 'a' to 'age'
#> [1] 25

# [[ ]] does NOT partial match (SAFER)
my_list[["n"]]      # NULL
#> NULL
my_list[["name"]]   # Works
#> [1] "Alice"

Best Practice: Use [[]] in production code to avoid partial matching surprises.

# Instead of:
# data$col

# Use:
data[["col"]]  # Exact match required
#> NULL

8.7 Comparing Operators

💡 Key Insight: Complete Comparison

lst <- list(x = 1:3, y = "text", z = list(a = 10, b = 20))

# Extraction comparison
lst$x           # c(1, 2, 3) - vector
#> [1] 1 2 3
lst[["x"]]      # c(1, 2, 3) - vector
#> [1] 1 2 3
lst["x"]        # list(x = c(1, 2, 3)) - list
#> $x
#> [1] 1 2 3

# Type returned
class(lst$x)      # "integer"
#> [1] "integer"
class(lst[["x"]]) # "integer"
#> [1] "integer"
class(lst["x"])   # "list"
#> [1] "list"

# Multiple elements
# lst$c("x", "y")     # Can't do this
# lst[[c("x", "y")]]  # Error
lst[c("x", "y")]      # Works - returns list
#> $x
#> [1] 1 2 3
#> 
#> $y
#> [1] "text"

# Nested access
# lst$z$a            # 10
lst[["z"]]$a         # 10
#> [1] 10
lst[["z"]][["a"]]    # 10
#> [1] 10
lst[[c("z", "a")]]   # 10 (recursive indexing)
#> [1] 10
# lst[c("z", "a")]   # list with both z and a (different!)

Decision Tree:

Need to extract from list/data frame?
├─ One element?
│  ├─ Known name, interactive? → use $
│  ├─ Programmatic, exact name? → use [[]]
│  └─ By position? → use [[]]
└─ Multiple elements?
   └─ Use []

8.8 Data Frame Special Cases

🎯 Best Practice: Data Frame Extraction

df <- data.frame(x = 1:3, y = 4:6, z = 7:9)

# Column extraction
df$x              # Vector (drops to 1D)
#> [1] 1 2 3
df[["x"]]         # Vector (drops to 1D)
#> [1] 1 2 3
df["x"]           # Data frame (keeps 2D)
#>   x
#> 1 1
#> 2 2
#> 3 3
df[, "x"]         # Vector (drops by default)
#> [1] 1 2 3
df[, "x", drop = FALSE]  # Data frame (preserved)
#>   x
#> 1 1
#> 2 2
#> 3 3

# Multiple columns
# df$c("x", "y")  # Can't do
# df[[c("x", "y")]]  # Error
df[c("x", "y")]    # Data frame with 2 columns
#>   x y
#> 1 1 4
#> 2 2 5
#> 3 3 6
df[, c("x", "y")]  # Data frame with 2 columns
#>   x y
#> 1 1 4
#> 2 2 5
#> 3 3 6

# With dplyr (clearest!)
library(dplyr)
df %>% pull(x)       # Vector
#> [1] 1 2 3
df %>% select(x)     # Data frame
#> Error in select(., x): unused argument (x)
df %>% select(x, y)  # Data frame
#> Error in select(., x, y): unused arguments (x, y)

Rule: - $ and [[]] → Drop to vector (single column) - [] → Keep as data frame - [, , drop = FALSE] → Force data frame

8.9 Summary

Key Takeaways:

  1. $ only works on lists/data frames - Not atomic vectors
  2. [[ ]] requires exact names - No partial matching
  3. [[ ]] extracts one element - Use [] for multiple
  4. Recursive indexing - [[c(“a”, “b”)]] goes deep
  5. $ does partial matching - Dangerous, use [[ ]] in code
  6. [] keeps structure - Returns list/data frame
  7. [[ ]] and $ simplify - Return element itself

Quick Reference:

Operator Structure Elements Partial Match Use Case
$ List/DF One Yes Interactive
[[]] Any One No Programmatic
[] Any Multiple No Subsetting

Common Errors:

Error Cause Fix
$ invalid for atomic vectors Used $ on vector Use [[ ]] or []
recursive indexing failed Path too deep Check structure
select less than one Empty index in [[ ]] Check length first
select more than one Multiple indices in [[ ]] Use [] instead

Best Practices:

# ✅ Good
data[["column"]]           # Exact, no partial matching
data[c("col1", "col2")]    # Multiple columns
if (length(idx) > 0) data[[idx]]  # Check before [[]]

# ❌ Avoid in production
data$col                   # Partial matching risk
data[[multiple_indices]]   # Will error
data[[empty_vector]]       # Will error

8.10 Exercises

📝 Exercise 1: Operator Selection

Which operator(s) work for each scenario?

vec <- c(a = 1, b = 2)
lst <- list(a = 1, b = 2)
df <- data.frame(a = 1:3, b = 4:6)

# A: Get element "a" from vec
# B: Get element "a" from lst
# C: Get column "a" from df as vector
# D: Get column "a" from df as data frame
# E: Get elements "a" and "b" from lst

📝 Exercise 2: Debug the Extraction

Fix these extraction errors:

# Problem 1
numbers <- c(x = 10, y = 20, z = 30)
result <- numbers$x

# Problem 2
data <- list(values = c(1, 2, 3))
item <- data[[c("values", "1")]]

# Problem 3
my_list <- list(a = 1, b = 2, c = 3)
subset <- my_list[[c(1, 3)]]

# Problem 4
nested <- list(level1 = list(level2 = 10))
value <- nested$level1$level2$level3

📝 Exercise 3: Safe Accessor

Write safe_get(x, path, default = NULL) that: 1. Works with nested lists 2. Handles missing names gracefully 3. Returns default if path doesn’t exist 4. Works with both character names and numeric indices 5. Provides helpful error messages

📝 Exercise 4: Extraction Comparison

For this structure, show what each extraction returns:

data <- list(
  user = list(
    name = "Alice",
    scores = c(85, 90, 95)
  )
)

# What does each return? (value and type)
data$user
data[["user"]]
data["user"]
data$user$name
data[[c("user", "name")]]
data[["user"]][["scores"]][[2]]

8.11 Exercise Answers

Click to see answers

Exercise 1:

vec <- c(a = 1, b = 2)
lst <- list(a = 1, b = 2)
df <- data.frame(a = 1:3, b = 4:6)

# A: Get "a" from vector
vec["a"]      # ✅ Works
#> a 
#> 1
vec[["a"]]    # ✅ Works
#> [1] 1
# vec$a       # ❌ Error (atomic vector)

# B: Get "a" from list
lst$a         # ✅ Works
#> [1] 1
lst[["a"]]    # ✅ Works
#> [1] 1
lst["a"]      # ✅ Works (but returns list)
#> $a
#> [1] 1

# C: Get column "a" as vector
df$a          # ✅ Works
#> [1] 1 2 3
df[["a"]]     # ✅ Works
#> [1] 1 2 3
df[, "a"]     # ✅ Works
#> [1] 1 2 3
# df["a"]     # ❌ Returns data frame, not vector

# D: Get column "a" as data frame
df["a"]       # ✅ Works
#>   a
#> 1 1
#> 2 2
#> 3 3
df[, "a", drop = FALSE]  # ✅ Works
#>   a
#> 1 1
#> 2 2
#> 3 3
# df$a        # ❌ Returns vector
# df[["a"]]   # ❌ Returns vector

# E: Get multiple elements from list
lst[c("a", "b")]  # ✅ Only this works
#> $a
#> [1] 1
#> 
#> $b
#> [1] 2
# lst$c("a", "b")  # ❌ Syntax error
# lst[[c("a", "b")]]  # ❌ Error

Exercise 2:

# Problem 1 - $ on atomic vector
numbers <- c(x = 10, y = 20, z = 30)
result <- numbers[["x"]]  # or numbers["x"]

# Problem 2 - Can't recurse into atomic vector
data <- list(values = c(1, 2, 3))
item <- data[["values"]][1]  # or data$values[1]

# Problem 3 - Multiple indices in [[]]
my_list <- list(a = 1, b = 2, c = 3)
subset <- my_list[c(1, 3)]  # Use single bracket

# Problem 4 - Path doesn't exist
nested <- list(level1 = list(level2 = 10))
# Check if exists first
if (!is.null(nested$level1$level2)) {
  value <- nested$level1$level2
} else {
  value <- NA
}
# Or just:
value <- nested$level1$level2  # This is 10
# nested$level1$level2$level3 would be NULL

Exercise 3:

safe_get <- function(x, path, default = NULL) {
  # Handle empty path
  if (length(path) == 0) {
    return(x)
  }
  
  # Iterate through path
  current <- x
  for (i in seq_along(path)) {
    step <- path[i]
    
    # Check if current is indexable
    if (!is.list(current) && !is.environment(current)) {
      message("Cannot index into ", class(current)[1], " at step ", i)
      return(default)
    }
    
    # Check if step exists
    if (is.character(step)) {
      if (!step %in% names(current)) {
        message("Name '", step, "' not found at step ", i)
        return(default)
      }
      current <- current[[step]]
    } else if (is.numeric(step)) {
      if (step < 1 || step > length(current)) {
        message("Index ", step, " out of bounds at step ", i)
        return(default)
      }
      current <- current[[step]]
    } else {
      stop("Path element must be character or numeric")
    }
  }
  
  return(current)
}

# Test
nested <- list(a = list(b = list(c = 42)))
safe_get(nested, c("a", "b", "c"))        # 42
#> [1] 42
safe_get(nested, c("a", "b", "c", "d"))   # NULL
#> Cannot index into numeric at step 4
#> NULL
safe_get(nested, c("a", "x"), default = NA)  # NA
#> Name 'x' not found at step 2
#> [1] NA
safe_get(nested, c(1, 1, 1))              # 42 (by index)
#> [1] 42

Exercise 4:

data <- list(
  user = list(
    name = "Alice",
    scores = c(85, 90, 95)
  )
)

# data$user
# Returns: list(name = "Alice", scores = c(85, 90, 95))
# Type: list

# data[["user"]]
# Returns: list(name = "Alice", scores = c(85, 90, 95))
# Type: list

# data["user"]
# Returns: list(user = list(name = "Alice", scores = c(85, 90, 95)))
# Type: list (wrapped in another list!)

# data$user$name
# Returns: "Alice"
# Type: character

# data[[c("user", "name")]]
# Returns: "Alice"
# Type: character

# data[["user"]][["scores"]][[2]]
# Returns: 90
# Type: numeric

# Show them
data$user
#> $name
#> [1] "Alice"
#> 
#> $scores
#> [1] 85 90 95
data[["user"]]
#> $name
#> [1] "Alice"
#> 
#> $scores
#> [1] 85 90 95
data["user"]
#> $user
#> $user$name
#> [1] "Alice"
#> 
#> $user$scores
#> [1] 85 90 95
data$user$name
#> [1] "Alice"
data[[c("user", "name")]]
#> [1] "Alice"
data[["user"]][["scores"]][[2]]
#> [1] 90