Intermediate

Functions in R

Learn how to define and use functions in R, including default arguments, scope, recursion, closures, and the functional apply family with Docker-ready examples

Functions are the heart of R. Almost everything you do in R—from print() to sum() to fitting a regression model—is a function call, and the language treats functions themselves as ordinary values you can store, pass around, and return. This first-class treatment of functions is a direct inheritance from R’s Scheme and Lisp ancestry, and it makes R far more functional in spirit than its data-analysis reputation suggests.

In this tutorial you’ll learn how to define your own functions, supply default and named arguments, understand R’s lexical scoping rules, write recursive functions, and use closures and the apply family to write expressive, vectorized code. Because R is a multi-paradigm language, we’ll emphasize the functional patterns that R programmers reach for in practice rather than forcing imperative loops where a single higher-order function will do.

By the end you’ll be comfortable writing reusable functions and thinking the way experienced R users do: in terms of transformations applied to whole vectors and data structures.

Defining and Calling Functions

In R you create a function with the function keyword and assign it to a name using the standard assignment operator <-. A function automatically returns the value of its last evaluated expression, so an explicit return() is optional.

Create a file named functions_basic.R:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
# A function with an explicit return value
greet <- function(name) {
  message <- paste("Hello,", name)
  return(message)
}

# The last evaluated expression is returned automatically
square <- function(x) {
  x * x
}

print(greet("Ada"))
print(square(9))

# Functions can return multiple values bundled in a list
stats <- function(numbers) {
  list(total = sum(numbers), mean = mean(numbers))
}

result <- stats(c(2, 4, 6, 8))
cat("Total:", result$total, "\n")
cat("Mean:", result$mean, "\n")

Because R has no native tuple type, returning a named list is the idiomatic way to hand back more than one value. The caller then pulls out fields with the $ operator.

Default and Named Arguments

R has a flexible argument-matching system. Parameters can declare default values, callers can pass arguments by name in any order, and the special ... parameter captures a variable number of arguments.

Create a file named functions_args.R:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
# Default parameter values
power <- function(base, exponent = 2) {
  base ^ exponent
}

print(power(5))        # uses the default exponent of 2
print(power(2, 10))    # overrides the default

# Named arguments can be supplied in any order
describe <- function(name, age, city) {
  cat(name, "is", age, "years old and lives in", city, "\n")
}

describe(age = 30, city = "Auckland", name = "Grace")

# The ... (dots) parameter accepts a variable number of arguments
add_all <- function(...) {
  numbers <- c(...)
  sum(numbers)
}

print(add_all(1, 2, 3, 4, 5))

Named arguments make function calls self-documenting, which is why so much of R’s built-in API relies on them. The ... mechanism is how flexible functions like paste(), c(), and cat() accept any number of inputs.

Variable Scope

R uses lexical (static) scoping. Variables assigned inside a function are local to that function and do not leak out. To deliberately modify a variable in an enclosing scope, R provides the “super-assignment” operator <<-.

Create a file named functions_scope.R:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
# Variables created inside a function are local
counter <- 10

increment <- function() {
  counter <- counter + 1   # creates a LOCAL copy, global is untouched
  cat("Inside function:", counter, "\n")
}

increment()
cat("Outside function:", counter, "\n")

# The <<- operator modifies a variable in the enclosing scope
make_deposit <- function(amount) {
  counter <<- counter + amount   # modifies the GLOBAL counter
}

make_deposit(5)
cat("After deposit:", counter, "\n")

Notice that ordinary assignment inside increment() never touches the global counter, while <<- inside make_deposit() does. Reaching for <<- is uncommon in day-to-day analysis code, but it is the foundation of stateful closures, which we’ll see shortly.

Recursion

Functions in R can call themselves, making recursion a natural fit for problems with self-similar structure such as factorials and the Fibonacci sequence.

Create a file named functions_recursion.R:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
# Classic recursive factorial
factorial_r <- function(n) {
  if (n <= 1) {
    return(1)
  }
  n * factorial_r(n - 1)
}

print(factorial_r(5))

# Recursive Fibonacci
fib <- function(n) {
  if (n < 2) return(n)
  fib(n - 1) + fib(n - 2)
}

# Apply fib to a vector of indices with sapply
print(sapply(0:7, fib))

R is not optimized for deep recursion—it has a call-stack limit and no tail-call optimization—so for performance-critical numeric work you’d usually prefer R’s vectorized built-ins (R actually ships a fast factorial() function). Recursion remains valuable for tree-like and divide-and-conquer problems where it expresses the logic most clearly.

Higher-Order Functions and Closures

This is where R’s functional heritage shines. Functions are first-class values: you can pass them as arguments, return them from other functions, and create them anonymously. Instead of writing explicit loops, idiomatic R applies functions over vectors with the apply family and the Filter/Map/Reduce trio.

Create a file named functions_higher_order.R:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# Functions can be passed as arguments
apply_twice <- function(f, x) {
  f(f(x))
}

double <- function(x) x * 2
print(apply_twice(double, 5))   # double(double(5)) = 20

# Anonymous functions with the apply family
nums <- c(1, 2, 3, 4, 5)
squared <- sapply(nums, function(x) x ^ 2)
print(squared)

# R 4.1+ shorthand lambda syntax with \(x)
cubed <- sapply(nums, \(x) x ^ 3)
print(cubed)

# Closures: a function that returns a function
multiplier <- function(factor) {
  function(x) x * factor
}

triple <- multiplier(3)
print(triple(10))

# Filter and Reduce are built-in functional tools
print(Filter(function(x) x %% 2 == 0, nums))
print(Reduce(`+`, nums))

The closure multiplier() “remembers” the factor value from the scope in which it was created, so triple permanently multiplies by 3. The \(x) syntax introduced in R 4.1 is a concise lambda shorthand equivalent to function(x). Reaching for sapply, Filter, and Reduce instead of for loops produces shorter code and aligns with how R evaluates whole vectors at once.

Running with Docker

Run each example using the official R image—no local R installation required.

1
2
3
4
5
6
7
8
9
# Pull the official image
docker pull r-base:4.4.2

# Run each functions example
docker run --rm -v $(pwd):/app -w /app r-base:4.4.2 Rscript functions_basic.R
docker run --rm -v $(pwd):/app -w /app r-base:4.4.2 Rscript functions_args.R
docker run --rm -v $(pwd):/app -w /app r-base:4.4.2 Rscript functions_scope.R
docker run --rm -v $(pwd):/app -w /app r-base:4.4.2 Rscript functions_recursion.R
docker run --rm -v $(pwd):/app -w /app r-base:4.4.2 Rscript functions_higher_order.R

Expected Output

# functions_basic.R
[1] "Hello, Ada"
[1] 81
Total: 20
Mean: 5

# functions_args.R
[1] 25
[1] 1024
Grace is 30 years old and lives in Auckland
[1] 15

# functions_scope.R
Inside function: 11
Outside function: 10
After deposit: 15

# functions_recursion.R
[1] 120
[1]  0  1  1  2  3  5  8 13

# functions_higher_order.R
[1] 20
[1]  1  4  9 16 25
[1]   1   8  27  64 125
[1] 30
[1] 2 4
[1] 15

Key Concepts

  • Functions are values: Defined with function(...) { ... } and assigned with <-, functions are first-class objects you can pass, store, and return.
  • Implicit return: A function returns its last evaluated expression automatically; return() is only needed for early exits.
  • Return multiple values with a list: R has no tuple type, so bundle several outputs in a named list and access them with $.
  • Flexible arguments: Parameters support default values, name-based matching in any order, and the ... catch-all for variadic functions.
  • Lexical scope and <<-: Assignments inside a function are local; the super-assignment operator <<- reaches into the enclosing scope and powers stateful closures.
  • Closures capture their environment: A function returned from another function remembers the variables it was created with.
  • Think functionally, not imperatively: Prefer sapply, Filter, Map, and Reduce over explicit loops—they’re more concise and match R’s vectorized evaluation model.
  • Concise lambdas: R 4.1+ supports the \(x) shorthand as a drop-in replacement for function(x).

Running Today

All examples can be run using Docker:

docker pull r-base:4.4.2
Last updated:

Comments

Loading comments...

Leave a Comment

2000 characters remaining