Function documentation and dependencies

R package development workshop

Heather Turner and Ella Kaye
Department of Statistics

June 26, 2024

Overview

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

Function documentation with roxygen2

roxygen2

The roxygen2 package generates documentation from specially formatted comments, that we write 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 will 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 basemodels::dummy_classifier()).
  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

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("rlang")
usethis::use_package("glue", type = "Suggests")

NAMESPACE: imports

You might get tired of using :: all the time

Or you might want to use an infix function

`%>%` <- magittr::`%>%`

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

  df %>%
    purrr::keep(is.numeric) %>%
    purrr::modify(fun)
}

You can import functions into the package

#' @importFrom purrr keep modify
#' @importFrom magrittr %>%
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).

Adding formal imports is slightly more efficient than using ::.

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

Package-level import file

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

usethis::use_import_from()

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

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

It may be tempting to import a whole package…

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

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

…but 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?

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.

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).