16  Creating R Packages

16.1 Learning Objectives

In this lesson, you will learn:

  • The advantages of using R packages for organizing code
  • Simple techniques for creating R packages
  • Approaches to documenting code in packages

16.2 Why packages?

Most R users are familiar with loading and utilizing packages in their work. And they know how rich CRAN is in providing for many conceivable needs. Most people have never created a package for their own work, and most think the process is too complicated. Really it’s pretty straighforward and super useful in your personal work. Creating packages serves two main use cases:

  • Mechanism to redistribute reusable code (even if just for yourself)
  • Mechanism to reproducibly document analysis and models and their results

Even if you don’t plan on writing a package with such broad appeal such as, say, ggplot2 or dplyr, you still might consider creating a package to contain:

  • Useful utility functions you write i.e. a Personal Package. Having a place to put these functions makes it much easier to find and use them later.
  • A set of shared routines for your lab or research group, making it easier to remain consistent within your team and also to save time.
  • The analysis accompanying a thesis or manuscript, making it all that much easier for others to reproduce your results.

The usethis, devtools and roxygen2 packages make creating and maintining a package to be a straightforward experience.

16.3 Install and load packages

16.4 Create a basic package

Thanks to the great usethis package, it only takes one function call to create the skeleton of an R package using create_package(). Which eliminates pretty much all reasons for procrastination. To create a package called mytools, all you do is:

usethis::create_package("~/mytools")
✔ Setting active project to '/Users/jones/development/mytools'
✔ Creating 'R/'
✔ Creating 'man/'
✔ Writing 'DESCRIPTION'
✔ Writing 'NAMESPACE'
✔ Writing 'mytools.Rproj'
✔ Adding '.Rproj.user' to '.gitignore'
✔ Adding '^mytools\\.Rproj$', '^\\.Rproj\\.user$' to '.Rbuildignore'
✔ Opening new project 'mytools' in RStudio

Note that this will open a new project (mytools) and a new session in RStudio server.

The create_package function created a top-level directory structure, including a number of critical files under the standard R package structure. The most important of which is the DESCRIPTION file, which provides metadata about your package. Edit the DESCRIPTION file to provide reasonable values for each of the fields, including your own contact information.

Information about choosing a LICENSE is provided in the Extending R documentation. The DESCRIPTION file expects the license to be chose from a predefined list, but you can use it’s various utility methods for setting a specific license file, such as the MIT license or the Apache 2 license:

✔ Setting License field in DESCRIPTION to 'Apache License (>= 2.0)'
✔ Writing 'LICENSE.md'
✔ Adding '^LICENSE\\.md$' to '.Rbuildignore'

Once your license has been chosen, and you’ve edited your DESCRIPTION file with your contact information, a title, and a description, it will look like this:

Package: mytools
Title: Utility Functions Created by Matt Jones
Version: 0.1
Authors@R: "Matthew Jones <jones@nceas.ucsb.edu> [aut, cre]"
Description: Package mytools contains a suite of utility functions useful whenever I need stuff to get done.
Depends: R (>= 3.5.0)
License: Apache License (>= 2.0)
LazyData: true

16.5 Add your code

The skeleton package created contains a directory R which should contain your source files. Add your functions and classes in files to this directory, attempting to choose names that don’t conflict with existing packages. For example, you might add a file custom_theme that contains a function custom_theme() that you might want to reuse. The usethis::use_r() function will help set up you files in the right places. For example, running:

usethis::use_r("custom_theme")
● Modify 'R/custom_theme'

creates the file R/custom_theme, which you can then modify to add the implementation of the following function from the functions lesson:

custom_theme <- function(base_size = 9) {
    ggplot2::theme(
      axis.ticks       = ggplot2::element_blank(),
      text             = ggplot2::element_text(family = 'Helvetica', color = 'gray30', size = base_size),
      plot.title       = ggplot2::element_text(size = ggplot2::rel(1.25), hjust = 0.5, face = 'bold'),
      panel.background = ggplot2::element_blank(),
      legend.position  = 'right',
      panel.border     = ggplot2::element_blank(),
      panel.grid.minor = ggplot2::element_blank(),
      panel.grid.major = ggplot2::element_line(colour = 'grey90', linewidth = .25),
      legend.key       = ggplot2::element_rect(colour = NA, fill = NA),
      axis.line        = ggplot2::element_blank()
      )
}

If your R code depends on functions from another package, then you must declare so in the Imports list in the DESCRIPTION file for your package. In our example above, we depend on the ggplot2 package, and so we need to list it as a dependency. Once again, usethis provides a handy helper method:

usethis::use_package("ggplot2")
✔ Adding 'ggplot2' to Imports field in DESCRIPTION
● Refer to functions with `devtools::fun()`

16.6 Add documentation

You should provide documentation for each of your functions and classes. This is done in the roxygen2 approach of providing embedded comments in the source code files, which are in turn converted into manual pages and other R documentation artifacts. Be sure to define the overall purpose of the function, and each of its parameters.

#' Set a custom ggplot theme.
#'
#' This function sets ggplot theme elements that I like, with the ability to change
#' the base size of the text.
#'
#' @param base_size Base size of plot text
#'
#' @keywords plotting
#'
#' @export
#'
#' @examples
#' library(ggplot2)
#'
#' ggplot(iris, aes(Sepal.Length, Sepal.Width)) +
#'     geom_point() +
#'     custom_theme(base_size = 10)
#'
custom_theme <- function(base_size = 9) {
    ggplot2::theme(
        axis.ticks       = ggplot2::element_blank(),
        text             = ggplot2::element_text(family = 'Helvetica', color = 'gray30', size = base_size),
        plot.title       = ggplot2::element_text(size = ggplot2::rel(1.25), hjust = 0.5, face = 'bold'),
        panel.background = ggplot2::element_blank(),
        legend.position  = 'right',
        panel.border     = ggplot2::element_blank(),
        panel.grid.minor = ggplot2::element_blank(),
        panel.grid.major = ggplot2::element_line(colour = 'grey90', size = .25),
        legend.key       = ggplot2::element_rect(colour = NA, fill = NA),
        axis.line        = ggplot2::element_blank()
    )
}

Once your files are documented, you can then process the documentation using the document() function to generate the appropriate .Rd files that your package needs.

devtools::document()
Updating mytools documentation
Updating roxygen version in /Users/jones/development/mytools/DESCRIPTION
Writing NAMESPACE
Loading mytools
Writing NAMESPACE
Writing custom_theme.Rd

That’s really it. You now have a package that you can check() and install() and release(). See below for these helper utilities.

16.7 Test your package

You can test your code using the tetsthat testing framework. The ussethis::use_testthat() function will set up your package for testing, and then you can use the use_test() function to setup individual test files. For example, in the functions lesson we created some tests for our fahr_to_celsius functions but ran them line by line in the console.

First, lets add that function to our package. Run the use_r function in the console:

usethis::use_r("fahr_to_celsius")

Then copy the function and documentation into the R script that opens and save the file.

#' Convert temperature data from Fahrenheit to Celsius
#'
#' @param fahr Temperature data in degrees Fahrenheit to be converted
#' @return temperature value in degrees Celsius
#' @keywords conversion
#' @export
#' @examples
#' fahr_to_celsius(32)
#' fahr_to_celsius(c(32, 212, 72))
fahr_to_celsius <- function(fahr) {
  celsius <- (fahr-32)*5/9
  return(celsius)
}

Now, set up your package for testing:

usethis::use_testthat()
✔ Adding 'testthat' to Suggests field in DESCRIPTION
✔ Creating 'tests/testthat/'
✔ Writing 'tests/testthat.R'

Then write a test for fahr_to_celsius:

usethis::use_test("fahr_to_celsius")
✔ Writing 'tests/testthat/test-fahr_to_celsius.R'
● Modify 'tests/testthat/test-fahr_to_celsius.R'

You can now add tests to the test-fahr_to_celsius.R, and you can run all of the tests using devtools::test(). For example, if you add a test to the test-fahr_to_celsius.R file:

test_that("fahr_to_celsius works", {
  expect_equal(fahr_to_celsius(32), 0)
  expect_equal(fahr_to_celsius(212), 100)
})

Then you can run the tests to be sure all of your functions are working using devtools::test():

devtools::test()
Loading mytools
Testing mytools
✔ | OK F W S | Context
✔ |  2       | test-fahr_to_celsius [0.1 s]

══ Results ════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════
Duration: 0.1 s

OK:       2
Failed:   0
Warnings: 0
Skipped:  0

Yay, all tests passed!

16.8 Checking and installing your package

Now that your package is built, you can check it for consistency and completeness using check(), and then you can install it locally using install(), which needs to be run from the parent directory of your module.

devtools::check()
devtools::install()

Your package is now available for use in your local environment.

16.9 Sharing and releasing your package

  • GitHub: The simplest way to share your package with others is to upload it to a GitHub repository, which allows others to install your package using the install_github('mytools','github_username') function from devtools.

  • CRAN: If your package might be broadly useful, also consider releasing it to CRAN, using the release() method from devtools(). Releasing a package to CRAN requires a significant amount of work to ensure it follows the standards set by the R community, but it is entirely tractable and a valuable contribution to the science community. If you are considering releasing a package more broadly, you may find that the supportive community at ROpenSci provides incredible help and valuable feeback through their onboarding process.

  • R-Universe: A newer approach is to link your package release to R-Universe, which is an effective way to make it easy to test and maintain packages so that many people can install them using the familiar install.pacakges() function in R. In R-Universe, people and organizations can create their own universe of packages, which represent a collection of packages that appear as a CRAN-compatible repository in R. For example, in DataONE we maintaine the DataONE R-Universe (https://dataoneorg.r-universe.dev), which lists the packages we actively maintain as an organization. So, any R-user that wants to install these packages can do so by adding our universe to their list of repositories, and then installing packages as normal. For example, to install the codyn package, one could use:

install.packages('codyn', repos = c('https://dataoneorg.r-universe.dev', 'https://cloud.r-project.org'))

Challenge

Add the other temperature conversion functions with full documentation to your package, write tests to ensure the functions work properly, and then document(), check(), and install() the new version of the package. Don’t forget to update the version number before you install!

16.10 More reading