πŸ’» 🏦
Code You Can Bank On

Package development

Amelia McNamara

Motivation

In the R world, versions of this are attributed to Hadley Wickham:

Any time you copy-and-paste code three times, write a function.

Any time you copy-and-paste a function three times, write a package.

Also known as the rule of three: popularized by Martin Fowler (1999), attributed to Don Roberts

Why make a package?

Why make a package

  • makes it easier to reuse functions you write
  • have a consistent framework which encourages you to better organise, document and test, your code
  • using this consistent framework means you can use many standardised tools
  • is the easiest way to distribute code and data

Be nice to future you

In every project you have at least one other collaborator; future-you. You don’t want future-you to curse past-you

To avoid

via GIPHY

Script vs package

Script

  • one-off data analysis
  • defined by .R extension
  • library() calls
  • documentation in # comments
  • source()

Package

  • defines reusable components
  • defined by presence of DESCRIPTION file
  • Required packages specified in DESCRIPTION, made available in NAMESPACE file
  • documentation in files and Roxygen comments
  • Install and restart

Overview

BLT
β”œβ”€β”€ BLT.Rproj
β”œβ”€β”€ DESCRIPTION
β”œβ”€β”€ NAMESPACE
β”œβ”€β”€ R
β”‚   β”œβ”€β”€ AR1.R
β”‚   β”œβ”€β”€ BLT-package.R
β”‚   β”œβ”€β”€ data.R
β”‚   β”œβ”€β”€ perc_missing.R
β”‚   └── perc_missing_tidy.R
β”œβ”€β”€ data
β”‚   β”œβ”€β”€ bacon.rda
β”‚   β”œβ”€β”€ lettuce.rda
β”‚   └── tomatoes.rda

(Does this remind you of anything?)

Overview

BLT
β”œβ”€β”€ BLT.Rproj
β”œβ”€β”€ DESCRIPTION
β”œβ”€β”€ NAMESPACE
β”œβ”€β”€ R
β”‚   β”œβ”€β”€ AR1.R
β”‚   β”œβ”€β”€ BLT-package.R
β”‚   β”œβ”€β”€ data.R
β”‚   β”œβ”€β”€ perc_missing.R
β”‚   └── perc_missing_tidy.R
β”œβ”€β”€ data
β”‚   β”œβ”€β”€ bacon.rda
β”‚   β”œβ”€β”€ lettuce.rda
β”‚   └── tomatoes.rda

unchanged
changed
changed by you

(Does this remind you of anything?)

Create package

BLT
β”œβ”€β”€ BLT.Rproj
β”œβ”€β”€ DESCRIPTION
β”œβ”€β”€ NAMESPACE
└── R

unchanged
changed
changed by you

Create package (your turn)

Be deliberate about where you create your package

Do not nest inside another RStudio project, R package or git repo.

That includes if you created an RStudio project for these materials!

My practice is to put all packages under my home directory, but your Documents folder is also okay (as long as that’s not synced by OneDrive or other file-tracking software!).

create_package()

What happens when we run create_package()?

  • R will create a folder called BLT which is a package and an RStudio project

  • restart R in the new project

  • create some infrastructure for your package

  • start the RStudio Build pane

create_package()

What happens when we run create_package()?

  • BLT.Rproj

  • DESCRIPTION provides metadata about your package.

  • The R/ directory is where we will put .R files with function definitions.

  • NAMESPACE declares the functions your package exports and the functions your package imports from other packages.

create_package()

What happens when we run create_package()?

  • .Rbuildignore lists files that we need but that should not be included when building the R package from source.

  • .gitignore anticipates Git usage and ignores some standard, behind-the-scenes files created by R and RStudio.

Create package (your turn)

usethis::create_package("../BLT") # set the path to suit you
# getwd() will remind you where you currently are
  • creates a minimal set of files for an installable package

  • opens a new RStudio project

BLT
β”œβ”€β”€ .gitignore
β”œβ”€β”€ .Rbuildignore
β”œβ”€β”€ BLT.Rproj
β”œβ”€β”€ DESCRIPTION
β”œβ”€β”€ NAMESPACE
└── R

unchanged
changed
changed by you

Create framework

BLT
β”œβ”€β”€ .gitignore
β”œβ”€β”€ .Rbuildignore
β”œβ”€β”€ BLT.Rproj
β”œβ”€β”€ DESCRIPTION
β”œβ”€β”€ NAMESPACE
└── R

unchanged
changed
changed by you

Make package a git repo (your turn)

usethis::use_git()
  • creates a git repository in your (package) project

  • makes a first commit on your behalf

  • restarts RStudio

BLT
β”œβ”€β”€ .gitignore
β”œβ”€β”€ .Rbuildignore
β”œβ”€β”€ BLT.Rproj
β”œβ”€β”€ DESCRIPTION
β”œβ”€β”€ NAMESPACE
└── R

unchanged
changed
changed by you

Make package a git repo (your turn)

you will get some messages sort of like this:

usethis::use_git()
βœ” Setting active project to "/Users/amcnamara2/BLT".
βœ” Initialising Git repo.
βœ” Adding ".Rhistory", ".RData", ".httr-oauth", ".DS_Store", and ".quarto" to .gitignore.
β„Ή There are 5 uncommitted files:
β€’ .gitignore
β€’ .Rbuildignore
β€’ BLT.Rproj
β€’ DESCRIPTION
β€’ NAMESPACE
! Is it ok to commit them?

1: No
2: Absolutely not
3: Yup

The specific language and order of items will change over time, to make it harder to make mistakes!

Choose the option that means yes!

Create framework (we’re skipping this)

usethis::use_github()
  • this is the closest you may see to actual magic:

    • creates GitHub repository

    • creates git remote origin, sets to GitHub repository

    • pushes your master branch to git remote

    • adds information to DESCRIPTION file

    • opens your repository page at GitHub

Develop

BLT
β”œβ”€β”€ .gitignore
β”œβ”€β”€ .Rbuildignore
β”œβ”€β”€ BLT.Rproj
β”œβ”€β”€ DESCRIPTION
β”œβ”€β”€ NAMESPACE
β”œβ”€β”€ R
β”‚   └── AR1.R

unchanged
changed
changed by you

Develop (your turn)

There’s a usethis helper for adding .R files!

usethis::use_r("AR1")

In real life, I like the idea of documenting, then writing functions:

  • claim what the function will do, then do it

  • if your explanation is getting heavy, split into more functions

For today, we’re going to push off documentation.

Start by writing a shell for the function:

AR1 <- function(y) {
  
}
BLT
β”œβ”€β”€ .gitignore
β”œβ”€β”€ .Rbuildignore
β”œβ”€β”€ BLT.Rproj
β”œβ”€β”€ DESCRIPTION
β”œβ”€β”€ NAMESPACE
β”œβ”€β”€ R
β”‚   └── AR1.R

unchanged
changed
changed by you

Develop (your turn)

Write the function body

AR1 <- function(y) {
  forecast::Arima(y, order = c(1, 0, 0), include.constant = TRUE)
}
BLT
β”œβ”€β”€ .gitignore
β”œβ”€β”€ .Rbuildignore
β”œβ”€β”€ BLT.Rproj
β”œβ”€β”€ DESCRIPTION
β”œβ”€β”€ NAMESPACE
β”œβ”€β”€ R
β”‚   └── AR1.R

unchanged
changed
changed by you

Develop (your turn)

forecast::Arima(y)

☝️ Use this notation to be explicit about where R looks for the function Arima().


πŸ’Ύ Save AR1.R.

document()
load_all()
check()

πŸ€” What happened?

BLT
β”œβ”€β”€ .gitignore
β”œβ”€β”€ .Rbuildignore
β”œβ”€β”€ BLT.Rproj
β”œβ”€β”€ DESCRIPTION
β”œβ”€β”€ NAMESPACE
β”œβ”€β”€ R
β”‚   └── AR1.R

unchanged
changed
changed by you

Develop (your turn)

❯ checking DESCRIPTION meta-information ... WARNING
  Non-standard license specification:
    `use_mit_license()`, `use_gpl3_license()` or friends to pick a
    license
  Standardizable: FALSE

❯ checking dependencies in R code ... WARNING
  '::' or ':::' import not declared from: β€˜forecast’

❯ checking for future file timestamps ... NOTE
  unable to verify current time

1 error βœ– | 2 warnings βœ– | 1 note βœ–

Package dependencies

Our warnings are because we have used packages that we have not declared officially.


We need to document our package dependencies


Our users, and the package installation machinery, need to know what our package depends on before installing

Package dependencies

Levels of dependency

  • Imports: must be installed for your package to work. If they’re not, they will get installed.
  • Suggests: used by your package, but not required. Might provide data for examples, to run tests, build vignettes.
  • Depends: Avoid where possible. When your package requires a specific version of R, e.g. Depends: R (>= 3.4.0). Think critically: downstream effects on packages that depend on your package.

❯ checking dependencies in R code ... WARNING
  '::' or ':::' imports not declared from:
    'forecast' 'Arima'

Package dependencies

usethis again!

use_package(package, type = "Imports")

  • Type – one of β€œImports”, β€œDepends”, β€œSuggests”, β€œEnhances”, or β€œLinkingTo”
  • The default is β€œImports”

usethis::use_package()

🎬 Use usethis::use_package() to add the forecast package to Imports

usethis::use_package("forecast")


βœ” Adding forecast to Imports field in DESCRIPTION
β€’ Refer to functions with `forecast::fun()`


Look how your DESCRIPTION file changed!

BLT
β”œβ”€β”€ .gitignore
β”œβ”€β”€ .Rbuildignore
β”œβ”€β”€ BLT.Rproj
β”œβ”€β”€ DESCRIPTION
β”œβ”€β”€ NAMESPACE
β”œβ”€β”€ R
β”‚   └── AR1.R

unchanged
changed
changed by you

usethis::use_package()

Note: we get reminded to β€œRefer to functions with forecast::fun()”

That is, we do NOT use library(forecast) to make functions available to our package.

devtools::check()

🎬 Run devtools::check() on your package again

Check

R CMD check is the gold standard for checking that an R package is in full working order.


It is a program that is executed in the shell.


However, devtools has the check() function to allow you to run this without leaving your R session.

🎬 Check your package:

Aside: in case of error

On running devtools::check() you may get an error if you are using a networked drive.

Updating BLT documentation  
Error: The specified file is not readable: path-to\BLT\NAMESPACE


This is covered here and can be fixed.

Aside: in case of error

Save a copy of this file:

fix_for_networked_drives.R

Save it somewhere other than the BLT directory

Open the file from the BLT project session

Run the whole file

You should now find that check() proceeds normally

Commit

Now would be a good time to commit your changes.


Git icon

Jason Long, CC BY 3.0 <https://creativecommons.org/licenses/by/3.0>, via Wikimedia Commons

Experiment

Now that we’ve document()ed, load_all()ed and check()ed, we can try out our AR1() function in the console.

AR1(tomatoes$value)

Develop (again)

Now that we’ve made one function in our package, let’s try making another. I’d like this function to find the percent of a time series that is missing.

There are lots of approaches to this, I wrote a wrapper for imputeTS::statsNA().

Some functions you may find useful as you work:

use_r()
use_package()
document()
load_all()
check()

Develop (again)

perc_missing <- function(ts){
  na_stats <- imputeTS::statsNA(ts, print_only = FALSE)

  return(na_stats$percentage_NAs)
}

Commit

Now would be a good time to commit your changes.


Git icon

Jason Long, CC BY 3.0 <https://creativecommons.org/licenses/by/3.0>, via Wikimedia Commons

Develop (a third time)

Let’s take this up one more notch by making our function take the dataset and variable name as two separate arguments, so we could use it in a tidy pipeline.

Some functions you may find useful as you work:

use_r()
use_package()
document()
load_all()
check()

Develop (a third time)

perc_missing_tidy <- function(dataset, variable){
  var <- substitute(variable)
  var_eval <- eval(var, envir = dataset)
  na_stats <- imputeTS::statsNA(var_eval, print_only = FALSE)
  tibble_perc <- tibble::tibble(perc = readr::parse_number(x = na_stats$percentage_NAs))
  return(tibble_perc)
}

This one gets tricky for a couple of reasons. One is non-standard evaluation.

Non-standard evaluation

If you’re masking specific tidyverse functions, you can get away with { } and passing arguments along with ....

But if we’re doing more general-purpose programming, we need to leverage non-standard evaluation in R.

purpose base rlang rlang (quosure)
capture unevaluated expression quote() expr() quo()
substitute name for value substitute() enexpr() enquo()
evaluate a captured expression eval() eval_tidy() !!

There are many more NSE functions, but I find these three concepts usually get me pretty far.

Develop (a third time)

Could we change my tidy percent missing function so it uses rlang syntax rather than base R?

perc_missing_tidy <- function(dataset, variable){
  var <- substitute(variable)
  var_eval <- eval(var, envir = dataset)
  na_stats <- imputeTS::statsNA(var_eval, print_only = FALSE)
  tibble_perc <- tibble::tibble(perc = readr::parse_number(x = na_stats$percentage_NAs))
  return(tibble_perc)
}

Develop (a third time)

perc_missing_tidy <- function(dataset, variable){
  var <- rlang::enexpr(variable)
  var_eval <- rlang::eval_tidy(var, data = dataset)
  na_stats <- imputeTS::statsNA(var_eval, print_only = FALSE)
  tibble_perc <- tibble::tibble(perc = readr::parse_number(x = na_stats$percentage_NAs))
  return(tibble_perc)
}

I believe enquo() would work in place of enexpr(), as well.

Commit

Now would be a good time to commit your changes.


Git icon

Jason Long, CC BY 3.0 <https://creativecommons.org/licenses/by/3.0>, via Wikimedia Commons

Adding data to packages

Many packages come with example datasets bundled with them. There are many reasons for this, but the most salient is that if you include your own dataset you know it has exactly the features you need for examples and testing.

The most common location for package data is data/. We recommend that each file in this directory be an .rda file created by save() containing a single R object, with the same name as the file.

Of course, usethis has a helper function for this, usethis::use_data().

Adding data to packages (your turn)

Add the bacon data to the package,

usethis::use_data(bacon)
βœ” Adding R to Depends field in DESCRIPTION.
βœ” Creating data/.
βœ” Setting LazyData to "true" in DESCRIPTION.
βœ” Saving "bacon" to "data/bacon.rda".
☐ Document your data (see <https://r-pkgs.org/data.html>).
BLT
β”œβ”€β”€ BLT.Rproj
β”œβ”€β”€ DESCRIPTION
β”œβ”€β”€ NAMESPACE
β”œβ”€β”€ R
β”‚   β”œβ”€β”€ AR1.R
β”‚   └── BLT-package.R
β”œβ”€β”€ data
β”‚   └── bacon.rda

unchanged
changed
changed by you

Adding data to packages (your turn)

Let’s also add lettuce and tomato data, for completeness.

Data in packages

There is a lot more detail specific to including data in packages. Sometimes you want to include raw data, sometimes you need data only available to the package and not the user, etc. There is much more detail in the R packages book.

Commit

Now would be a good time to commit your changes.


Git icon

Jason Long, CC BY 3.0 <https://creativecommons.org/licenses/by/3.0>, via Wikimedia Commons

Summary

BLT
β”œβ”€β”€ BLT.Rproj
β”œβ”€β”€ DESCRIPTION
β”œβ”€β”€ NAMESPACE
β”œβ”€β”€ R
β”‚   β”œβ”€β”€ AR1.R
β”‚   β”œβ”€β”€ BLT-package.R
β”‚   β”œβ”€β”€ data.R
β”‚   β”œβ”€β”€ perc_missing.R
β”‚   └── perc_missing_tidy.R
β”œβ”€β”€ data
β”‚   β”œβ”€β”€ bacon.rda
β”‚   β”œβ”€β”€ lettuce.rda
β”‚   └── tomatoes.rda

unchanged
changed
changed by you

Summary

  • It is useful to make a package
    • it is fairly easy with devtools
    • it will help you work more reproducibly
  • A minimal package comprises
    • a folder which is a package and a RProj
    • DESCRIPTION, NAMESPACE, .Rbuildignore .gitignore
    • R/ directory for functions
  • We add functions with usethis:use_r()
  • We use the package interactively with devtools::load_all()
  • We use devtools::check() to execute R CMD check

Resources