Functions

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

Checked against R 4.6.1 (2026-06-24) on 2026-10-20; R 4.6.2 is scheduled for 2026-10-29. Everything on this page is base R (the base, utils and stats packages that ship with R), so no add-on package version applies; the lintr 3.4.0 default linters were used only to check the scripts. In R almost everything you do is a function call, and the functions you write yourself are ordinary objects that differ from the ones that ship with R only in who wrote them. This page is about writing them and calling them well: what a function is made of, how a call is matched to the arguments the function declares, how the dots, defaults, missing() and match.arg() work, what a function returns and when it prints, how to clean up after it, and the two ways of defining your own syntax, replacement functions such as names(x) <- value and infix operators such as %+%.

Every example is a script that was run with Rscript on R 4.6.1 with warnings printed as they happen (options(warn = 1)), and the lines that start with #> are what it printed. The scripts also pass the default lintr linters; a # nolint: comment marks a line where the idiom on show is what a default linter dislikes, so the linter does not hide the lesson. The names books, loans, shelf_today, loan_days and the book() constructor come from the running library-shelf scenario described on the section index: a script that needs them starts with source("scenario.R"). Where a message contains quotation marks, they are the typographic quotes R prints in a UTF-8 locale. The lazy-evaluation details of arguments (promises, force(), default arguments that depend on one another) are the subject of Lazy Evaluation and Promises; scoping rules are in Scoping, Environments and Closures.

What a function is in R

R is a functional language in the sense that functions are values: they can be stored in variables and lists, passed to other functions and returned from them. The manual describes the objects as function closures, a combination of the code of the function and the bindings of the environment it was created in (R-lang: Function objects). An Introduction to R makes the practical point first: most of the functions supplied with R, such as mean() and var(), are themselves written in R and do not differ materially from the ones you write (R-intro: Writing your own functions). That is why typing a function’s name without parentheses usually shows its source, which is the answer to R FAQ 7.40: sometimes the code is hidden in a namespace or compiled.

Defining a function

The syntax is function(arglist) body. The argument list is a comma-separated list of names, of name = default pairs, and possibly the special ...; the body is any expression, usually a block in braces. The value of the function expression is the function, so it can be assigned to a name, but it does not have to be: an unnamed function is an anonymous function, most often seen as the argument of vapply() or Map(). Since R 4.1.0 a shorter spelling is available: \(x) x + 1 is parsed as function(x) x + 1. The 4.1.0 NEWS entry says exactly that, and the ?function help page repeats it. The two forms are not two kinds of function; the parser produces the same call, and identical() confirms that the resulting objects are equal.

source("scenario.R")

# A function is an object with three components.
due_date <- function(loan_date,
                     days = loan_days) { # nolint: object_usage_linter.
  loan_date + days
}
due_date(as.Date("2026-06-01"))
#> [1] "2026-06-22"

names(formals(due_date))
#> [1] "loan_date" "days"
formals(due_date)$days
#> loan_days
body(due_date)
#> {
#>     loan_date + days
#> }
environment(due_date)
#> <environment: R_GlobalEnv>

# The backslash form is the same function, parsed from different text.
add_one <- function(x) x + 1
add_one_short <- \(x) x + 1
identical(add_one, add_one_short)
#> [1] TRUE
(\(x) x + 1)(41)
#> [1] 42

# Rscript does not keep source references (R in a terminal or an IDE does), so a
# function prints as its deparsed code.
getOption("keep.source")
#> [1] FALSE
deparse(add_one)
#> [1] "function (x) " "x + 1"

# Functions have a type, a class and a mode.
typeof(due_date)
#> [1] "closure"
class(due_date)
#> [1] "function"
is.function(due_date)
#> [1] TRUE
is.function("due_date")
#> [1] FALSE

Two details of the output are worth a look. body(due_date) is a call to {, because braces are also a function; and formals(due_date)$days is the unevaluated symbol loan_days, not its value, since a default is an expression that runs when the argument is needed. An interactive R session keeps the source text of functions you type (options(keep.source = TRUE)), so printing a function there shows your own layout and comments. Rscript does not, which is why deparse() above gives the standard layout of the deparser.

The three components

A closure has three parts, which can be read and replaced with matching functions (formals, body and environment).

Part Read with Replace with What it is

formals

formals(f)

formals(f) <- value

A pairlist with one element per formal argument. The names are the argument names and the values are the defaults; an argument without a default has the empty symbol.

body

body(f)

body(f) <- value

A language object: a call (usually to {), a symbol or a constant.

environment

environment(f)

environment(f) <- value

The environment that was active when the function was created. When the function is called, a new evaluation frame is created whose enclosure is this environment.

The environment is what makes a closure a closure: the free variables of the body, those that are neither arguments nor local variables, are looked up in it. A function defined at the console has the global environment as its environment, and so does a function sourced from a script; a function defined inside another function has that function’s evaluation frame, and a function in a package has the package namespace. That is the "lexical scoping" that R FAQ 3.3.1 lists as the main difference from S, and R FAQ 7.12 separates the enclosing environment (fixed when the function is created, environment(f)) from the parent environment (where it is called from, parent.frame()). The assignment forms of the three accessors change a copy of the function, as every R assignment does, and as.function() builds a closure from a list of formals followed by a body.

# formals<-, body<- and environment<- change a copy of the function.
price <- function(net, vat = 0.21) net * (1 + vat)
price(100)
#> [1] 121
formals(price)$vat <- 0.1
price(100)
#> [1] 110
body(price) <- quote(round(net * (1 + vat), 1))
price(100.37)
#> [1] 110.4
environment(price) <- new.env()
class(environment(price))
#> [1] "environment"

# as.function() builds a closure from an alist() of formals followed by a body.
add <- as.function(alist(a = , b = 2, a + b))
add(1)
#> [1] 3

Primitives

Not every function is a closure. Functions such as sum, +, [], if and length are primitives, implemented in C and displayed as .Primitive("name"). They come in two kinds: a builtin evaluates its arguments before the call, a special receives them unevaluated and decides itself (if, quote, on.exit, function). typeof() tells them apart from a closure, is.primitive() is a yes or no test, and formals() and body() return NULL for a primitive because it has neither. args() is the supported way to see the argument list of any function: it returns a closure with the same formals and a NULL body, which also works for primitives that have a documented interface.

Primitives also match arguments differently. The manual states that the three-pass rule of the next section applies to closures, not to primitives, which typically ignore the names and match by position; their help pages list the exceptions, among them log, round, signif, rep and seq.int (R-lang: Argument matching). Printing a primitive therefore shows only its interface, never its code. is.primitive() is documented together with is.function() (R documentation: is.function).

# Primitives are not closures: they have no formals and no body.
is.primitive(sum)
#> [1] TRUE
is.primitive(mean)
#> [1] FALSE
formals(sum)
#> NULL
body(sum)
#> NULL
sum
#> function (..., na.rm = FALSE)  .Primitive("sum")
typeof(sum)
#> [1] "builtin"
typeof(`if`)
#> [1] "special"
typeof(mean)
#> [1] "closure"

# args() returns a closure with the right formals and a NULL body.
args(sum)
#> function (..., na.rm = FALSE)
#> NULL
formals(args(sum))$na.rm
#> [1] FALSE
args(`+`)
#> function (e1, e2)
#> NULL
names(formals(paste))
#> [1] "..."      "sep"      "collapse" "recycle0"

Functions are values

Because a function is an object, a function can take another function as an argument. match.fun() accepts either a function or the name of one, which is how many base functions (Map, Reduce, sapply) let you write "sum" or sum. This page only needs the idea; the functional-programming pages build on it: The apply Family and Functionals for Map, Filter, Reduce and friends (all documented together on the funprog help page), and Function Factories and Higher-Order Functions for functions that return functions.

source("scenario.R")

# Functions are values: pass them, return them, store them.
summarise_pages <- function(pages, how = mean) {
  how <- match.fun(how)
  how(pages, na.rm = TRUE)
}
summarise_pages(books$pages)
#> [1] 268.2143
summarise_pages(books$pages, "median")
#> [1] 268
spread <- \(p, ...) max(p, ...) - min(p, ...)
summarise_pages(books$pages, spread)
#> [1] 344
tryCatch(match.fun("no_such_function"), error = conditionMessage)
#> [1] "object 'no_such_function' of mode 'function' was not found"
# The function receives na.rm = TRUE, so it must have a place for it.
tryCatch(
  summarise_pages(books$pages, \(p) max(p) - min(p)),
  error = conditionMessage
)
#> [1] "unused argument (na.rm = TRUE)"

checks <- list(
  has_isbn = \(b) nzchar(b$isbn),
  recent = \(b) !is.na(b$year) && b$year >= 2000L
)
b <- book("Orchard Logic", "Iris Fenwick", "9780306406157", 2019L)
vapply(checks, \(check) check(b), logical(1L))
#> has_isbn   recent
#>     TRUE     TRUE

# A function can return a function (see Function Factories).
make_late_fee <- function(rate) {
  \(days_late) pmax(days_late, 0) * rate
}
late_fee <- make_late_fee(0.25)
late_fee(c(-3, 0, 4, 10))
#> [1] 0.0 0.0 1.0 2.5

The failure in the middle is the call how(pages, na.rm = TRUE) meeting a function that does not declare na.rm; the next section explains why that is an error and how a ... formal makes a function accept it.

Calling a function: argument matching

The first thing that happens when a closure is called is that the arguments in the call, the supplied or actual arguments, are matched to the formal arguments in the definition. The rules are the same for every closure, they are applied before any argument is evaluated, and most of the error messages that beginners meet ("unused argument", "argument is missing") come out of this step. The manual describes a three-pass process (R-lang: Argument matching):

  1. Exact matching on names. For each named supplied argument, the formal with exactly that name is chosen. It is an error if the same formal is matched by several actual arguments.

  2. Partial matching on names. Each remaining named supplied argument is compared to the remaining formals, and if its name is the first part of a formal’s name, they match. It is an error if one supplied name is the first part of more than one formal. If the formals contain ..., partial matching is applied only to the formals that come before it.

  3. Positional matching. Any formal that is still unmatched is bound, in order, to the unnamed supplied arguments. If there is a ..., it takes all the remaining arguments, named or not.

If anything is left over, the call fails with an "unused argument" error. The diagram follows one call through the passes; its three error boxes are the first three rows of the table of messages further down.

flowchart TD A(["Call f(...) with supplied arguments"]) --> B["Pass 1 exact: each named argument is matched to the formal with the same name"] B --> B1{"Does one formal get more than one actual argument?"} B1 -->|"yes"| E1(["Error 1: formal argument matched by multiple actual arguments"]) B1 -->|"no"| C["Pass 2 partial: each remaining named argument is matched to a remaining formal that starts with that name. Formals after ... are skipped"] C --> C1{"Does one supplied name start more than one formal?"} C1 -->|"yes"| E2(["Error 2: argument matches multiple formal arguments"]) C1 -->|"no"| D["Pass 3 positional: unnamed arguments fill the remaining formals before ... in order"] D --> D1{"Is there a ... formal?"} D1 -->|"yes"| F["Everything still unmatched, named or not, goes into ..."] D1 -->|"no"| G{"Is any supplied argument left over?"} G -->|"yes"| E3(["Error 3: unused argument"]) G -->|"no"| H F --> H["Each matched formal becomes a promise: the supplied expression plus the caller's environment"] H --> I["A formal without an argument keeps its default expression, evaluated later in the function's own frame, or is missing"] I --> J(["The body runs. Using a missing argument with no default fails: argument is missing, with no default"])

The three passes in practice

The manual’s own example is a function with the formals fumble and fooey. The first four calls below end in the same match, by three different routes. The fifth is the illegal one the manual points out: the name f is a prefix of both formals, so partial matching cannot decide which one is meant, even though fo could only be fooey. Giving the second name in full removes fooey from the partial pass, and then f has only one candidate.

# R-lang example: f(fumble, fooey)
f <- function(fumble, fooey) c(fumble = fumble, fooey = fooey)

# Pass 1: exact names. Pass 2: partial names. Pass 3: position.
f(fumble = 1, fooey = 2)
#> fumble  fooey
#>      1      2
f(fu = 1, fo = 2)
#> fumble  fooey
#>      1      2
f(2, fumble = 1)
#> fumble  fooey
#>      1      2
f(fooey = 2, 1)
#> fumble  fooey
#>      1      2

# The manual's illegal example: "f" is a prefix of both formals, so partial
# matching cannot decide, even though "fo" could only be fooey.
tryCatch(f(f = 1, fo = 2), error = conditionMessage)
#> [1] "argument 1 matches multiple formal arguments"
# Giving the second name in full removes fooey from partial matching.
f(f = 1, fooey = 2)
#> fumble  fooey
#>      1      2

# Two actuals for one formal.
tryCatch(f(fumble = 1, fumble = 2), error = conditionMessage)
#> [1] "formal argument \"fumble\" matched by multiple actual arguments"
tryCatch(f(fu = 1, fum = 2), error = conditionMessage)
#> [1] "formal argument \"fumble\" matched by multiple actual arguments"

Partial matching is convenient in an interactive session and a risk in code. A name that is unique today can become ambiguous when the called function gains a new argument, and a misspelling can silently match something else. That is the reason for the common advice to write argument names in full everywhere except for the first one or two arguments of a call, and the reason for the option warnPartialMatchArgs, which makes R warn each time partial matching is used. It is off by default and is documented with the other warnPartialMatch* options, which cover $ and attr().

f <- function(alpha, beta) c(alpha, beta)

# Partial matching is silent by default...
getOption("warnPartialMatchArgs")
#> [1] FALSE
f(al = 1, be = 2)
#> [1] 1 2

# ...but can be made to warn while you tidy up a code base.
old <- options(warnPartialMatchArgs = TRUE)
tryCatch(f(al = 1, 2), warning = conditionMessage)
#> [1] "partial argument match of 'al' to 'alpha'"
options(old)
getOption("warnPartialMatchArgs")
#> [1] FALSE

The dots cut partial matching

The rule about the dots matters more than it looks: a formal after ... can only be matched by its exact name, and cannot be matched by position at all, because every positional argument is taken by the dots first. This is by design, since after the dots the function cannot know how many arguments are coming. In the first function below an abbreviated verb does not reach verbose; it is absorbed by the dots, where it stays as a named element, and verbose keeps its default. With the formal before the dots the same abbreviation works. The join() function shows the positional side of the rule: the trailing "-" is one more element of the dots, not the separator.

# Formals before ... are matched partially; formals after ... only exactly.
count <- function(x, ..., verbose = FALSE) {
  list(x = x, dots = names(list(...)), verbose = verbose)
}
str(count(1, verbose = TRUE))
#> List of 3
#>  $ x      : num 1
#>  $ dots   : NULL
#>  $ verbose: logi TRUE
str(count(1, verb = TRUE))
#> List of 3
#>  $ x      : num 1
#>  $ dots   : chr "verb"
#>  $ verbose: logi FALSE

# The same abbreviation works when the formal comes before the dots.
count2 <- function(x, verbose = FALSE, ...) {
  list(x = x, dots = names(list(...)), verbose = verbose)
}
str(count2(1, verb = TRUE))
#> List of 3
#>  $ x      : num 1
#>  $ dots   : NULL
#>  $ verbose: logi TRUE

# Positional matching never reaches a formal after ...: it must be named.
join <- function(..., sep = " ") paste(..., sep = sep)
join("a", "b", "-")
#> [1] "a b -"
join("a", "b", sep = "-")
#> [1] "a-b"

Functions that take dots therefore put their optional arguments after them on purpose, to force callers to name them. Base R does this widely (paste(..., sep = " "), sum(..., na.rm = FALSE)), and a function you write for others should do the same whenever a positional call would be ambiguous.

What goes wrong, in R’s own words

A mismatch is reported as an error before the body runs. The messages below are the real ones, with the call that produced each. An unused argument is an error, not a warning, and the message names the argument as it was written in the call, with its name if it had one. Each error carries the call as conditionCall(e), which is what R prints after the word Error in.

f <- function(alpha, beta) c(alpha, beta)

# Real error texts of the matching step.
tryCatch(f(1, 2, 3), error = conditionMessage)
#> [1] "unused argument (3)"
tryCatch(f(1, 2, gamma = 3), error = conditionMessage)
#> [1] "unused argument (gamma = 3)"
tryCatch(f(1, alpha = 2, alpha = 3), error = conditionMessage)
#> [1] "formal argument \"alpha\" matched by multiple actual arguments"
tryCatch(f(), error = conditionMessage)
#> [1] "argument \"alpha\" is missing, with no default"
tryCatch(f(1), error = conditionMessage)
#> [1] "argument \"beta\" is missing, with no default"
tryCatch(f(al = 1, al = 2), error = conditionMessage)
#> [1] "formal argument \"alpha\" matched by multiple actual arguments"

# A missing argument only fails when it is used.
g <- function(alpha, beta) alpha
g(1)
#> [1] 1

# The condition carries the call that failed.
tryCatch(f(1, 2, 3), error = \(e) deparse(conditionCall(e)))
#> [1] "f(1, 2, 3)"

The last pair shows that a missing argument is a different failure from an unused one. Matching leaves beta unbound, and nothing complains until the body touches it: g(1) never uses beta, so it works. The error appears when beta is evaluated, which is the laziness that the Lazy Evaluation and Promises page describes.

Message Cause Fix

unused argument (3) or unused argument (gamma = 3)

A supplied argument matched no formal and there is no ... to take it.

Remove it, correct the name, or add ... to the function if forwarding is intended.

formal argument "alpha" matched by multiple actual arguments

Two supplied names match the same formal in pass 1 or 2, or a name and an abbreviation do.

Name each argument once.

argument 1 matches multiple formal arguments

A supplied name is a prefix of two or more remaining formals (pass 2).

Write the full name.

argument "beta" is missing, with no default

A formal with no default was evaluated but no argument was supplied for it.

Supply it, give it a default, or test missing(beta) first.

Defaults, missing() and match.arg()

Default arguments

A formal written name = expression has a default. The expression is not evaluated when the function is called; it is evaluated if and when the argument is needed, in the evaluation frame of the function, so it can use the other arguments and any local variable that exists by then (R-lang: Arguments). An argument that the caller supplies is different: it is evaluated in the caller’s frame, as a promise (R-lang: Argument evaluation). An Introduction to R notes that defaults can be arbitrary expressions, even involving other arguments (R-intro: Named arguments and defaults).

source("scenario.R")

# A default is an expression evaluated in the function's own frame, when needed.
loan_window <- function(start,
                        days = loan_days, # nolint: object_usage_linter.
                        end = start + days) {
  c(start = start, end = end)
}
as.Date(loan_window(as.Date("2026-06-01")))
#>        start          end
#> "2026-06-01" "2026-06-22"

# It may use another argument and a local variable.
span <- function(x, n = length(x)) {
  x <- c(x, 99)
  n
}
span(1:3)
#> [1] 4

# An argument given by the caller is evaluated in the caller's frame instead.
days <- 7L
loan_window(as.Date("2026-06-01"), end = as.Date("2026-06-01") + days)
#>        start          end
#> "2026-06-01" "2026-06-08"

# Evaluation is delayed until the value is needed.
lazy <- function(a, b) a
lazy(1, stop("never evaluated"))
#> [1] 1
needs_b <- function(a, b) b
tryCatch(needs_b(1, stop("evaluated now")), error = conditionMessage)
#> [1] "evaluated now"

# A default is evaluated at first use, so a change to the argument is seen.
late <- function(a, b = a) {
  a <- 10
  b
}
late(1)
#> [1] 10

Three things in this output deserve a pause. span(1:3) returns 4, not 3: the default length(x) runs after the body has changed x, because it runs at first use. The days <- 7L in the global environment is not what the days default of loan_window() sees; the call that supplies end evaluates days in the caller’s frame, where it is 7, while the default of end would have used the argument days of the function, 21. And late(1) returns 10: a default that refers to another argument sees that argument’s current value, not the one it had at the start. Code that depends on this order is fragile, and the usual repair is to compute the dependent value before changing its source or to call force() early, both covered on the lazy-evaluation page.

missing()

missing(x) is TRUE when the formal x was not matched with any supplied argument and has not been assigned to in the body. It does not evaluate the argument. An argument that has a default is still "missing" when the caller did not supply it, which makes missing() the way to tell "the caller chose this value" from "R filled in the default". It follows a wrapper: if outer_fn(x) calls inner(x) and x was missing in outer_fn, then x is missing in inner too. An empty argument in the call, as in pick(, 2), counts as missing as well. nargs() counts the supplied arguments, empty ones included.

describe <- function(title, author) {
  if (missing(author)) {
    return(paste(title, "(author unknown)"))
  }
  paste0(title, " by ", author)
}
describe("Orchard Logic")
#> [1] "Orchard Logic (author unknown)"
describe("Orchard Logic", "Iris Fenwick")
#> [1] "Orchard Logic by Iris Fenwick"

# missing() is also TRUE for an argument that has a default.
with_default <- function(x = 5) missing(x)
with_default()
#> [1] TRUE
with_default(5)
#> [1] FALSE

# ...and stops being TRUE once the argument is assigned to.
reassigned <- function(x) {
  before <- missing(x)
  x <- 1
  c(before = before, after = missing(x))
}
reassigned()
#> before  after
#>   TRUE  FALSE

# missing() follows an argument passed on from a wrapper.
inner <- function(x) missing(x)
outer_fn <- function(x) inner(x)
outer_fn()
#> [1] TRUE
outer_fn(1)
#> [1] FALSE

# An empty argument counts as missing.
pick <- function(a, b) c(a_missing = missing(a), b_missing = missing(b))
pick(, 2)
#> a_missing b_missing
#>      TRUE     FALSE
nargs_demo <- function(a, b, ...) nargs()
nargs_demo(1)
#> [1] 1
nargs_demo(1, 2, 3, 4)
#> [1] 4

Use missing() sparingly. A default of NULL that the body tests with is.null() is often easier for callers to pass through from their own functions, and a real default value documents itself in the signature. missing() earns its place when no default value can express "not supplied", for instance when an argument is derived from another one.

match.arg(): a closed set of choices

An argument that must be one of a few strings is declared with the whole set as its default, and match.arg() validates the value that was passed. In the one-argument form match.arg(type) the choices are read from the default of the formal type of the calling function; if the caller passes nothing, type is the whole vector and the first element is returned. Matching uses pmatch(), so an unambiguous abbreviation is enough, and the result is always the unabbreviated choice. The empty string never matches (R documentation: match.arg).

# match.arg() turns the default vector into a validated choice.
center <- function(x, type = c("mean", "median", "trimmed")) {
  type <- match.arg(type)
  switch(type,
    mean = mean(x),
    median = median(x),
    trimmed = mean(x, trim = 0.2)
  )
}
pages <- c(310, 288, 412, 96, 255, 1200)
center(pages)
#> [1] 426.8333
center(pages, "med")
#> [1] 299
center(pages, "t")
#> [1] 316.25
tryCatch(center(pages, "m"), error = conditionMessage)
#> [1] "'arg' should be one of “mean”, “median”, “trimmed”"
tryCatch(center(pages, "mode"), error = conditionMessage)
#> [1] "'arg' should be one of “mean”, “median”, “trimmed”"
tryCatch(center(pages, c("mean", "median")), error = conditionMessage)
#> [1] "'arg' must be of length 1"
center(pages, NULL)
#> [1] 426.8333

# The match is reported back unabbreviated.
pick <- function(type = c("alpha", "beta")) match.arg(type)
pick("b")
#> [1] "beta"

# An empty string never matches, not even a choice that is "".
tryCatch(match.arg("", c("", "a", "B")), error = conditionMessage)
#> [1] "'arg' should be one of “a”, “B”"

# Without a formal default the choices must be given.
match.arg("gau", c("gaussian", "epanechnikov"))
#> [1] "gaussian"

The "m" call fails because m is a prefix of both mean and median. The help page warns that the exact wording of these error messages is liable to change and did so in R 4.2.0, so a package should not test for them; a test should check that an error occurs. The center() function is the example from that help page, and the switch() it ends with is described on Control Flow.

Several values at once: several.ok

With several.ok = TRUE the argument may have more than one element, and every element that matches is returned; the ones that do not match are dropped without a word. R 4.6.0 added a third value, several.ok = "all" (any string starting with "all" is accepted), which keeps the several-values behaviour but requires every element to match. The R 4.6.0 NEWS entry reads: match.arg() gets a new option several.ok = "all", fulfilling a wish recorded as PR#16659 (R NEWS: Changes in R 4.6.0). It replaces the check that people wrote by hand after several.ok = TRUE.

# several.ok = TRUE keeps the elements that match and silently drops the rest.
columns <- c("title", "author", "year", "genre")
match.arg(c("ti", "au"), columns, several.ok = TRUE)
#> [1] "title"  "author"
match.arg(c("ti", "oops"), columns, several.ok = TRUE)
#> [1] "title"

# several.ok = "all" (R 4.6.0) requires every element to match.
match.arg(c("ti", "au"), columns, several.ok = "all")
#> [1] "title"  "author"
tryCatch(
  match.arg(c("ti", "oops"), columns, several.ok = "all"),
  error = conditionMessage
)
#> [1] "'arg' should be one of “title”, “author”, “year”, “genre”"
tryCatch(
  match.arg(c("ti", "oops"), columns, several.ok = FALSE),
  error = conditionMessage
)
#> [1] "'arg' must be of length 1"

# Inside a function the choices come from the default of the argument.
select_columns <- function(cols = c("title", "author", "year", "genre")) {
  match.arg(cols, several.ok = "all")
}
select_columns()
#> [1] "title"  "author" "year"   "genre"
select_columns(c("year", "ge"))
#> [1] "year"  "genre"
tryCatch(select_columns(c("year", "isbn")), error = conditionMessage)
#> [1] "'arg' should be one of “title”, “author”, “year”, “genre”"

With several.ok = FALSE, the default, a vector of length greater than one is an error with the message 'arg' must be of length 1, unless it is identical to the whole set of choices, which is the "nothing was passed" case. With "all", a non-matching element produces the same 'arg' should be one of ... message that a single non-matching value does, listing all the choices. Code that has to run on R 4.5 or older cannot use "all"; there the portable form is to call match.arg(..., several.ok = TRUE) and compare the length of the result with the length of the input.

The dots argument

The special formal ... stands for any number of supplied arguments. The manual lists two uses: functions that take an arbitrary number of arguments, and functions that absorb some arguments in order to pass them on (R-lang: Arguments). An Introduction to R gives the second use as the motivating example: plot() accepts graphical parameters that it hands to par() (R-intro: The ... argument). ..., ..1, ..2 and so on are reserved words and can only be used inside a function that has ... among its formals (R documentation: dots).

Forwarding the dots

The ordinary use is to write ... in the call of another function, which splices in exactly the arguments the caller supplied, names and all. The function below builds a report of the overdue loans in the shelf scenario and hands everything it does not use to head(), so that the caller can decide how many rows to see. today and days are formals before the dots, which makes them subject to partial matching; n is not a prefix of loans, today or days, so it travels through.

source("scenario.R")

# A report on overdue loans. Everything else goes to head() through "...".
report_overdue <- function(loans,
                           today = shelf_today, # nolint: object_usage_linter.
                           days = loan_days, # nolint: object_usage_linter.
                           ...) {
  end <- loans$return_date
  end[is.na(end)] <- today
  days_out <- as.integer(end - loans$loan_date)
  late <- which(days_out > days)
  report <- data.frame(
    loan_id = loans$loan_id[late],
    loan_date = loans$loan_date[late],
    days_out = days_out[late]
  )
  utils::head(report, ...)
}

report_overdue(loans, n = 4L)
#>   loan_id  loan_date days_out
#> 1    L004 2025-01-26       26
#> 2    L005 2025-02-19       26
#> 3    L007 2025-03-11       24
#> 4    L009 2025-03-30       31
nrow(report_overdue(loans, n = nrow(loans)))
#> [1] 32

# "to" and "da" are abbreviations of formals before the dots.
nrow(report_overdue(loans, to = as.Date("2026-07-31"), da = 30L, n = 100L))
#> [1] 12

# "n" abbreviates none of loans, today and days, so it travels through the dots.
report_overdue(loans, n = 2L)
#>   loan_id  loan_date days_out
#> 1    L004 2025-01-26       26
#> 2    L005 2025-02-19       26

# A misspelled name falls through the dots and is silently ignored by head().
nrow(report_overdue(loans, n = 100L, days = 60L))
#> [1] 4
nrow(report_overdue(loans, n = 100L, dys = 60L))
#> [1] 32

The last two lines show the price of the pattern. days = 60L is matched to the formal and changes the result (4 rows), but a misspelled dys = 60L does not match anything before the dots, so it is passed to head(), which has its own dots and quietly ignores it (32 rows, the same as with the default of 21). Nothing was reported. A function that forwards its dots to something that ignores unknown names cannot warn about typos by itself; if that matters, check ...names() against a list of the names you expect, or put the optional arguments you care about in the signature, after the dots, so that they must be spelled exactly.

Looking inside the dots

Several ways of examining the dots exist, and they differ in whether they evaluate the arguments. list(...) evaluates every argument and returns a named list; ..1, ..2, and so on evaluate one argument, the n-th; ...elt(n) is the functional form of ..n; ...length() returns how many arguments there are; and ...names() returns their names, or NULL if none has a name. The last two do not evaluate the arguments, which is the difference from length(list(...)) and names(list(...)). To get at the expressions rather than the values, use substitute(list(...)) or match.call(expand.dots = FALSE)$....

Expression Returns Evaluates the arguments?

list(...)

A list of the values, named where the arguments are named

all of them

..1, ..2, and so on

The value of the n-th argument

only that one

...elt(n)

The same as ..n, as a function of n; the help page says it is "more elegant and efficient" than eval(paste0("..", n))

only that one

...length()

The number of arguments

none

...names()

The names, with "" for an unnamed argument, or NULL if none is named

none

substitute(list(...))

A call holding the unevaluated expressions

none

match.call(expand.dots = FALSE)$...

A pairlist of the unevaluated expressions, with their names

none

nargs()

The number of supplied arguments, including those matched to named formals

none

tools <- function(...) {
  list(
    length = ...length(),
    names = ...names(),
    first = if (...length() > 0L) ..1,
    as_list = list(...)
  )
}
str(tools(10, b = 20, 30))
#> List of 4
#>  $ length : int 3
#>  $ names  : chr [1:3] "" "b" ""
#>  $ first  : num 10
#>  $ as_list:List of 3
#>   ..$  : num 10
#>   ..$ b: num 20
#>   ..$  : num 30

# ...length() and ...names() do not evaluate the arguments.
lazy_dots <- function(...) c(n = ...length(), named = sum(nzchar(...names())))
lazy_dots(stop("not evaluated"), x = stop("not evaluated either"))
#>     n named
#>     2     1

# ...names() is NULL when no argument has a name.
only_names <- function(...) ...names()
only_names(1, 2)
#> NULL
only_names(a = 1, 2)
#> [1] "a" ""
only_names()
#> NULL

# ...elt(n) is the functional form of ..n.
nth <- function(n, ...) ...elt(n)
nth(2, "a", "b", "c")
#> [1] "b"
nth(1, pi = pi * 0:1, 2:4)
#> [1] 0.000000 3.141593

# Compare with list(...), which evaluates every argument.
tryCatch(list(stop("evaluated")), error = conditionMessage)
#> [1] "evaluated"

# Unevaluated arguments: substitute() and match.call().
names_of <- function(...) {
  vapply(substitute(list(...))[-1L], deparse, character(1L))
}
names_of(books$title, loans$member)
#> [1] "books$title"  "loans$member"
capture <- function(x, ...) match.call(expand.dots = FALSE)$...
capture(1, a = 2, b + c)
#> $a
#> [1] 2
#>
#> [[2]]
#> b + c
#>

...names() is new in R 4.1.0 (the NEWS entry credits the proposal to PR#17705), and in R 4.1.0 to 4.1.2 it sometimes disagreed with names(list(...)); the 4.1.3 NEWS says it "now matches" it, and the help page keeps a check for that case. The same 4.1.0 NEWS says ...elt() now propagates visibility like ..n, which matters when the argument is an invisible() call. A switch(n, ...) is close to ...elt(n) but returns an invisible NULL instead of an error when n is out of range (R NEWS: Changes in R 4.1.0 and 4.1.3).

Errors the dots can cause

The dots are not an object. They can be examined and spliced into a call, but not assigned or returned, and the message depends on where the mistake is. The following are the real messages for the usual mistakes: asking for an element that is not there, using the helpers in a function without dots, treating the dots as a value, and passing an empty argument.

# Errors raised by the dots helpers, with their real text.
too_few <- function(...) ..2
tryCatch(too_few(1), error = conditionMessage)
#> [1] "the ... list contains fewer than 2 elements"
tryCatch(too_few(), error = conditionMessage)
#> [1] "the ... list contains fewer than 2 elements"
none <- function(...) ...elt(0)
tryCatch(none(1), error = conditionMessage)
#> [1] "indexing '...' with non-positive index 0"
no_dots <- function(x) ..1 # nolint: object_usage_linter.
tryCatch(no_dots(1), error = conditionMessage)
#> [1] "..1 used in an incorrect context, no ... to look in"
tryCatch(...length(), error = conditionMessage)
#> [1] "incorrect context: the current call has no '...' to look in"
tryCatch(...elt(1), error = conditionMessage)
#> [1] "..1 used in an incorrect context, no ... to look in"

# "..." is not an ordinary object.
use_dots <- function(...) {
  x <- ... # nolint: object_usage_linter.
  x
}
tryCatch(use_dots(1), error = conditionMessage)
#> [1] "'...' used in an incorrect context"
a_function <- function() list(...) # nolint: object_usage_linter.
tryCatch(a_function(), error = conditionMessage)
#> [1] "'...' used in an incorrect context"

# An empty argument inside the dots fails when it is evaluated.
tryCatch(c(1, , 3), error = conditionMessage)
#> [1] "argument 2 is empty"
count_dots <- function(...) ...length()
count_dots(1, , 3)
#> [1] 3

# A wrapper passes everything on, so the inner function reports the problem.
wrapper <- function(...) sqrt(...)
tryCatch(wrapper(4, 9), error = conditionMessage)
#> [1] "2 arguments passed to 'sqrt' which requires 1"

An empty argument inside the dots is legal to pass (count_dots(1, , 3) counts 3), because nothing is evaluated, and fails with argument 2 is empty when c() tries to use it. A wrapper that forwards the dots to a function that takes only one argument reports its message in the inner function’s terms (2 arguments passed to 'sqrt' which requires 1), so a long list of forwarded arguments can fail far from where it was typed. The classic variant is a function that forwards the same dots to two other functions, one of which does not accept some of them: that produces unused argument for a name the caller wrote correctly. The cure is to split the arguments explicitly or to give each callee only the names it understands.

Return values and visibility

A function returns the value of the last expression it evaluates. return(value) ends it early; its argument is evaluated in the function’s frame first, and return() with no argument returns NULL (R documentation: function). return is called like a function, so the parentheses are required. The idiom in R, and the one that the lintr default return_linter enforces, is to leave return() out of the last line and keep it for early exits.

# The value of a function is its last evaluated expression.
average <- function(x) {
  total <- sum(x)
  total / length(x)
}
average(c(2, 4, 9))
#> [1] 5

# return() ends the function early; its argument is evaluated first.
first_year <- function(years) {
  if (length(years) == 0L) {
    return(NA_integer_)
  }
  min(years, na.rm = TRUE)
}
first_year(integer())
#> [1] NA
first_year(c(1974L, NA, 1813L))
#> [1] 1813

# return() with no argument returns NULL.
nothing <- function() return() # nolint: return_linter.
is.null(nothing())
#> [1] TRUE

# A function whose last expression is an assignment, or an if without an else
# that does not fire, returns invisibly or NULL.
assign_last <- function() total <- 42 # nolint: object_usage_linter.
assign_last()
x <- assign_last()
x
#> [1] 42
no_else <- function(flag) if (flag) "yes"
is.null(no_else(FALSE))
#> [1] TRUE

# Several values: return a list (or a named vector when they share a type).
page_stats <- function(pages) {
  list(
    n = length(pages),
    mean = mean(pages, na.rm = TRUE),
    missing = sum(is.na(pages))
  )
}
stats <- page_stats(c(310L, NA, 288L))
stats$mean
#> [1] 299
str(stats)
#> List of 3
#>  $ n      : int 3
#>  $ mean   : num 299
#>  $ missing: int 1

# Return inside an anonymous function returns from that function only.
doubled <- vapply(1:3, \(i) {
  if (i == 2L) {
    return(0)
  }
  i * 2
}, numeric(1L))
doubled
#> [1] 2 0 6

A function that ends with an assignment returns the assigned value, but invisibly, so assign_last() printed nothing at the console while x <- assign_last() kept 42. A function whose last expression is an if without an else that does not fire returns NULL, also invisibly. A function can return only one object, so several results go in a list (or a vector when they share a type); a data frame or a classed list is the same idea with a printing method, and the S3 Classes and Generics page shows how to give the result a class. A return() inside an anonymous function returns from that function only, as the vapply() call shows.

invisible() and withVisible()

R decides whether to print the value of a top-level expression from a visibility flag that every evaluation sets. invisible(x) returns x with the flag off; the printing is the only thing it changes. It is a primitive function (R documentation: invisible). Assignments, library(), cat() and most print() methods return their result invisibly, so that calling them at the console is silent but the value can still be captured. withVisible() reports the flag without acting on it, and wrapping a call in parentheses turns the flag on again.

# invisible() returns a value without printing it at the top level.
shout <- function(x) {
  cat(toupper(x), "\n")
  invisible(x)
}
shout("quiet streets")
#> QUIET STREETS
y <- shout("kept")
#> KEPT
y
#> [1] "kept"

# Wrapping the call in parentheses, or printing it, shows the value.
(shout("parentheses"))
#> PARENTHESES
#> [1] "parentheses"
print(shout("print"))
#> PRINT
#> [1] "print"

# withVisible() reports the flag instead of acting on it.
str(withVisible(shout("flag")))
#> FLAG
#> List of 2
#>  $ value  : chr "flag"
#>  $ visible: logi FALSE
str(withVisible(1 + 1))
#> List of 2
#>  $ value  : num 2
#>  $ visible: logi TRUE

# Visibility belongs to the call that just returned; assignment resets it.
z <- (shout("assigned"))
#> ASSIGNED
z
#> [1] "assigned"

# print methods conventionally return their argument invisibly.
b <- structure(list(title = "Orchard Logic"), class = "note")
print.note <- function(x, ...) {
  cat("<note>", x$title, "\n")
  invisible(x)
}
b
#> <note> Orchard Logic
r <- print(b)
#> <note> Orchard Logic
identical(r, b)
#> [1] TRUE

# A function can return invisibly from one branch only.
lookup <- function(key, table, quiet = FALSE) {
  value <- table[[key]]
  if (quiet) {
    return(invisible(value))
  }
  value
}
lookup("a", list(a = 1))
#> [1] 1
lookup("a", list(a = 1), quiet = TRUE)

# Top-level auto-print also applies to a call that ends with an assignment
# inside braces: nothing is shown.
f <- function() {
  v <- 1 # nolint: object_usage_linter.
}
f()
(f())
#> [1] 1

Two practical consequences follow. A print() method should return its argument invisibly, as print.note() does, so that x <- print(x) works and so that print() does not print twice at the console. And printing is a property of the top level only: R’s own FAQ explains that in a file read with source() or inside a function a value is never printed automatically, so code that relies on the auto-print has to call print() (R FAQ 7.16). The scripts on this page print because Rscript evaluates each top-level expression the way the console does.

Cleaning up with on.exit()

on.exit(expr) records an expression to run when the current function exits, whether it returns normally or an error ends it. It is how R code restores an option, a working directory or a graphics parameter, and how it closes a connection or removes a temporary file even when something in between fails. The expression is stored unevaluated and runs in the function’s frame at exit, so it sees the variables as they are then; to capture the value as it is now, build the expression with substitute() or a similar function. on.exit is a special primitive and evaluates only add and after, and it returns NULL invisibly (R documentation: on.exit).

The signature is on.exit(expr = NULL, add = FALSE, after = TRUE). The default add = FALSE replaces whatever handlers were registered, which is almost never what a function that registers several cleanups wants, so the usual form is on.exit(cleanup, add = TRUE). after = FALSE puts the new handler at the front, which gives the last-in first-out order that releasing resources in reverse order needs. With add = TRUE, all the handlers run even if one of them signals an error. on.exit() with no argument removes the handlers, and sys.on.exit() returns the expression currently stored. R 4.0.2 fixed on.exit() so that it matches named arguments correctly (R NEWS: Changes in R 4.0.2).

# on.exit() records code to run when the function exits, normally or on error.
cleanup_demo <- function() {
  on.exit(cat("cleanup ran\n"))
  cat("working\n")
  stop("the work failed")
}
tryCatch(cleanup_demo(), error = conditionMessage)
#> working
#> cleanup ran
#> [1] "the work failed"

# Without add = TRUE a second call replaces the first.
replace <- function() {
  on.exit(cat("A\n"))
  on.exit(cat("B\n"))
  invisible(NULL)
}
replace()
#> B

# add = TRUE appends (after = TRUE, the default); after = FALSE puts it first.
order_demo <- function() {
  on.exit(cat("one\n"))
  on.exit(cat("two\n"), add = TRUE)
  on.exit(cat("zero\n"), add = TRUE, after = FALSE)
  cat("body\n")
}
order_demo()
#> body
#> zero
#> one
#> two

# on.exit() with no argument removes the handlers.
removed <- function() {
  on.exit(cat("never printed\n"))
  on.exit()
  "done"
}
removed()
#> [1] "done"

# The expression sees the variables as they are at exit time.
at_exit <- function() {
  x <- 1
  on.exit(cat("x at exit:", x, "\n"))
  x <- 2
  invisible(x)
}
at_exit()
#> x at exit: 2

# Typical uses: restore an option, a working directory, close a connection.
show_pi <- function() {
  old <- options(digits = 3)
  on.exit(options(old))
  print(pi)
}
show_pi()
#> [1] 3.14
print(pi)
#> [1] 3.141593

in_dir <- function(path, code) {
  old <- setwd(path)
  on.exit(setwd(old))
  code
}
in_dir(tempdir(), basename(getwd())) == basename(tempdir())
#> [1] TRUE

read_two_lines <- function() {
  con <- textConnection("first\nsecond\nthird")
  on.exit(close(con))
  readLines(con, n = 2L)
}
read_two_lines()
#> [1] "first"  "second"

# A return() inside on.exit() overrides the returned value.
override <- function() {
  on.exit(return("from on.exit"))
  "from the body"
}
override()
#> [1] "from on.exit"

The order in order_demo() is body, then zero (registered last with after = FALSE, so first), then one, then two. A return() inside an on.exit() handler replaces the function’s value, as override() shows; the ?function help page notes that the value of an ordinary return(value) is computed before any handler runs, which is why a handler that modifies a local variable does not change what is returned. For code that has to defer cleanups from helper functions, the withr package (version 3.0.3 was installed for this check) provides defer() and the local_* and with_* functions; it is a community package, not part of base R.

The running call: sys.call(), sys.function(), match.call() and Recall()

A function can ask R about the call it is running. sys.call() returns the call as it was written, match.call() returns the same call with all the arguments named in their formal-argument form (and, with expand.dots = FALSE, the dots collected into one element), sys.function() returns the function being run, and parent.frame() and sys.frame() give the environments of the caller and of any frame on the stack. They are all documented together with the call-stack functions on the sys.parent help page, with match.call() on its own page (R documentation: match.call). match.call() is what modelling functions such as lm() use to store the call in their result.

Recall(...) calls the function in which it appears, whatever its current name: a recursive function written with Recall() still works after it is renamed or copied. The help page warns that it does not work when passed as a function argument, for instance to vapply(), where it fails with the missing-argument error shown below; naming the function, or using sys.function(), avoids the problem. sys.function() also makes anonymous recursion possible, as in the first example.

# sys.call(), sys.function() and match.call() describe the running call.
describe_call <- function(x, ...) {
  list(call = sys.call(), matched = match.call())
}
describe_call(1, b = 2)
#> $call
#> describe_call(1, b = 2)
#>
#> $matched
#> describe_call(x = 1, b = 2)
#>
describe_call(b = 2, 1)
#> $call
#> describe_call(b = 2, 1)
#>
#> $matched
#> describe_call(x = 1, b = 2)
#>

# sys.function() returns the function being run: anonymous recursion.
(\(n) if (n <= 1) 1 else n * sys.function()(n - 1))(5)
#> [1] 120

# Recall() calls the current function under whatever name it has.
fact <- function(n) if (n <= 1) 1 else n * Recall(n - 1)
fact_renamed <- fact
rm(fact)
fact_renamed(5)
#> [1] 120

# Recall() does not work when handed to another function as an argument.
walk <- function(x) if (is.list(x)) vapply(x, Recall, numeric(1)) else x
tryCatch(walk(list(1, 2)), error = conditionMessage)
#> [1] "argument \"FUN\" is missing, with no default"
walk2 <- function(x) if (is.list(x)) vapply(x, walk2, numeric(1)) else x
walk2(list(1, 2))
#> [1] 1 2

# Recursion depth is limited by options(expressions) and by the C stack.
count_down <- function(n) if (n == 0) "done" else count_down(n - 1)
count_down(100)
#> [1] "done"
tryCatch(count_down(1e5), error = conditionMessage)
#> [1] "evaluation nested too deeply: infinite recursion / options(expressions=)?"

Recursion in R uses a stack of evaluation frames and is limited. The option expressions (default 5000) limits the nesting of expressions, and the C stack can overflow before that; once the limit is reached an error is thrown, as the last line shows (R documentation: options). R does not optimise tail calls by itself. R 4.4.0 added the experimental functions Tailcall() and Exec() to support stack-efficient recursive functions (R NEWS: Changes in R 4.4.0); they are documented on the Tailcall help page, and as the interface is labelled experimental this page does not use them. For deep iteration the usual answer in R is a loop, or a vectorized function, from Vectorisation and Iteration.

Replacement functions

An assignment whose left side is a call, such as names(x) <- c("a", "b"), x[3] <- 5 or class(x) <- "foo", is a call to a replacement function. The function has the name of the accessor with <- pasted on (names<-, [<-, class<-), takes the original object first, and takes the new value as its last argument, which must be named value. It returns the modified object, and that object is stored back in the variable (R-lang: Function calls). The manual adds that some extra effort is made to avoid unnecessary data duplication, so the rewrite is conceptual, not a literal copy.

How f(x) <- value is rewritten

The precise rewrite is documented under "Subset assignment" (R-lang: Subset assignment). For names(x) <- c("a", "b") the manual writes it as three steps: the value of x is saved in a temporary variable called *tmp*, then x is assigned the result of calling the replacement function with that temporary and the value, and finally *tmp* is removed. Nested targets are rewritten recursively, so names(x)[3] <- "Three" becomes a call of names<- whose value is a call of [<- on names(*tmp*). Any existing variable called *tmp* is overwritten and deleted, so the name should not be used in code. The superassignment form, names(x)[3] <<- "Three", evaluates only the target variable in the enclosing environment, and the index expressions in the local one; the manual gives an example and recommends not having a local variable with the same name as the target.

# names(x) <- v, x[3] <- v and attr(x, "a") <- v call replacement functions.
x <- c(a = 1, b = 2, c = 3)
names(x) <- c("one", "two", "three")
x
#>   one   two three
#>     1     2     3

# A replacement function is named `name<-`; its last argument is value.
`second<-` <- function(x, value) {
  x[2L] <- value
  x
}
v <- 1:5
second(v) <- 100L
v
#> [1]   1 100   3   4   5

# The assignment is rewritten to the following three steps.
w <- 1:5
`*tmp*` <- w # nolint: object_name_linter.
w <- `second<-`(`*tmp*`, value = 100L)
rm(`*tmp*`)
identical(v, w)
#> [1] TRUE

# The parser only sees an assignment whose left side is a call.
quote(second(v) <- 100L)
#> second(v) <- 100L
as.list(quote(second(v) <- 100L))
#> [[1]]
#> `<-`
#>
#> [[2]]
#> second(v)
#>
#> [[3]]
#> [1] 100
#>

# The replacement function is called with a stand-in for the variable.
`spy<-` <- function(x, value) {
  print(sys.call())
  x
}
spy(v) <- 0L
#> `spy<-`(`*tmp*`, value = 0L)

# Extra arguments go between the object and value.
`part<-` <- function(x, i, value) {
  x[i] <- value
  x
}
part(v, 3L) <- 0L
v
#> [1]   1 100   0   4   5

# Nested targets are rewritten recursively: names(x)[2] <- "two" is
# `names<-`(x, value = `[<-`(names(x), 2, value = "two")).
y <- c(a = 1, b = 2)
names(y)[2L] <- "two"
y
#>   a two
#>   1   2

# A replacement function must return the whole modified object. This one
# returns the value of its last assignment instead, which is value itself.
`bad<-` <- function(x, value) {
  x[1L] <- value
}
z <- c(10L, 20L, 30L)
bad(z) <- 99L
z
#> [1] 99

Three observations come out of the output. The parser keeps the assignment as a call to <- whose first argument is itself a call (second(v)), which is why an assignment can have any function on its left. The call that a replacement function sees, printed by sys.call() inside spy<-, has the stand-in *tmp* as its first argument and the new value as the argument named value. And a replacement function that forgets to return the object is a common mistake: bad(z) <- 99L made z the number 99, because the value of the last assignment inside the function, which is value, was stored back into z.

A replacement function for the shelf

A replacement function is an ordinary function, so it can be an S3 generic with methods, which is how names<- and levels<- behave for the classes that define them. The book class of the running scenario has a genre field that arrives untidy in books.csv ("Fiction ", "fiction"), so a setter that trims and validates it belongs next to the constructor. The generic is genre<- with UseMethod("genre<-"), and the method is named genre<-.book, the generic name followed by the class. The method name has a <- inside it, which is why the example needs a nolint comment: the default object_name_linter expects snake_case names.

source("scenario.R")

# A replacement generic and a method for the book class.
`genre<-` <- function(x, value) UseMethod("genre<-")

`genre<-.book` <- function(x, value) { # nolint: object_name_linter.
  stopifnot(is.character(value), length(value) == 1L)
  x$genre <- trimws(value)
  x
}

b <- book(
  "The Quiet Streets", "Iris Fenwick", "9780306406157", 2019L, "Fiction", 320L
)
genre(b) <- "  Mystery "
b$genre
#> [1] "Mystery"
b
#> <book> The Quiet Streets (Iris Fenwick, 2019)

# The caller's own variable is rebound, other references are not affected.
copy <- b
genre(b) <- "Poetry"
c(b = b$genre, copy = copy$genre)
#>         b      copy
#>  "Poetry" "Mystery"

# A nested target is rewritten too: shelf is rebound to the modified list.
shelf <- list(featured = b, spare = copy)
genre(shelf$featured) <- "Classic"
vapply(shelf, \(item) item$genre, character(1L))
#>  featured     spare
#> "Classic" "Mystery"

# Validation errors come from inside the replacement function.
tryCatch(genre(b) <- 3, error = conditionMessage)
#> [1] "is.character(value) is not TRUE"
tryCatch(genre(b) <- c("a", "b"), error = conditionMessage)
#> [1] "length(value) == 1L is not TRUE"

# Real errors when the pieces are missing.
tryCatch(undefined(b) <- 1, error = conditionMessage)
#> [1] "could not find function \"undefined<-\""
tryCatch(genre(zzz) <- "x", error = conditionMessage)
#> [1] "object 'zzz' not found"
tryCatch(genre(b$title) <- "x", error = conditionMessage)
#> [1] "no applicable method for 'genre<-' applied to an object of class \"character\""

# Inside a function the assignment changes the local copy only.
retitle <- function(item) {
  genre(item) <- "Reference" # nolint: object_usage_linter.
  item
}
c(before = b$genre, returned = retitle(b)$genre, after = b$genre)
#>      before    returned       after
#>    "Poetry" "Reference"    "Poetry"

A function that "modifies" its argument modifies a copy, so the caller’s object does not change unless the caller assigns the result: retitle() returned a new book and b kept its genre. The same holds for the nested target shelf$featured: R rewrites the assignment to *tmp* <- shelf, a call of $<- on the list containing the result of genre<-, and rebinds the variable shelf; the book stored in copy is unaffected. Both effects come from the copy-on-modify semantics described on Memory and Copy Semantics. The errors at the end of the block are the three ways the pieces can be missing: no function named undefined<-, a target variable that does not exist (the target must exist before the replacement call runs), and no method for the class of the target. The S4, R6 and S7 systems have their own ways of getting and setting a field, shown in the pages on those systems.

Infix functions

The grammar knows a family of operators written %name%: any function whose name starts and ends with a percent sign can be called between two arguments. An Introduction to R shows the definition with "%!%" <- function(X, y) { ... } and notes that the matrix product %*% and the outer product %o% are defined the same way (R-intro: Defining new binary operators). The name must be quoted or backtick-quoted when it is assigned, because it is not a syntactic name. Base R defines %in%, %o%, %x%, %%, %/% and %*%, and since R 4.4.0 also %||%, the "if the left side is NULL use the right side" operator (R NEWS: Changes in R 4.4.0; the operator lives on the Control help page).

The precedence of %name% is fixed by the grammar: it binds tighter than * and / and less tightly than : and unary minus; the native pipe |> has the same level (R-lang: Infix and prefix operators). That is the surprising part, because a user-defined operator does not group like +. The parsed form of an infix operation is a call to the function with the operands as arguments, so calling it by name, with backticks around the operator (as in the second example below), is the same expression as a %+% b. The full precedence table, and what it implies for -1^2 and for |>, is on Operators and Precedence.

# Any function named %something% is an infix operator.
`%+%` <- function(lhs, rhs) paste0(lhs, rhs)
"Orchard" %+% " " %+% "Logic"
#> [1] "Orchard Logic"

# It is an ordinary function: call it with prefix syntax too.
`%+%`("a", "b")
#> [1] "ab"
vapply(c("x", "y"), `%+%`, character(1L), "!")
#>    x    y
#> "x!" "y!"

# %anything% binds tighter than * and / but looser than ":", so the grouping
# can surprise: 1 + 2 %x2% 3 is 1 + (2 %x2% 3).
`%x2%` <- function(a, b) a * 2 + b
1 + 2 %x2% 3
#> [1] 8
2 * 3 %x2% 4
#> [1] 20
quote(1 + 2 %x2% 3)
#> 1 + 2 %x2% 3

# %+% binds tighter than * and /.
tryCatch(2 %+% 3 * 2, error = conditionMessage)
#> [1] "non-numeric argument to binary operator"

# A pipeline-style operator: the left value becomes the argument of a function.
`%then%` <- function(x, f) f(x)
c(4, 9, 16) %then% sqrt %then% sum
#> [1] 9

# Base R already defines several, and %||% since R 4.4.0.
NULL %||% "default"
#> [1] "default"
"set" %||% "default"
#> [1] "set"
c(1, 3) %in% 1:2
#> [1]  TRUE FALSE

# An undefined infix operator.
tryCatch(1 %nope% 2, error = conditionMessage)
#> [1] "could not find function \"%nope%\""

1 + 2 %x2% 3 is 1 + (2 %x2% 3), which is 8; and 2 %+% 3 * 2 is (2 %+% 3) * 2, which fails because "23" is not a number, not 2 %+% 6 as a reader used to the arithmetic operators would expect. When in doubt, add parentheses. A name that is not defined gives could not find function "%nope%", the same message as any missing function. The native pipe |> is not an infix function: the parser rewrites it into a nested call before evaluation, which is described on Pipes and the Native Pipe.

Calling with a list: do.call()

do.call(what, args, quote = FALSE, envir = parent.frame()) builds and runs a call from a function, or the name of one, and a list of arguments; the names of the list become the argument names (R documentation: do.call). It is the tool when the arguments exist as data, for example a list of options read from a file, or a list of data frames that rbind() has to bind, which is the most common use in practice. Reduce() and Map() are the neighbours for folding and element-wise application; they are described in The apply Family and Functionals and only mentioned here.

source("scenario.R")

# do.call() calls a function with the arguments held in a list.
args_list <- list(c(310, 288, NA, 96), na.rm = TRUE)
do.call(mean, args_list)
#> [1] 231.3333
do.call("paste", list("a", "b", sep = "-"))
#> [1] "a-b"

# The usual use: combine a list of pieces.
pieces <- list(c(1, 2), c(3, 4), c(5, 6))
do.call(rbind, pieces)
#>      [,1] [,2]
#> [1,]    1    2
#> [2,]    3    4
#> [3,]    5    6
by_member <- split(loans$loan_id, loans$member)
length(do.call(c, by_member))
#> [1] 60

# Names of the list become argument names.
do.call(seq, list(from = 1, to = 10, by = 3))
#> [1]  1  4  7 10

# quote = FALSE (the default) evaluates a symbol in the calling environment,
# quote = TRUE hands the symbol itself to the function.
shelf_name <- "Main shelf"
do.call(paste, list(as.name("shelf_name"), "!"))
#> [1] "Main shelf !"
do.call(paste, list(as.name("shelf_name"), "!"), quote = TRUE)
#> [1] "shelf_name !"

# What the called function sees as its call.
show_call <- function(...) sys.call()
do.call(show_call, list(1:3, "x"))
#> (function (...)
#> sys.call())(1:3, "x")
do.call("show_call", list(1:3, "x"))
#> show_call(1:3, "x")

# Passing the function itself puts its whole definition into the call that
# error messages and tracebacks show; passing its name keeps the call short.
fail <- function(x) stop("failed")
tryCatch(do.call("fail", list(1)), error = \(e) deparse(conditionCall(e)))
#> [1] "fail(1)"
tryCatch(do.call(fail, list(1)), error = \(e) deparse(conditionCall(e)))
#> [1] "(function (x) "       "stop(\"failed\"))(1)"

# A long list is not a problem: this is the idiom for binding many pieces.
parts <- lapply(seq_len(500L), \(i) data.frame(id = i, square = i^2))
nrow(do.call(rbind, parts))
#> [1] 500
big <- as.list(seq_len(10000L))
length(do.call(c, big))
#> [1] 10000

Two details of do.call() matter. With quote = FALSE the arguments are evaluated in the calling environment before the call, so a symbol in the list is looked up, whereas with quote = TRUE each argument is quoted and the function receives the symbol itself ("shelf_name !"). And the function can be given as an object or as a string: with a string, the call that appears in error messages and tracebacks is short (fail(1)), while with the function object, R puts the whole deparsed definition into the call, which makes long and sometimes unreadable messages. The help page also cautions that the semantics of substitute() and similar functions are undefined under do.call() and subject to change. Long lists are not a problem for do.call() itself: binding 500 data frames with do.call(rbind, parts) is a one-line idiom, and do.call(c, big) with 10,000 elements works.

Pitfalls collected

The previous sections each carry their own pitfalls; this table puts the main ones next to the R FAQ entry or the manual passage that explains them.

Symptom Cause and remedy Where

unused argument (...) for a name written correctly

The function has no ... and no formal with that name, or the same dots were forwarded to two callees. Check the formals with args(); split the arguments.

R-lang: Argument matching

An abbreviated argument silently goes to the wrong formal, or to the dots

Partial matching only applies to formals before .... Spell the name out; turn on options(warnPartialMatchArgs = TRUE) to find the uses.

R-lang: Argument matching

A misspelled option has no effect

It was absorbed by ... and forwarded to a function that ignores unknown names. Compare ...names() with the expected names.

This page

A default argument gives a different value than expected

Defaults are evaluated at first use, in the function’s own frame. Compute the value first, or use force().

R-lang: Argument evaluation

A function prints nothing, or its value cannot be saved

It ends with an assignment or invisible(); the value is there, (f()) shows it.

R FAQ 7.16

A replacement call changes x to the wrong thing

The replacement function did not return the whole object, or the last argument is not called value.

R-lang: Subset assignment

outer() fails with the condition has length > 1

The function is not vectorized: outer() passes whole vectors to it. Wrap it in Vectorize() or rewrite it with vectorized operations.

R FAQ 7.17

A cleanup handler never runs, or only the last one runs

on.exit() was called without add = TRUE, or outside a function.

?on.exit

Recall() fails inside vapply() or sapply()

Recall cannot be passed as an argument; name the function.

?Recall

FAQ 7.17 is the one that most often surprises: outer(x, y, FUN) builds two large vectors holding every combination of the elements and calls FUN once with both, so the function must work on vectors. A function written with a scalar if does not, and Vectorize() is the wrapper that the FAQ recommends. The following script reproduces the failure on a late-fee rule from the shelf scenario and shows both repairs.

# R FAQ 7.17: outer() needs a function that is vectorized over its arguments.
overdue_cost <- function(days_late, rate) {
  if (days_late <= 0) 0 else days_late * rate
}
tryCatch(
  outer(c(0, 3, 10), c(0.1, 0.25), overdue_cost),
  error = conditionMessage
)
#> [1] "the condition has length > 1"

# Vectorize() wraps a scalar function so that it works elementwise.
vectorized_cost <- Vectorize(overdue_cost)
outer(c(0, 3, 10), c(0.1, 0.25), vectorized_cost)
#>      [,1] [,2]
#> [1,]  0.0 0.00
#> [2,]  0.3 0.75
#> [3,]  1.0 2.50

# Rewriting the function with vectorized operations is usually better.
overdue_cost2 <- function(days_late, rate) pmax(days_late, 0) * rate
outer(c(0, 3, 10), c(0.1, 0.25), overdue_cost2)
#>      [,1] [,2]
#> [1,]  0.0 0.00
#> [2,]  0.3 0.75
#> [3,]  1.0 2.50

Vectorize() hides a loop, so it is a convenience rather than a speed-up; the pmax() version works on whole vectors and is the better rewrite when the logic allows it.

Version notes

Version Change Source

R 4.0.2

on.exit() now matches its named arguments correctly.

NEWS 4.0.2

R 4.1.0

\(x) x + 1 is accepted as a shorthand for function(x) x + 1; ...names() is new; ...elt() propagates visibility consistently with ..n.

NEWS 4.1.0

R 4.1.3

...names() now matches names(list(...)) closely.

NEWS 4.1.3

R 4.2.0

The error message of match.arg("", c("", "a", "B")) was improved; the wording of the match.arg() errors changed.

NEWS 4.2.0, ?match.arg

R 4.4.0

%||% is added to base; Tailcall() and Exec() are added as experimental.

NEWS 4.4.0

R 4.6.0

match.arg() gets several.ok = "all".

NEWS 4.6.0

R-devel (4.7.0, under development)

New higher-order functions Compose(), Funcall() and Partial() are listed in the R-devel NEWS. They do not exist in R 4.6.1 (exists("Compose") is FALSE there), so no example on this page uses them.

R-devel NEWS

The R-devel entries are in the development NEWS; they may change before 4.7.0 is released.

Where older tutorials differ

Many pages on the web predate R 4.1.0 and some predate R 3.x. The table lists what they do differently from the current practice used on this page.

Older tutorial Current practice Why Version

Only function(x) ... for short functions, and sapply(x, function(i) ...) everywhere

\(x) ... for short anonymous functions, and vapply() with a declared result type

the lambda is parsed into the same function call; vapply() fails loudly on a wrong result type

R 4.1.0

return(x) as the last line of every function

Leave return() for early exits and end with the value

implicit return is idiomatic and is what the default lintr return_linter enforces

always

names(list(...)) or names(match.call(expand.dots = FALSE)$...) to read the names of the dots

...names(), ...length() and ...elt(n)

they do not evaluate the arguments

R 4.1.0

match.arg(x, several.ok = TRUE) followed by a hand-written length check

match.arg(x, several.ok = "all")

it reports the non-matching element with the usual message

R 4.6.0

if (is.null(x)) y else x

x %||% y

one operator in base R

R 4.4.0

on.exit(close(con)) repeated in a function body

on.exit(close(con), add = TRUE)

the second call without add silently replaces the first

always

Mutating a variable of the caller with <<- or assign()

Return the new value, or use a replacement function and assign the result

R functions work on copies, and a replacement function states its intent

always

Treating foo(x <- y) as a way to set x

Assign before the call

the assignment may never run, because arguments are lazy (the manual advises against it)

always

Recursion as the default way to loop

Vectorized functions and loops; Tailcall() only as an experimental tool

R does not eliminate tail calls

R 4.4.0

T and F as argument values, such as na.rm = T

TRUE and FALSE

T and F are variables that can be overwritten (R FAQ 3.3.3 lists the differences between R and S)

always

"R uses dynamic scope for functions as in S"

Free variables are found in the environment where the function was created

lexical scoping (R FAQ 3.3.1)

always

For the full list of changes by release see What’s New and Migrating Older Code.

Where to go next

References