R Assignment Deep Dive: <- vs = vs <<- vs -> -- Know the Difference
R has five assignment operators: <-, =, <<-, ->, and ->>, plus the assign() function. Each has different scoping behavior and appropriate use cases. This tutorial explains exactly when to use each one and why <- is the standard.
Assignment is something you do hundreds of times in every R script. Understanding the subtle differences between these operators prevents hard-to-find bugs, especially with nested functions and closures.
The Standard: <- (Left Assignment)
<- is R's standard assignment operator. The R community, Google's R style guide, and the tidyverse style guide all recommend it.
# Standard assignment
x <- 42
name <- "Alice"
data <- mtcars[1:5, 1:3]
cat("x:", x, "\n")
cat("name:", name, "\n")
cat("data rows:", nrow(data), "\n")
# Works anywhere: top level, inside functions, inside loops
my_func <- function() {
local_var <- "I exist only inside this function"
cat(local_var, "\n")
}
my_func()
Why <- and not =?
Historical reason: <- has been R's assignment operator since the 1970s (inherited from S). But there's also a practical reason:
# <- is unambiguous
x <- 3 # Always assignment
# = can be ambiguous in function calls
# Is this assignment or a named argument?
mean(x = 1:10) # Named argument, NOT assignment
cat("x still:", x, "\n") # x is still 3!
# With <-, it's always assignment
mean(x <- 1:10) # Assigns 1:10 to x AND passes it to mean
cat("Now x is:", x[1], "...", x[10], "\n")
# This is why style guides prefer <- for assignment
The Equals Sign: =
= works as assignment at the top level and inside { } blocks, but it does NOT work for assignment inside function call arguments.
# These are equivalent at the top level
a <- 10
b = 20
cat("a:", a, "\n")
cat("b:", b, "\n")
# Inside a function body, both work
my_func <- function() {
local1 <- 100
local2 = 200
cat("local1:", local1, "\n")
cat("local2:", local2, "\n")
}
my_func()
Where = does NOT assign
# Inside a function call, = is for naming arguments, not assignment
result <- data.frame(x = 1:3, y = 4:6)
cat("Does 'x' exist in global scope?", exists("x"), "\n")
# x = 1:3 created a column, not a global variable
# Contrast with <-
result2 <- data.frame(a <- 1:3, b <- 4:6)
cat("Does 'a' exist in global scope?", exists("a"), "\n")
cat("a:", a, "\n") # a was created as a side effect!
# This is usually a bug, not intentional
Rule of thumb: Use <- for assignment, = for function arguments. This removes all ambiguity.
The Superassignment: <<-
<<- assigns to the parent environment, not the current one. It's used to modify variables in enclosing scopes (closures).
# <<- modifies a variable in the parent/global environment
counter <- 0
increment <- function() {
counter <<- counter + 1 # Modifies the global 'counter'
}
cat("Before:", counter, "\n")
increment()
increment()
increment()
cat("After 3 increments:", counter, "\n")
How <<- searches for the variable
<<- walks up the chain of parent environments looking for the variable. If it doesn't find it anywhere, it creates it in the global environment.
# <<- searches parent environments
outer <- function() {
x <- 10
cat("outer: x =", x, "\n")
inner <- function() {
cat("inner before: x =", x, "\n")
x <<- 99 # Finds x in outer's environment
cat("inner after: x =", x, "\n")
}
inner()
cat("outer after inner(): x =", x, "\n") # Changed!
}
outer()
# <<- can accidentally modify global variables
x <- "important data"
cat("Before:", x, "\n")
oops <- function() {
# Typo or careless use of <<-
x <<- "overwritten!"
}
oops()
cat("After:", x, "\n") # Surprise!
# Restore
x <- "important data"
Use <<- only in closures where you intentionally need to modify a variable in an enclosing scope. Never use it as a general assignment operator.
Right Assignment: -> and ->>
-> and ->> are the mirror images of <- and <<-. They assign left-to-right.
# Right assignment: value -> name
42 -> answer
"hello" -> greeting
cat("answer:", answer, "\n")
cat("greeting:", greeting, "\n")
# Useful at the end of a pipe chain
mtcars |>
subset(cyl == 4) |>
nrow() -> four_cyl_count
cat("4-cylinder cars:", four_cyl_count, "\n")
->> is right superassignment (modifies parent scope), but it's extremely rare in practice.
# ->> is right superassignment (rare)
outer_val <- 0
f <- function() {
100 ->> outer_val
}
f()
cat("outer_val:", outer_val, "\n")
Style note: Most R programmers avoid -> entirely. The tidyverse pipe style prefers result <- data |> transform() over data |> transform() -> result.
The assign() Function
assign() is the functional form of assignment. It's useful when the variable name is stored in a string.
# assign() when the variable name is dynamic
var_name <- "my_variable"
assign(var_name, 42)
cat("my_variable:", my_variable, "\n")
# Create multiple variables in a loop
for (i in 1:3) {
assign(paste0("var_", i), i * 10)
}
cat("var_1:", var_1, "\n")
cat("var_2:", var_2, "\n")
cat("var_3:", var_3, "\n")
# assign() can target specific environments
my_env <- new.env(parent = emptyenv())
assign("secret", 99, envir = my_env)
cat("In my_env:", get("secret", envir = my_env), "\n")
get() is the complement of assign()
# get() retrieves a variable by name
x <- 100
var_name <- "x"
cat("Using get():", get(var_name), "\n")
# Useful for dynamic variable access
cols <- c("mpg", "cyl", "wt")
for (col in cols) {
vals <- mtcars[[col]]
cat(col, "mean:", round(mean(vals), 2), "\n")
}
Warning: Using assign() in loops to create var_1, var_2, etc. is generally bad practice. Use a list instead: results <- list() and results[[i]] <- value.
Exercise 1: What does this print? f <- function() { x <- 1; g <- function() { x <<- 2 }; g(); cat(x) }; f()
Click to reveal solution
It prints **2**. Inside `g()`, `x <<- 2` searches the parent environment (which is `f()`'s environment), finds `x` there, and modifies it to 2. When `f()` then prints `x`, it sees the modified value.
```r
f <- function() {
x <- 1
g <- function() {
x <<- 2 # Modifies x in f()'s environment
}
g()
cat("x:", x, "\n")
}
f()
Exercise 2: Create a make_accumulator(start) function that returns a function. Each call to the returned function should add its argument to the running total and return the new total. Use <<-.
Click to reveal solution
```r
make_accumulator <- function(start = 0) {
total <- start
function(x) {
total <<- total + x
total
}
}
acc <- make_accumulator(100)
cat(acc(10), "\n") # 110
cat(acc(20), "\n") # 130
cat(acc(-5), "\n") # 125
Exercise 3: Why does data.frame(x = 1:3) not create a variable x in your global environment, but data.frame(x <- 1:3) does?
Click to reveal solution
Inside a function call:
- `x = 1:3` is interpreted as a **named argument** -- it names the column "x" but does not assign to the variable `x` in the calling environment.
- `x <- 1:3` is interpreted as an **assignment expression** that evaluates to `1:3`. The assignment creates `x` in the global environment as a side effect, and the resulting value `1:3` is passed to `data.frame()` (but without the name "x").
```r
rm(list = "x", envir = globalenv()) # Clean up
df1 <- data.frame(x = 1:3)
cat("After x = 1:3, 'x' exists?", exists("x"), "\n") # FALSE
df2 <- data.frame(x <- 1:3)
cat("After x <- 1:3, 'x' exists?", exists("x"), "\n") # TRUE
cat("x:", x, "\n")
# Notice the column names differ too
cat("df1 colnames:", names(df1), "\n") # "x"
cat("df2 colnames:", names(df2), "\n") # Something like "x....1.3."
FAQ
Q: Should I ever use = for assignment instead of <-? In top-level scripts, = works identically to <-. Some programmers from other languages prefer it. However, the overwhelming R convention is <-. Mixing both in a codebase creates inconsistency. Stick with <-.
Q: Is <<- ever okay to use? Yes, in closures (factory functions that return functions). The make_counter() and make_accumulator() patterns are legitimate uses. Avoid it everywhere else.
Q: Does the space around <- matter? Yes! x<-3 is ambiguous -- is it x <- 3 or x < -3? Always add spaces: x <- 3. R parses x<-3 as assignment, but x< -3 is a comparison. Don't risk it.