Overview

  • Documenting functions with roxygen2
  • NAMESPACE: exporting functions
  • Dependencies
  • NAMESPACE: importing functions

Function documentation with roxygen2

roxygen2

The roxygen2 package generates documentation from specially formatted comments.

comments -> .Rd files -> HTML

This powers R’s help system, e.g. ?function and package websites.

roxygen2 8.0.0 was released May 2026, with over 100 improvements and bug fixes.

This blog post outlines the major changes.

packageVersion("roxygen2")
[1] '8.0.0'

roxygen2 comments

We write comments above the function code, e.g.

#' @param x A numeric vector.
  • #' is a roxygen comment.

  • @param is a roxygen tag.

  • The @param tag takes an argument: the name of the parameter

  • The remaining text (until the next tag in the file) is the documentation relevant to the tag.

Common tags

There are four tags you’ll use for most functions:


Tag Purpose
@param arg Describe inputs
@examples Show how the function works
@return Describe the return value (not needed if NULL)
@export Add this tag if the function should be user-visible


Usual RStudio shortcuts work in the @examples section, allowing you to run code interactively.

The description block

The roxygen comment should start with a description block.

  • First sentence is the title.
  • Next paragraph is the description.
  • Everything else is the details (optional).
#' Title in Title Case of up to 65 Characters
#'
#' Mandatory description of what the function does. 
#' Should be a short paragraph of a few lines only.
#'
#' The details section is optional and may be several paragraphs. It can even
#' contain sub-sections (not illustrated here).

RStudio helps you get started

Put your cursor inside a function, then select ‘Insert Roxygen Skeleton’ from the Code menu.

#' Title
#'
#' @param animal
#' @param sound
#'
#' @return
#' @export
#'
#' @examples
animal_sounds <- function(animal, sound) {
  stopifnot(is.character(animal) & length(animal) == 1)
  stopifnot(is.character(sound) & length(sound) == 1)
  paste0("The ", animal, " goes ", sound, "!")
}

Example roxygen documentation

#' Sort a Numeric Vector in Decreasing Order
#'
#' Sort a numeric vector so that the values are in deceasing order.  
#' Missing values are optionally removed or put last.
#'
#' @param x A numeric vector.
#' @param na.rm A logical value indicating whether to remove missing values
#' before sorting.
#' @return A vector with the values sorted in descreasing order.
#' @export
#'
#' @examples
#' x <- c(3, 7, 2, NA)
#' high_to_low(x)
#' high_to_low(x, na.rm = TRUE)

R documentation file

roxygen2 converts the roxygen block to an .Rd file in the /man directory

% Generated by roxygen2: do not edit by hand
% Please edit documentation in R/high_to_low.R
\name{high_to_low}
\alias{high_to_low}
\title{Sort a Numeric Vector in Decreasing Order}
\usage{
high_to_low(x, na.rm = FALSE)
}
\arguments{
\item{x}{A numeric vector.}
\item{na.rm}{A logical value indicating whether to remove missing values
before sorting.}
}
\value{
...

HTML file

When the package is installed, the .Rd is converted by R to HTML on demand

Regular documentation workflow

You must have loaded the package with load_all() at least once.

NAMESPACE: exports

A namespace splits functions into two classes


Internal External
Only for use within package For use by others
Documentation optional Must be documented
Easily changed Changing could break other people’s code

Default NAMESPACE

  • It is best to export functions explicitly
  • The NAMESPACE file as created by usethis::create_package() does not export anything by default.

Warning

A package created from the RStudio menus via File > New Project > New Directory > R Package creates a NAMESPACE that exports everything by default, with exportPattern("^[[:alpha:]]+")

This is a good reason not to do this: always call usethis::create_package() to create a package.

For similar reasons, also avoid package.skeleton().

Exporting functions

#' @export
fun1 <- function(...) {}

When we call devtools::document(), an export() directive will be added to NAMESPACE for each function that has an #' @export comment.

# Generated by roxygen2: do not edit by hand

export(fun1)

What to export

Only export functions that you want your package users to use, i.e. those that are relevant to the purpose of the package.

Don’t export internal helpers, e.g.

# Defaults for NULL values
`%||%` <- function(a, b) if (is.null(a)) b else a

# Remove NULLs from a list
compact <- function(x) {
  x[!vapply(x, is.null, logical(1))]
}

Note

Using the ‘Insert Roxygen Skeleton’ option adds an @export tag.

Your turn

For the animal_sounds function:

  1. Insert a Roxygen skeleton using the RStudio helper.
  2. Create a draft documentation file with devtools::document() or Cmd/Ctrl + Shift + D.
  3. Click on “Diff” in the Git pane and view the changes that have been made.
  4. Preview the HTML help with ?animal_sounds.
  5. Fill in the Roxygen skeleton for animal_sounds(), recreating the documentation file and previewing the HTML help to view your updates.
  6. When you have finished editing, run devtools::document() to ensure the .Rd file is in sync. Make a git commit with your updated R/animal_sounds.R file, the updated NAMESPACE, and the new man/animal_sounds.Rd file.

.Rd Markup

.Rd files recognise LaTeX-like mark-up in most text-based fields, e.g.

#' This is a convenience function that is a wrapper around
#' \code{\link{sort.int}}.

Details can be found in the Writing R documentation files section of the Writing R Extensions manual.

Using markdown

Most commonly used mark-up is easier with markdown (and can be mixed with .Rd mark-up).

  • Text formatting: **bold**, _italic_, `code`

  • Create links

    • To a function in the same package: [func()]
    • To a function in a different package: [pkg::func()]
    • With different link text, e.g. [link text][func()]

For more details, see the (R)Markdown support vignette.

Your turn

  1. Add some details to the help page for animal_sounds(), with a link to paste0() and some markdown syntax.
  2. Add a link to a function from a package you don’t have installed (perhaps grumpy::read_npy()).
  3. Run devtools::document() and check the link in the help page. What happens?
  4. Run devtools::check(). Does the link cause problems?
  5. Delete the link to the package you don’t have installed and run devtools::document() again.
  6. Commit all your changes to the git repo.

Dependencies

No library() calls!

We cannot use library() calls in R packages.

We need another way to access functions in other packages, and to ensure that users of our package have those other packages installed on their system too.

To do this, we can take dependencies on other packages.

Dependencies

Dependencies are other R packages that our package uses. There are three types of dependency:

Imports: required packages, will be installed when our package is installed if they are not already installed.

Suggests: optional packages, e.g. only used for development; only used in documentation. Not installed automatically with our package.

Depends: essentially deprecated for packages, may be used to specify a minimum required version of R (i.e., version of the core packages).

Imported packages

In DESCRIPTION

Imports: 
    pkgname1
    pkgname2

Use :: to access functions

new_function <- function(x, y, z) {
  w <- pkgname1::imported_function(x, y)
  pkgname2::imported_function(w, z)
}

Suggested packages

In DESCRIPTION

Suggests: 
    pkgname

In package functions or examples, handle the case where pkgname is not available:

if (!requireNamespace("pkgname", quietly = TRUE)){
  warning("pkgname must be installed to perform this function",
          "returning NULL")
  return(NULL)
}

use_package()

use_package() will modify the DESCRIPTION and remind you how to use the function.

By default, packages will be added to “Imports”.

usethis::use_package("cli")
✔ Adding cli to Imports field in DESCRIPTION.
☐ Refer to functions with `cli::fun()`.
usethis::use_package("withr", type = "Suggests")
✔ Adding withr to Suggests field in DESCRIPTION.
☐ Use `requireNamespace("withr", quietly = TRUE)` to test if
  withr is installed.
☐ Then directly refer to functions with `withr::fun()`.

NAMESPACE: imports

Accessing functions in other packages

Once we have taken a dependency on a package, we have a choice about how to access the functions in that package:

  • directly, with pkgname::function()
  • importing the function, then just calling function()

Advantages of pkgname::function()

  • Makes it really clear to you, and other readers of your code, where functions have come from.
  • Requires no further setup
  • Necessary to dissambiguate when have two functions with the same name in two different packages

Advantages of importing a function

  • Don’t need to keep typing pkgname:: - this can get tedious and make code much more noisy
  • Code is slightly more efficient to run (though only by about 2 microseconds)
    • Difference will only be noticeable if running 100,000+ calls in a tight loop
  • Some packages already have function names that make it clear where they are from, e.g. stringr with str_*()
  • Necessary for infix operators, e.g. %>%
    • N.B. recommendation is now to use native pipe |> in packages instead.

Largely a matter of personal preference. A hybrid approach is also fine!

Aside: %>% vs |>

The native pipe, |>, has been available in R since 4.1.0, and we now prefer this to importing the %>% pipe from magrittr.

There are then two options:

  • Depend on R >= 4.1.0, with this in DESCRIPTION:
Depends:
    R (>= 4.1.0)

See slides 28-31 here for more about the pipes, and this blog post.

Importing functions into the package manually

#' @importFrom purrr keep modify

col_summary <- function(df, fun) {
  stopifnot(is.data.frame(df))

  df |>
    keep(is.numeric) |>
    modify(fun)
}

devtools::document() will add corresponding import() statements to the NAMESPACE, e.g. import(purr, keep, modify).

Here, the @importFrom tag is placed above the function in which the imported function is used.

Package-level import file, manually

Imports belong to the package, not to individual functions, so alternatively you can recognise this by storing them in a central location, e.g. R/animalsounds-package.R

#' @importFrom purrr keep modify
#' @importFrom magrittr %>%
NULL

This removes the possibility of multiple (redundant) imports of the same function. But harder to remember to remove import if function changes! It’s a matter of personal taste.

usethis::use_import_from()

There can be several steps to importing a function. usethis::use_import_from() takes care of all of them.

This is a (preferred) alternative to writing @importFrom tags manually.

It will first create the package documentation file R/animalsounds-package.R (if it doesn’t already exist – you will also need to agree to this).

usethis::use_import_from("purrr", c("keep", "modify"))
✔ Adding 'purrr' to Imports field in DESCRIPTION
✔ Adding '@importFrom purrr keep', '@importFrom purrr modify' to 'R/animalsounds-package.R'
✔ Writing 'NAMESPACE'
✔ Loading animalsounds

You can import all functions from a package…

It may be tempting to import all the functions from a package:

#' @import purrr
col_summary <- function(df, fun) {
  stopifnot(is.data.frame(df))

  df |>
    keep(is.numeric) |>
    map_dfc(fun)
}

… but don’t!

It is dangerous:

#' @import pkg1
#' @import pkg2
fun <- function(x) {
  fun1(x) + fun2(x)
}

Works today…

… but next year, what if pkg2 adds a fun1 function?

And not clear:

You lose direct evidence in your package of which functions come from which packages.

Metapackages

It is bad practice to import “metapackages” (i.e. packages that are collections of packages), such as tidyverse.

See the blog post https://www.tidyverse.org/blog/2018/06/tidyverse-not-for-packages/ for more details.

Documenting dependencies


DESCRIPTION NAMESPACE
Makes package available Makes function available
Mandatory Optional (can use :: instead)
use_package() use_import_from()

Example: rlang and cli

Currently we are using stopifnot() for argument validation

stopifnot(is.character(animal) & length(animal) == 1)
stopifnot(is.character(sound) & length(sound) == 1)

We might instead use rlang::is_character() with cli::cli_abort()

sound <- c("woof", "bark")

if (!rlang::is_character(sound, n = 1)) {
  cli::cli_abort("`sound` must be a single string!")
}
Error:
! `sound` must be a single string!

Aside: informative messages with cli

cli functions can combine glue interpolation and inline classes to produce informative, nicely-formatted error messages.

In animal_sounds() we can use

cli::cli_abort(
  c("{.var animal} must be a single string!",
    "i" = "It was {.type {animal}} of length {length(animal)} instead.")
)

This gives the error message

animal_sounds(c("dog", "cat"), c("woof", "miaow"))
Error in `animal_sounds()`:
! `animal` must be a single string!
ℹ It was a character vector of length 2 instead.

Your turn

  1. Use use_package() to add rlang and cli to Imports.
  2. Update animal_sounds() to use is_character() to check the arguments and cli_abort to throw an informative error if necessary, using :: to fully qualify the function calls.
  3. Load all and try giving animal_sounds() invalid inputs for animal and/or sound.
  4. Commit your changes to git.
  5. Push your commits for this session.

Best practice: helper functions

We’ve essentially repeated code to check the animal and sound arguments. Better practice is to use a helper function.

Try the following:

check_arg <- function(arg, n = 1) {
  if (!rlang::is_character(arg, n = n)) {
    cli::cli_abort(
      c("{.var arg} must be a single string!",
        "i" = "It was {.type {arg}} of length {length(arg)} instead.")
    )
  }
}

Improvement 1: actual arg name

check_arg <- function(arg, n = 1) {
  if (!rlang::is_character(arg, n = n)) {
    cli::cli_abort(
      c(
        "{.var {rlang::caller_arg(arg)}} must be a single string!",
        "i" = "It was {.type {arg}} of length {length(arg)} instead."
      )
    )
  }
}

Improvement 2: calling function

check_arg <- function(arg, n = 1) {
  if (!rlang::is_character(arg, n = n)) {
    cli::cli_abort(
      c(
        "{.var {rlang::caller_arg(arg)}} must be a single string!",
        "i" = "It was {.type {arg}} of length {length(arg)} instead."
      ),
      call = rlang::caller_env()
    )
  }
}

Improvement 3: incorporating n

check_arg <- function(arg, n = 1) {
  if (!rlang::is_character(arg, n = n)) {
    cli::cli_abort(
      c(
        "{.var {rlang::caller_arg(arg)}} must be a character vector of length {n}!",
        "i" = "It was {.type {arg}} of length {length(arg)} instead."
      ),
      call = rlang::caller_env()
    )
  }
}

Minute cards

End matter

References

Wickham, H and Bryan, J, R Packages (2nd edn, in progress), https://r-pkgs.org.

R Core Team, Writing R Extensions, https://cran.r-project.org/doc/manuals/r-release/R-exts.html

License

Licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License (CC BY-NC-SA 4.0).