R Functions — Reusable Code Building Blocks
Learning Objectives
By the end of this tutorial, you will be able to:
- Define functions with single and multiple arguments
- Use default values, variable arguments, and named arguments
- Create anonymous and recursive functions
- Apply functional programming concepts with
Map(),Reduce(), andFilter() - Write clean, well-structured functions
Creating Functions
Basic Syntax
# Simple function
greet <- function(name) {
paste("Hello,", name, "!")
}
greet("Alice")
# [1] "Hello, Alice !"
# Single expression (implicit return)
square <- function(x) x^2
square(5) # [1] 25
Multiple Arguments
# Multiple arguments
add <- function(a, b) {
a + b
}
add(3, 5) # [1] 8
# Named arguments
add(a = 3, b = 5) # [1] 8
add(b = 5, a = 3) # [1] 8 (order doesn't matter)
Return Values
# Explicit return
divide <- function(a, b) {
if (b == 0) {
return("Cannot divide by zero")
}
a / b
}
divide(10, 3) # [1] 3.333333
divide(10, 0) # [1] "Cannot divide by zero"
# Multiple return values (as list)
get_stats <- function(x) {
list(
mean = mean(x),
sd = sd(x),
n = length(x)
)
}
stats <- get_stats(1:10)
stats$mean # [1] 5.5
stats$sd # [1] 3.02765
Default Arguments
# Default values
greet <- function(name, greeting = "Hello") {
paste(greeting, name, "!")
}
greet("Alice") # [1] "Hello Alice !"
greet("Alice", "Hi") # [1] "Hi Alice !"
greet("Alice", "Good morning") # [1] "Good morning Alice !"
# Multiple defaults
power <- function(x, base = 2, times = 1) {
(x^base) * times
}
power(3) # [1] 9
power(3, base = 3) # [1] 27
power(3, times = 5) # [1] 45
Variable Arguments
... (Ellipsis)
# Pass arguments to another function
summary_stats <- function(x, ...) {
list(
mean = mean(x, ...),
sd = sd(x, ...)
)
}
summary_stats(c(1, 2, NA, 4, 5))
# $mean: NA (because of NA)
summary_stats(c(1, 2, NA, 4, 5), na.rm = TRUE)
# $mean: 3
# $sd: 1.732051
# Flexible arguments
add_all <- function(...) {
sum(...)
}
add_all(1, 2, 3, 4, 5) # [1] 15
# Capture arguments
log_args <- function(...) {
args <- list(...)
cat("Arguments received:", paste(names(args), collapse = ", "), "\n")
}
log_args(x = 1, y = 2, z = 3)
# Arguments received: x y z
Argument Matching
R matches arguments in three ways:
# 1. Exact name matching
f <- function(x, y, z) {
list(x = x, y = y, z = z)
}
f(x = 1, y = 2, z = 3)
# 2. Partial name matching (unique prefix)
f(x = 1, y = 2, z = 3)
f(x = 1, y = 2, z = 3)
# 3. Positional matching (after named args)
f(1, 2, 3)
f(z = 3, x = 1, 2) # y matched by position
# Best practice: always use full names
f(x = 1, y = 2, z = 3)
Scope and Environments
# Global vs local
x <- 10
my_func <- function() {
x <- 20 # Local x
cat("Inside:", x, "\n")
}
my_func() # Inside: 20
cat("Outside:", x, "\n") # Outside: 10
# Lexical scoping — functions see their parent environment
outer <- function() {
x <- "outer"
inner <- function() {
cat(x, "\n") # Can see outer's x
}
inner()
}
outer() # outer
# Global assignment
x <- 10
modify <- function() {
x <<- 20 # Modifies global x
}
modify()
x # [1] 20
Anonymous Functions
# Lambda functions (no name)
function(x) x^2
# Assign to variable
square <- function(x) x^2
square(5) # [1] 25
# Use inline with apply family
sapply(1:5, function(x) x^2)
# [1] 1 4 9 16 25
# Short form (R 4.1+)
sapply(1:5, \(x) x^2)
# [1] 1 4 9 16 25
# In pipes
1:5 |>
sapply(\(x) x^2) |>
sum()
# [1] 55
Higher-Order Functions
lapply, sapply, vapply
# lapply — always returns list
lapply(1:5, function(x) x^2)
# sapply — simplified (vector when possible)
sapply(1:5, function(x) x^2)
# [1] 1 4 9 16 25
# vapply — type-safe sapply
vapply(1:5, function(x) x^2, numeric(1))
# [1] 1 4 9 16 25
Map, Reduce, Filter
# Map — apply function to multiple vectors
Map("+", c(1, 2, 3), c(10, 20, 30))
# [[1]]
# [1] 11
# [[2]]
# [1] 22
# [[3]]
# [1] 33
# Reduce — fold/accumulate
Reduce("+", c(1, 2, 3, 4, 5)) # [1] 15
# Filter
Filter(function(x) x > 3, c(1, 2, 3, 4, 5))
# [1] 4 5
# Find
Find(function(x) x > 3, c(1, 2, 3, 4, 5))
# [1] 4
# Position
Position(function(x) x > 3, c(1, 2, 3, 4, 5))
# [1] 4
Purrr (tidyverse)
library(purrr)
# map — like lapply
map(1:5, \(x) x^2)
# map_dbl — like vapply for doubles
map_dbl(1:5, \(x) x^2)
# [1] 1 4 9 16 25
# map_chr, map_lgl, map_int — type-safe variants
map_chr(1:5, \(x) paste("item", x))
# map2 — two inputs
map2(c(1, 2, 3), c(10, 20, 30), \(x, y) x + y)
# pmap — multiple inputs
pmap(list(c(1, 2, 3), c(10, 20, 30), c(100, 200, 300)), \(x, y, z) x + y + z)
# keep and discard
keep(1:10, \(x) x > 5) # [1] 6 7 8 9 10
discard(1:10, \(x) x > 5) # [1] 1 2 3 4 5
Recursive Functions
# Factorial
factorial_r <- function(n) {
if (n <= 1) return(1)
n * factorial_r(n - 1)
}
factorial_r(5) # [1] 120
# Fibonacci
fib <- function(n) {
if (n <= 1) return(n)
fib(n - 1) + fib(n - 2)
}
sapply(0:10, fib)
# [1] 0 1 1 2 3 5 8 13 21 34 55
# Binary search
binary_search <- function(arr, target, low = 1, high = length(arr)) {
if (low > high) return(-1)
mid <- (low + high) %/% 2
if (arr[mid] == target) return(mid)
if (arr[mid] < target) {
binary_search(arr, target, mid + 1, high)
} else {
binary_search(arr, target, low, mid - 1)
}
}
binary_search(1:100, 42) # [1] 42
Practice Exercises
Exercise 1: BMI Calculator
Write a function that calculates BMI from weight (kg) and height (cm), and returns both the BMI value and the category.
Solution
bmi_calculator <- function(weight_kg, height_cm) {
height_m <- height_cm / 100
bmi <- weight_kg / (height_m ^ 2)
category <- case_when(
bmi < 18.5 ~ "Underweight",
bmi < 25 ~ "Normal weight",
bmi < 30 ~ "Overweight",
TRUE ~ "Obese"
)
list(
bmi = round(bmi, 1),
category = category
)
}
bmi_calculator(70, 175)
# $bmi: 22.9
# $category: "Normal weight"
Exercise 2: Custom Summary
Write a function that takes a numeric vector and returns a list with mean, median, sd, min, max, and IQR.
Solution
custom_summary <- function(x) {
list(
mean = mean(x, na.rm = TRUE),
median = median(x, na.rm = TRUE),
sd = sd(x, na.rm = TRUE),
min = min(x, na.rm = TRUE),
max = max(x, na.rm = TRUE),
iqr = IQR(x, na.rm = TRUE)
)
}
custom_summary(1:100)
Key Takeaways
- Functions are first-class objects in R — can be assigned, passed, and returned
- Use default arguments for flexibility
...passes extra arguments to inner functions- Anonymous functions are useful with
applyfamily - Lexical scoping — functions see their parent environment
- Use
lapply()/sapply()instead of loops for cleaner code purrrprovides type-safe map functions- Recursive functions are elegant for tree-like problems
Next: Learn about R Apply Family — the powerful apply functions.