Skip to content

04 · Functions

Defining and calling a function

greet <- function(name) {
    message <- paste("Hello,", name, "!")
    return(message)
}

greet("Alice")
# [1] "Hello, Alice !"

A function is just a value bound to a name with <-, like anything else in R. function(...) creates the function object; the { } block is its body.

Implicit return

The last evaluated expression in a function body is returned automatically — an explicit return() is optional and mostly used to exit early:

square <- function(x) {
    x * x     # no "return" needed -- this is the last expression
}

square(5)
# [1] 25
safe_divide <- function(a, b) {
    if (b == 0) {
        return(NA)     # early return -- explicit here on purpose
    }
    a / b               # implicit return for the normal case
}

safe_divide(10, 2)
# [1] 5
safe_divide(10, 0)
# [1] NA

Default parameter values

power <- function(base, exponent = 2) {
    base ^ exponent
}

power(5)          # 25 -- uses the default exponent
# [1] 25
power(5, 3)       # 125 -- overrides the default
# [1] 125
power(base = 2, exponent = 10)   # named arguments -- order doesn't matter
# [1] 1024

Named vs. positional arguments

R lets you mix positional and named arguments; named arguments can appear in any order:

describe_person <- function(name, age, city) {
    paste(name, "is", age, "years old and lives in", city)
}

describe_person("Sam", 30, "Boston")               # positional
describe_person(age = 30, city = "Boston", name = "Sam")   # named, any order
describe_person("Sam", city = "Boston", age = 30)   # mixed
# All three produce: "Sam is 30 years old and lives in Boston"

... — variable numbers of arguments

The ... ("dots") parameter collects any number of extra arguments, which you can forward to another function:

summarize_all <- function(...) {
    values <- c(...)
    cat("Sum:", sum(values), " Mean:", mean(values), "\n")
}

summarize_all(1, 2, 3, 4, 5)
# Sum: 15  Mean: 3

... is also how functions like paste() and cat() themselves accept an arbitrary number of arguments, and how wrapper functions forward unknown arguments to an underlying function without listing every one explicitly.

Functions as values

Functions are ordinary values in R — you can store them in variables, pass them as arguments, and return them from other functions:

apply_twice <- function(f, x) {
    f(f(x))
}

add_one <- function(x) x + 1

apply_twice(add_one, 5)
# [1] 7   -- add_one(add_one(5)) = add_one(6) = 7

Anonymous (lambda) functions

# Full anonymous function syntax
sapply(c(1, 2, 3), function(x) x ^ 2)
# [1] 1 4 9

# R 4.1+ shorthand: \(x) is equivalent to function(x)
sapply(c(1, 2, 3), \(x) x ^ 2)
# [1] 1 4 9

sapply() here applies the anonymous function to every element of the vector — a first look at the apply family, covered fully in Level 2.

Scope basics

A function has its own local environment: variables created inside it don't leak out, and by default a function reads (but does not modify) variables from the enclosing scope:

x <- 10

modify <- function() {
    x <- 20        # creates a NEW local x -- does not touch the outer one
    print(x)
}

modify()
# [1] 20
print(x)
# [1] 10   -- unchanged

To deliberately modify a variable in an enclosing scope (rare, and generally avoided in favor of returning a new value), R has the <<- "superassignment" operator:

counter <- 0

increment <- function() {
    counter <<- counter + 1   # modifies the outer counter, not a local copy
}

increment()
increment()
print(counter)
# [1] 2

Prefer return values over <<-

Relying on <<- to mutate outer variables makes code harder to reason about, since a function's effect isn't visible from its return value alone. It's useful to know it exists (you'll see it in some counter/cache patterns), but idiomatic R almost always prefers a function that takes inputs and returns a new value.

Function basics cheat sheet

Concept Syntax
Define f <- function(x, y = 1) { ... }
Call positionally f(10, 20)
Call by name f(y = 20, x = 10)
Variadic args f <- function(...) { c(...) }
Anonymous function function(x) x + 1 or \(x) x + 1
Early exit return(value)
Modify outer scope x <<- new_value (avoid unless needed)

How It Actually Works

Every R function carries its own environment — the environment active where it was defined, not where it's called from (lexical scoping). When you call a function, R creates a brand-new execution environment whose parent is the function's enclosing environment, binds the arguments into it, and evaluates the body there. This is why a function can "see" a variable from where it was written even when called from somewhere else entirely, and why two calls to the same function never share local variables — each call gets a fresh environment.

Arguments in R are lazily evaluated via promises: when you call f(x = long_computation()), R doesn't run long_computation() immediately. It creates a promise object recording the unevaluated expression and the caller's environment, and only forces (evaluates) it the first time x is actually referenced inside f's body — and then caches the result, so it's never computed twice. This is how missing() can detect an unsupplied argument, and how default arguments can reference other arguments (function(x, y = x * 2)) — x * 2 is just another unevaluated promise until y is touched.

🔀 See this in another language

Exercise

Write a function bmi(weight_kg, height_m) that returns the body mass index (weight_kg / height_m^2), rounded to 1 decimal place with round(). Give height_m a default of 1.7. Then write a second function classify_bmi(bmi_value) that returns "Underweight", "Normal", "Overweight", or "Obese" based on standard BMI thresholds (below 18.5, 18.5–24.9, 25–29.9, 30+). Call classify_bmi(bmi(70, 1.75)) and print the result.