Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

R (4.6)  Module 4 – Control flow, functions and errors

Writing functions

Write R functions with clear return values, predict how arguments match, check inputs with stopifnot() and match.arg(), and use ... and lazy defaults safely.

  • Beginner
  • 15 minutes
  • Examples run with R 4.6.1 and Python 3.14.8
  • By MySmartCoPilot
Free lesson, Pro answers: 15 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

What you will learn

  • Define functions with arguments, defaults and a clear return value
  • Predict how R matches the arguments of a call by exact name, partial name and position
  • Validate inputs with stopifnot() and match.arg(), with messages a user understands
  • Pass extra arguments through with ... and detect optional ones with missing()
  • Explain lazy evaluation and predict when an argument or a default is computed

Before you start

What you’ll learn · Before you start

What you will learn

  • Define functions with arguments, defaults and a clear return value
  • Predict how R matches the arguments of a call by exact name, partial name and position
  • Validate inputs with stopifnot() and match.arg(), with messages a user understands
  • Pass extra arguments through with ... and detect optional ones with missing()
  • Explain lazy evaluation and predict when an argument or a default is computed

Before you start

On this page

mean(c(1, NA, 3), na.mr = TRUE) returns NA, with no error and no warning: the misspelt argument was accepted and ignored. Functions are how R code is reused, and the way a function takes its arguments decides whether a slip like that fails loudly or passes in silence.

On runs of R 4.6.1 you will define functions with defaults and clear return values, predict how R matches the arguments of a call, check inputs at the top with stopifnot() and match.arg(), pass extra arguments through ..., and predict when an argument is computed, which R does only when the function first uses it.

A function returns the value of its last expression

mean_of(c(4, 8, 9)) returns 7. A function is made with function(arguments) body and stored under a name like any other value, and the help page ?function gives the rule for its result: when the end of the body is reached without a call to return(), the value of the last evaluated expression is returned. Step through one call, and one more that changes its argument:

Interactive · message by message What happens when you call a function Step through mean_of(c(4, 8, 9)), then double_it(marks). Messages travel from the caller to the function and back.
What happens when you call a function, step 5 of 5. Messages so far: The caller to The function: x = c(4, 8, 9); The function to The caller: returns 7; The caller to The function: v = marks, 1 2 3; The function to The caller: returns 2 4 6.The callermarks is still 1 2 3The functionv is 2 4 6x = c(4, 8, 9)returns 7v = marks, 1 2 3returns 2 4 6

The drawing shows the whole scene; its 5 steps are listed below.

Every step as text
  1. The call. mean_of(c(4, 8, 9)) sends the value c(4, 8, 9) to the function, which gives it the name of its argument, x.
  2. The body runs. The body runs in a new environment of its own. total <- sum(x) creates total there, 21; nothing outside the function sees it.
  3. The last value returns. The body's last expression is total / length(x), 7, and that is the value the call returns. No return() is needed.
  4. A local copy. double_it(marks) gives the function the values of marks. v <- v * 2 changes the function's v, not the caller's marks.
  5. The caller is unchanged. The function returns 2 4 6, and marks is still 1 2 3. To keep the result, the caller assigns it, marks <- double_it(marks).

The run defines five small functions and calls each:

anatomy.R, typed at the R console Python · anatomy_session.py
from session import run, version

version()
# R reads anatomy.R as if each line were typed at the console.
run("""
R --vanilla --quiet -f anatomy.R
""")

Output

# R version 4.6.1 -- "Happy Hop"
$ R --vanilla --quiet -f anatomy.R
> # A function returns the value of its last expression.
> mean_of <- function(x) {
+   total <- sum(x)
+   total / length(x)
+ }
> mean_of(c(4, 8, 9))
[1] 7
> # return() leaves early, here for an empty input.
> mean_safe <- function(x) {
+   if (length(x) == 0) return(NA_real_)
+   sum(x) / length(x)
+ }
> mean_safe(numeric(0))
[1] NA
> # Several results come back as one object, such as a named vector.
> describe <- function(x) c(n = length(x), mean = mean(x), max = max(x))
> describe(c(4, 8, 9))
   n mean  max 
   3    7    9 
> # invisible() returns a value without printing it.
> log_line <- function(text) {
+   line <- paste("Note:", text)
+   invisible(line)
+ }
> log_line("check row 12")
> saved <- log_line("check row 12")
> saved
[1] "Note: check row 12"
> # Changing an argument inside changes a local copy only.
> double_it <- function(v) {
+   v <- v * 2
+   v
+ }
> marks <- c(1, 2, 3)
> double_it(marks)
[1] 2 4 6
> marks
[1] 1 2 3

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 anatomy_session.py

  • return() is for leaving early. mean_safe() returns NA_real_ at once for an empty input, so the formula below it never divides by zero; the normal result is still its last line.
  • Several results come back as one object. A function returns one value, so describe() returns a named vector, n, mean and max; a list does the same for results of different types.
  • invisible() returns without printing. log_line("check row 12") printed nothing at the console, yet saved received the line. Use it for functions whose main job is a side effect, such as writing a file, so that the console stays quiet while the value is still there for whoever wants it.
  • Arguments are local. double_it(marks) doubled its own v; marks was still 1 2 3 afterwards. A function changes nothing outside itself unless its result is assigned, marks <- double_it(marks).

The help page adds that \(x) x + 1 is a short form of function(x) x + 1, handy for the small functions passed to other functions in the next module.

Arguments match by exact name, then partial name, then position

fit(1:5, tr = 0.3) sets trim to 0.3, although the call never spells trim out. The R Language Definition describes how a call’s arguments are matched to a function’s formal arguments in three passes: first supplied names that equal a formal name exactly, then supplied names that are the start of exactly one formal name, then the unnamed values, by position, into the formals not yet matched. Arguments nobody supplied take their default: The value written for an argument in a function's definition, as in digits = 2, used when a call gives none. R computes it inside the function, when the argument is first used.. Step through the three passes:

Interactive · step by step How R matches the arguments of a call Step through fit(1:5, tr = 0.3). The left column is what the call supplies; the right column is the arguments fit() defines.
How R matches the arguments of a call, step 6 of 6. Showing 1:5, tr = 0.3 (v = 1: fits two names), data (data = 1:5), weights, trim (trim = 0.3). In focus: tr = 0.3.partial nameposition1:5no nametr = 0.3v = 1: fits two namesdatadata = 1:5weightsdefault NULLtrimtrim = 0.3

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. The call and the formals. fit() defines three arguments, data, weights and trim. The call supplies one value without a name and one named tr.
  2. Pass 1: exact names. R first looks for supplied names that equal a formal name exactly. tr is not data, weights or trim, so nothing matches yet.
  3. Pass 2: partial names. Then it looks for supplied names that start a formal name. tr starts trim and nothing else, so tr = 0.3 becomes trim.
  4. Pass 3: positions. Unnamed values fill the formals not yet matched, in order. 1:5 goes to data, the first of them.
  5. Defaults for the rest. weights got no value, so it keeps its default, NULL, and the call returns "n = 5, weights: none, trim = 0.3".
  6. When a start fits two names. In f(v = 1), with formals value and verbose, v starts both, so pass 2 cannot choose and R stops with "argument 1 matches multiple formal arguments".

The run tries the same function with names in different places, then the cases that stop:

matching.R, typed at the R console Python · matching_session.py
from session import run, version

version()
# R reads matching.R as if each line were typed at the console.
run("""
R --vanilla --quiet -f matching.R
""")

Output

# R version 4.6.1 -- "Happy Hop"
$ R --vanilla --quiet -f matching.R
> fit <- function(data, weights = NULL, trim = 0) {
+   sprintf("n = %d, weights: %s, trim = %.1f",
+     length(data), if (is.null(weights)) "none" else "given", trim)
+ }
> # Exact names first, then partial names, then positions.
> fit(1:5, trim = 0.1)
[1] "n = 5, weights: none, trim = 0.1"
> fit(trim = 0.1, 1:5)
[1] "n = 5, weights: none, trim = 0.1"
> fit(1:5, NULL, 0.2)
[1] "n = 5, weights: none, trim = 0.2"
> fit(1:5, tr = 0.3)
[1] "n = 5, weights: none, trim = 0.3"
> # A start that fits two names is an error.
> f <- function(value, verbose = FALSE) value
> try(f(v = 1))
Error in f(v = 1) : argument 1 matches multiple formal arguments
> # Arguments that match nothing are errors too.
> try(fit(1:5, colour = "red"))
Error in fit(1:5, colour = "red") : unused argument (colour = "red")
> # mean() takes na = TRUE for na.rm = TRUE, by partial matching.
> mean(c(1, NA, 3), na = TRUE)
[1] 2
> # This option makes R warn whenever it matches a name partially.
> options(warnPartialMatchArgs = TRUE)
> mean(c(1, NA, 3), na = TRUE)
[1] 2
Warning message:
In mean.default(c(1, NA, 3), na = TRUE) :
  partial argument match of 'na' to 'na.rm'

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 matching_session.py

The first four calls all worked. A name can come before the value it skips, as fit(trim = 0.1, 1:5) shows, because names are matched before positions. f(v = 1) stopped, because v is the start of both value and verbose, and an argument that matches no formal at all, colour = "red", stopped with “unused argument”. mean() accepted na = TRUE as na.rm = TRUE by the same partial matching, and with options(warnPartialMatchArgs = TRUE) R said so.

Relying on partial names

mean(x, na = TRUE) works today, but a later version of a function can add an argument that starts the same way, and the short name then stops with “matches multiple formal arguments”. Spell names out in code you keep, and turn on options(warnPartialMatchArgs = TRUE) in your tests to find the short ones.

Checkpoint

Check: Return values, invisible() and local copies · How arguments are matched

  1. Question 1 of 2 What does h(3) return?

    Read the code, then choose one answer.

    h <- function(x) {
      y <- x + 1
      y * 2
    }
    Show the answer: Question 1 of 2

    Answer: 8

    ?function: if the end of a function is reached without calling return, the value of the last evaluated expression is returned.

  2. Question 2 of 2 What does g(ga = 10, 1) return?

    Read the code, then choose one answer.

    g <- function(alpha, beta = 2, gamma = 3) c(alpha, beta, gamma)
    Show the answer: Question 2 of 2

    Answer: 1 2 10

    Exact names, then partial names, then positions: the three passes of the R Language Definition.

Check the inputs at the top with stopifnot() and match.arg()

summarise_vec(c("62", "71")) stops at once with “x must be numeric”, the sentence written in the check. Each check at the top of a function turns a confusing failure deep inside it into a message that names the problem. The help page of stopifnot() says it stops at the first expression that is not all TRUE, and that names given to its arguments are used instead of the default message; R 4.0.0 added those names. match.arg(type) checks a choice against the vector of choices in the argument’s default, c("t", "normal"), accepts an abbreviation and returns the full name, and with no choice given returns the first. Step through the calls of the run:

Interactive · step by step The checks at the top of summarise_vec() Step through the calls of the run. Each check on the left lets a good input through; a bad one stops with the message on the right.
The checks at the top of summarise_vec(), step 5 of 5. Showing is.numeric(x), length(x) >= 2, level in (0, 1), match.arg(type), The interval, Error, Error, Error, Error. In focus: The interval.FALSEFALSEFALSE or NAno matchis.numeric(x)length(x) >= 2level in (0, 1)match.arg(type)The intervaln, mean, sd, lower, upperErrorx must be numericErrorx needs at least 2 valuesErrorlevel not between 0 and 1Errorshould be one of t, normal

The drawing shows the whole scene; its 5 steps are listed below.

Every step as text
  1. Text, not numbers. summarise_vec(c("62", "71")) fails the first check: the marks are text, so stopifnot() stops with the name of that check, "x must be numeric".
  2. One value. summarise_vec(62) is numeric but has one value, and a standard deviation needs two, so the second check stops it.
  3. A percentage for a share. level = 95 was meant as 95 %, but the function takes a share. The third check stops it, and level = NA stops there too, because NA is not TRUE.
  4. An unknown choice. "bootstrap" is not one of the choices in the default c("t", "normal"), so match.arg() stops and lists the choices.
  5. A good call. summarise_vec(marks, "norm") passes every check; match.arg() expands "norm" to "normal", and the function returns n 6, mean 68.17 and the interval 61.06 to 75.28.

The run builds the function, calls it twice with good input and once for each check:

validate.R, typed at the R console Python · validate_session.py
from session import run, version

version()
# R reads validate.R as if each line were typed at the console.
run("""
R --vanilla --quiet -f validate.R
""")

Output

# R version 4.6.1 -- "Happy Hop"
$ R --vanilla --quiet -f validate.R
> summarise_vec <- function(x, type = c("t", "normal"), level = 0.95) {
+   stopifnot(
+     "x must be numeric" = is.numeric(x),
+     "x needs at least 2 values" = length(x) >= 2,
+     "level must be one number between 0 and 1" =
+       length(level) == 1 && level > 0 && level < 1
+   )
+   type <- match.arg(type)
+   n <- length(x)
+   m <- mean(x)
+   se <- sd(x) / sqrt(n)
+   q <- if (type == "t") qt((1 + level) / 2, df = n - 1) else qnorm((1 + level) / 2)
+   round(c(n = n, mean = m, sd = sd(x), lower = m - q * se, upper = m + q * se), 2)
+ }
> marks <- c(62, 71, 55, 80, 67, 74)
> summarise_vec(marks)
    n  mean    sd lower upper 
 6.00 68.17  8.89 58.84 77.49 
> summarise_vec(marks, "norm")
    n  mean    sd lower upper 
 6.00 68.17  8.89 61.06 75.28 
> # Each check names its own problem.
> try(summarise_vec(c("62", "71")))
Error in summarise_vec(c("62", "71")) : x must be numeric
> try(summarise_vec(62))
Error in summarise_vec(62) : x needs at least 2 values
> try(summarise_vec(marks, level = 95))
Error in summarise_vec(marks, level = 95) : 
  level must be one number between 0 and 1
> try(summarise_vec(marks, level = NA))
Error in summarise_vec(marks, level = NA) : 
  level must be one number between 0 and 1
> try(summarise_vec(marks, "bootstrap"))
Error in match.arg(type) : 'arg' should be one of “t”, “normal”
> # several.ok = TRUE keeps the choices that match; "all" (R 4.6.0) refuses any that do not.
> match.arg(c("mean", "sdd"), c("n", "mean", "sd"), several.ok = TRUE)
[1] "mean"
> try(match.arg(c("mean", "sdd"), c("n", "mean", "sd"), several.ok = "all"))
Error in match.arg(c("mean", "sdd"), c("n", "mean", "sd"), several.ok = "all") : 
  'arg' should be one of “n”, “mean”, “sd”

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 validate_session.py

The good calls give a 95 % interval of 58.84 to 77.49 with the t distribution and 61.06 to 75.28 with the normal, narrower because the normal ignores that the standard deviation was estimated from only six marks. level = NA stopped at the third check too: the expression gives NA, and stopifnot() treats anything but TRUE as a failure. length(level) == 1 comes first in that check because && needs single values, and the help page of stopifnot() adds that each expression must give TRUE itself: a positive number is not TRUE there, although if () would accept it.

New in R 4.6.0: several.ok = "all"

match.arg(arg, choices, several.ok = "all") accepts several choices but requires every one of them to match, so a misspelt choice is an error instead of being dropped. On R 4.5 and older, compare arg with the choices yourself first.

The last two lines of the run show the difference. For a choice that may take several values, several.ok = TRUE returned only "mean" for c("mean", "sdd"), silently dropping the typo, while several.ok = "all" stopped and listed the valid choices.

Key idea

Check types, lengths, ranges and choices in the first lines of a function, each with a message that names the problem, so that bad input stops at the door instead of failing somewhere inside.

Checkpoint

Check: Input checks with stopifnot() and match.arg()

  1. Question 1 of 2 y <- -1. What does stopifnot("y must be positive" = y > 0) do?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: It stops with the error "y must be positive"

    Since R 4.0.0, the names of stopifnot()'s arguments become its error messages.

  2. Question 2 of 2 What do f("med") and f("me") return?

    Read the code, then choose one answer.

    f <- function(type = c("mean", "median")) match.arg(type)
    Show the answer: Question 2 of 2

    Answer: "median", and an error for "me"

    match.arg() matches with pmatch(), so a unique abbreviation works and an ambiguous one is an error.

The dots argument passes extra arguments on, typos too

show_dots(1, 2, digits = 3) reported 2 extra arguments: ...: The dots argument of a function: it collects every argument that matches no other name, so the function can pass them on, as mean(x, ...) does. ...length() counts them and ...names() names them. collects every argument that matches no other name, and the help page ?dots lists the tools to read it: ...length() counts them, ...names() gives their names, "" for an unnamed one, and ..1 is the first. The usual use is to pass them on, as avg <- function(x, ...) mean(x, ...) does. Follow a misspelt argument down that path:

Interactive · stage by stage How ... passes a misspelt argument through to nowhere Step through avg(c(1, NA, 3), na.mr = TRUE). Each stage is one function the arguments pass through; the branch is the call with the right name.
How ... passes a misspelt argument through to nowhere, step 6 of 6. Showing avg(x, na.mr = TRUE), avg <- function(x, ...), mean(x, ...), mean.default(x, ...) (na.mr ignored, na.rm FALSE), The result (2).na.rm = TRUE: 2avg(x, na.mr = TRUE)na.mr is a typo of na.rmavg <- function(x, ...)... collects na.mr = TRUEmean(x, ...)passes ... onmean.default(x, ...)na.mr ignored, na.rm FALSEThe result2na.rm = TRUE: 2

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. The call. avg(c(1, NA, 3), na.mr = TRUE) gives x a vector with an NA and one more argument, named na.mr, a slip for na.rm.
  2. Into the dots. avg() has only x and .... na.mr is not x, so it goes into ..., which takes every argument that matches no other name, without checking it.
  3. Passed on. mean(x, ...) passes the contents of ... to mean(), which hands them to its method for numbers, mean.default().
  4. Swallowed. mean.default(x, trim = 0, na.rm = FALSE, ...) has its own .... na.mr matches neither trim nor na.rm, so it lands there and is ignored. na.rm stays FALSE.
  5. NA, and no error. With na.rm FALSE, the mean of a vector with an NA is NA. Nothing warned about the typo; the only sign is the wrong answer.
  6. The right name. With na.rm = TRUE spelt in full, the same path reaches mean.default()'s na.rm, and the result is 2, the mean of 1 and 3.

The run shows the tools, the pass-through, the typo and two ways to catch it:

dots.R, typed at the R console Python · dots_session.py
from session import run, version

version()
# R reads dots.R as if each line were typed at the console.
run("""
R --vanilla --quiet -f dots.R
""")

Output

# R version 4.6.1 -- "Happy Hop"
$ R --vanilla --quiet -f dots.R
> # ... collects every argument that matches no other name.
> show_dots <- function(x, ...) {
+   cat("extra arguments:", ...length(), "\n")
+   print(...names())
+   if (...length() > 0) print(..1)
+ }
> show_dots(1, 2, digits = 3)
extra arguments: 2 
[1] ""       "digits"
[1] 2
> # Pass them on to another function.
> avg <- function(x, ...) mean(x, ...)
> avg(c(1, NA, 3), na.rm = TRUE)
[1] 2
> # A misspelt name lands in ... and is ignored without an error.
> avg(c(1, NA, 3), na.mr = TRUE)
[1] NA
> mean(c(1, NA, 3), na.mr = TRUE)
[1] NA
> # chkDots() warns about arguments that a function takes in ... but never uses.
> first_of <- function(x, ...) {
+   chkDots(...)
+   x[1]
+ }
> first_of(c(7, 8), na.mr = TRUE)
[1] 7
Warning message:
In first_of(c(7, 8), na.mr = TRUE) :
 extra argument ‘na.mr’ will be disregarded 
> # Names after ... must be spelt in full: se is not sep.
> paste("a", "b", se = "-")
[1] "a b -"
> paste("a", "b", sep = "-")
[1] "a-b"
> # missing() tells whether a caller gave an argument.
> share_of <- function(x, total) {
+   if (missing(total)) total <- sum(x)
+   round(x / total, 2)
+ }
> share_of(c(2, 3, 5))
[1] 0.2 0.3 0.5
> share_of(c(2, 3, 5), total = 20)
[1] 0.10 0.15 0.25

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 dots_session.py

avg() with na.rm = TRUE returned 2, and with na.mr = TRUE it returned NA: the typo travelled through two ... and was dropped by mean.default(), as it was when mean() was called directly. chkDots(), for a function that takes ... but uses none of it, turned the same typo into a warning, “extra argument ‘na.mr’ will be disregarded”. paste("a", "b", se = "-") shows the other rule of ...: the R Language Definition says partial matching applies only to the formals before ..., so se was not taken as sep and became a third string to paste.

missing(total) is TRUE when the caller gave no total, so share_of() divides by the sum by default and by 20 when told. A default, total = sum(x), would do the same here; missing() is the tool when “not given” must mean something no single default can express.

A typo passed through ...

Any name passed through ... is accepted, so na.mr = TRUE gives a wrong answer instead of an error. Name the arguments you pass on, say in the help of your function where ... goes, and test the calls you document.

Checkpoint

Check: The dots argument and missing()

  1. Question 1 of 2 What does k(1, 2, b = 3) return?

    Read the code, then choose one answer.

    k <- function(x, ...) ...length()
    Show the answer: Question 1 of 2

    Answer: 2

    ...length() counts what ... holds; ...names() would give "" and "b".

  2. Question 2 of 2 What does this return?

    Read the code, then choose one answer.

    paste("x", "y", se = "+")
    Show the answer: Question 2 of 2

    Answer: "x y +"

    The R Language Definition: partial matching applies only to the formals that come before ....

Arguments are computed lazily, when first used

f(1) returns 4, where function(x, y = x * 2) seems to promise 2. The R Language Definition calls this lazy evaluation: R's rule that a function's argument is computed only when the body first uses it, not when the function is called, so an argument that is never used is never computed.: arguments are not evaluated until they are needed. Each argument waits as a promise: What R passes in place of an argument until it is used: the argument's expression and the environment to compute it in. The first use computes it, and later uses reuse the value., the expression and the environment to compute it in, and a default is computed inside the function the first time the body uses it. In f(), the body changes x to 2 before it reads y, so x * 2 is 4. Step through it:

Interactive · step by step Arguments wait until they are used Step through f(1). The left column is the body, line by line; the right column is what x and y hold at that moment.
Arguments wait until they are used, step 6 of 6. Showing f(1) (twice(noisy()): returns 6), x <- x + 1, y, x (2), y (4). In focus: f(1).f(1)twice(noisy()): returns 6x <- x + 1ythe last linex2y4

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. The call. f(1) does not compute its arguments. x is a promise of the expression 1, and y, with no value supplied, a promise of its default, x * 2.
  2. x is needed. The first line reads x, so R computes the promise, 1, and then assigns x + 1. x is now 2.
  3. y is needed. The last line reads y, so R computes its default now, x * 2, inside the function, where x is already 2.
  4. The value. y is 4, and f(1) returns 4, not the 2 that x * 2 would give with the x of the call.
  5. Never needed, never computed. In g(5, stop("this never runs")), the body never reads the second argument, so its promise is never computed and stop() never runs.
  6. Computed once. In twice(noisy()), v + v reads v twice, but "computing the argument" printed once. A promise is computed the first time and then keeps its value.

The run shows the four consequences:

lazy.R, typed at the R console Python · lazy_session.py
from session import run, version

version()
# R reads lazy.R as if each line were typed at the console.
run("""
R --vanilla --quiet -f lazy.R
""")

Output

# R version 4.6.1 -- "Happy Hop"
$ R --vanilla --quiet -f lazy.R
> # A default is computed when the argument is first used, inside the function.
> f <- function(x, y = x * 2) {
+   x <- x + 1
+   y
+ }
> f(1)
[1] 4
> # So a default may use a variable the body creates first.
> with_tax <- function(amount, rate = standard_rate) {
+   standard_rate <- 0.18
+   amount * (1 + rate)
+ }
> with_tax(100)
[1] 118
> with_tax(100, rate = 0.05)
[1] 105
> # An argument the body never uses is never computed.
> g <- function(x, unused) x * 10
> g(5, stop("this never runs"))
[1] 50
> # An argument is computed once, then its value is reused.
> noisy <- function() {
+   cat("computing the argument\n")
+   3
+ }
> twice <- function(v) v + v
> twice(noisy())
computing the argument
[1] 6
> # force() computes an argument at once.
> h <- function(x) {
+   force(x)
+   "not reached"
+ }
> try(h(stop("computed by force()")))
Error in force(x) : computed by force()

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 lazy_session.py

  • A default can use what the body creates. with_tax(100) used standard_rate, a variable the body sets before it reads rate, and returned 118.
  • An argument the body never uses is never computed, so g(5, stop("this never runs")) returned 50.
  • A promise is computed once. twice(noisy()) read v twice, and “computing the argument” printed once.
  • force() computes an argument at once. In h(), the error came from force(x) on the first line, not later.

Laziness rarely matters for arguments a function uses straight away. It matters when a function creates another function that keeps an argument for later, which is the subject of the next lesson, and when an argument’s expression has a side effect, such as printing or reading a file, that the caller expects to happen.

Key idea

R computes an argument the first time the body uses it, in the function’s own environment, and only once; call force() when it must be computed at once.

Checkpoint

Check: Lazy evaluation and promises

  1. Question 1 of 2 What does f(1) return?

    Read the code, then choose one answer.

    f <- function(a, b = a * 10) {
      a <- 5
      b
    }
    Show the answer: Question 1 of 2

    Answer: 50

    Defaults are promises computed in the function's own environment at their first use.

  2. Question 2 of 2 g <- function(x, y) x. What does g(1, stop("no")) do?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: It returns 1

    The run showed the same with g(5, stop("this never runs")), which returned 50.

Practise: predict, then check

Work out each result on paper first; the run below is the answer key.

Your predictions

Predict add(2) and add(2, 10) for add <- function(a, b = a + 1) a + b, then pick() and pick("h") for pick <- function(type = c("low", "high")) match.arg(type), and k(1, 2, 3) for k <- function(x, ...) ...length(). Then type the lines into R.

practice.R, typed at the R console Python · practice_session.py
from session import run, version

version()
# R reads practice.R as if each line were typed at the console.
run("""
R --vanilla --quiet -f practice.R
""")

Output

# R version 4.6.1 -- "Happy Hop"
$ R --vanilla --quiet -f practice.R
> add <- function(a, b = a + 1) a + b
> add(2)
[1] 5
> add(2, 10)
[1] 12
> pick <- function(type = c("low", "high")) match.arg(type)
> pick()
[1] "low"
> pick("h")
[1] "high"
> k <- function(x, ...) ...length()
> k(1, 2, 3)
[1] 2

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 practice_session.py

The default b = a + 1 made add(2) 2 + 3, and a given b replaced it; match.arg() returned the first choice when none was given and expanded "h" to "high"; and x took the 1, so ... held two arguments.

Finish with the adaptive questions. They begin with your weakest concept of this lesson, and every choice says why it is right or what it misses:

Practice that adapts to you

Practise this lesson

15 questions about 5 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 15 What does invisible(x) at the end of a function do?

    Choose one answer.

    Show the answer: Question 1 of 15

    Answer: It returns x, but the console does not print it unless the call is assigned or wrapped in print()

    Use it for functions whose main job is a side effect, such as writing a file.

  2. Question 2 of 15 A function must give back a mean and a standard deviation. Which return is right?

    Choose one answer.

    Show the answer: Question 2 of 15

    Answer: c(mean = m, sd = s), or list(mean = m, sd = s)

    describe() in the run returned c(n = 3, mean = 7, max = 9), a named vector.

  3. Question 3 of 15 What does the last line print?

    Read the code, then choose one answer.

    v <- c(1, 2)
    f <- function(v) {
      v[1] <- 99
      v
    }
    invisible(f(v))
    v
    Show the answer: Question 3 of 15

    Answer: 1 2

    double_it(marks) in the run left marks as 1 2 3.

  4. Question 4 of 15 fit <- function(data, weights = NULL, trim = 0). What does fit(1:5, colour = "red") do?

    Choose one answer.

    Show the answer: Question 4 of 15

    Answer: It stops with an "unused argument" error that names colour

    An error for an unknown name is a feature: it is exactly what ... loses.

  5. Question 5 of 15 f <- function(value, verbose = FALSE) value. What does f(v = 1) do?

    Choose one answer.

    Show the answer: Question 5 of 15

    Answer: It stops with "argument 1 matches multiple formal arguments"

    Spell argument names out in code you keep.

  6. Question 6 of 15 Your script calls mean(x, na = TRUE). Why change it to na.rm = TRUE?

    Choose one answer.

    Show the answer: Question 6 of 15

    Answer: A later version could add an argument starting with na, and the short name would then be ambiguous

    options(warnPartialMatchArgs = TRUE) makes R warn whenever it matches a name partially, which finds such calls.

  7. Question 7 of 15 summarise_vec(marks) gives no type. What does match.arg(type) return inside it?

    Choose one answer.

    Show the answer: Question 7 of 15

    Answer: "t", the first choice of the default c("t", "normal")

    The run's first call used the t interval for this reason.

  8. Question 8 of 15 x <- NA. What does stopifnot("x must be positive" = x > 0) do?

    Choose one answer.

    Show the answer: Question 8 of 15

    Answer: It stops with "x must be positive"

    The run's level = NA stopped at the third check the same way.

  9. Question 9 of 15 A user asks for stats = c("mean", "sdd"). Which check reports the typo?

    Choose one answer.

    Show the answer: Question 9 of 15

    Answer: match.arg(stats, choices, several.ok = "all"), new in R 4.6.0

    The run printed "mean" for several.ok = TRUE and an error for several.ok = "all".

  10. Question 10 of 15 share_of <- function(x, total) ... is called as share_of(c(2, 3, 5)). What is missing(total)?

    Choose one answer.

    Show the answer: Question 10 of 15

    Answer: TRUE

    share_of() then set total to sum(x) and returned 0.2 0.3 0.5.

  11. Question 11 of 15 avg <- function(x, ...) mean(x, ...). What does avg(c(1, NA, 3), na.mr = TRUE) return?

    Choose one answer.

    Show the answer: Question 11 of 15

    Answer: NA, with no error and no warning

    A name passed through ... is not checked anywhere, which is the cost of passing arguments on.

  12. Question 12 of 15 Your function takes ... to fit a generic but uses none of it. How do you stop typos from vanishing?

    Choose one answer.

    Show the answer: Question 12 of 15

    Answer: Call chkDots(...) at the top, so extra arguments give a warning

    chkDots() is for functions that accept ... without using it; a function that passes ... on should name the arguments it forwards instead.

  13. Question 13 of 15 What does force(x) do?

    Choose one answer.

    Show the answer: Question 13 of 15

    Answer: It computes the argument x at once, instead of at its first use

    force() matters most when a function keeps an argument for later, as the next lesson shows.

  14. Question 14 of 15 twice <- function(v) v + v, and noisy() prints a line before returning 3. How many times does the line print for twice(noisy())?

    Type a number.

    A hint

    A promise keeps its value once it is computed.

    Show the answer: Question 14 of 15

    Answer: 1 times

    The first use of v computes noisy(), and the second reuses the value; the run printed the line once and 6.

  15. Question 15 of 15 wt <- function(amount, rate = standard_rate) amount * (1 + rate) has no standard_rate anywhere. What does wt(100) do?

    Choose one answer.

    Show the answer: Question 15 of 15

    Answer: It stops with "object 'standard_rate' not found"

    with_tax() worked because its body created standard_rate before it read rate.

Interview questions

Warm-up: what does lazy evaluation of arguments mean in R?

When a function is called, R does not compute its arguments first. Each argument is passed as a promise, its expression together with the environment it comes from, and the promise is computed the first time the function’s body uses the argument; after that its value is reused. A default is computed in the same way, but inside the function, so it can refer to other arguments or to variables the body creates before it reads the argument. Three consequences follow: an argument that is never used is never computed, so f(5, stop("x")) can succeed; a default such as y = x * 2 sees x as it is when y is first read; and an argument’s side effects happen at that moment, not at the call. force(x) computes an argument at once.

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. Mid to senior

    Your team's package exports summarise(x, ...), which passes ... to mean(). A user reports that summarise(x, na.mr = TRUE) returned NA without any message. Explain why, and how you would change the function so that this kind of mistake is reported.

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Fresher to mid

    You are writing describe(x, type = c("classic", "robust"), digits = 2) for your team's shared code. Which checks go at the top, what should each error say, and how do you test the checks themselves?

    Model answer, Pro. First step and follow-ups

Key takeaways

  • A function returns the value of its last expression; return() leaves early, several results come back as one named vector or list, invisible() returns without printing, and arguments are local copies.
  • R matches arguments by exact name, then by the start of a name, then by position, and unsupplied ones take their defaults; a start that fits two names, or a name that fits none, stops the call.
  • Check inputs in the first lines with stopifnot(), whose argument names become the messages, and choices with match.arg(), which expands abbreviations and lists the valid choices when one is wrong.
  • ... passes any argument on without checking it, so a misspelt name can be swallowed; arguments after ... must be named in full, and missing() tells whether an argument was given.
  • Arguments are promises, computed in the function the first time they are used and only once, so a default sees the body’s changes and an unused argument is never computed.

Terms in this lesson

...
The dots argument of a function: it collects every argument that matches no other name, so the function can pass them on, as mean(x, ...) does. ...length() counts them and ...names() names them.
default
The value written for an argument in a function's definition, as in digits = 2, used when a call gives none. R computes it inside the function, when the argument is first used.
lazy evaluation
R's rule that a function's argument is computed only when the body first uses it, not when the function is called, so an argument that is never used is never computed.
promise
What R passes in place of an argument until it is used: the argument's expression and the environment to compute it in. The first use computes it, and later uses reuse the value.

References

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.