Make the simplest R package with {pico}

Hexagonal logo for the pico package with the package name in very small font in light brown on a darker brown background.

tl;dr

I made {pico}, an R package for creating R packages with the absolute minimum structure and content. The goal: demystify package making.

Function in a haystack

I saw a @WeAreRLadies tweet from last week’s curator, @ShreyaLouis. The gist was ‘how can you be more organised when recalling and reusing your own R code?’

See the thread for ideas, but I had the same thought as Fabio: create a personal package of your frequently-used functions so you can invoke them whenever you want.1

What’s the problem?

Packages are daunting, particularly if you haven’t made one before. I’ve written a number of packages for fun and I still don’t know all the requirements.

From personal experience, I think the esoteric structure and content of R packages are a barrier to beginners. Like, what is the man/ folder and what’s an .Rd file? It’s easy to look at a chonky package repo on GitHub, like the popular {dplyr}, and despair.

Yes, you could RTFM (‘Read the Hecking Manual’), but have you looked at it before? And it’s not even necessary to follow all of these steps if you don’t have dreams of submitting to CRAN.

What if—for teaching purposes—we strip back to the absolute barest of package requirements with the goal of demystification and to make it easier to get started?

Minimalism

So what’s the least we need for a functioning package? Well, following Karl Broman, all you need is two files and a subfolder.

Here’s how it looks for an imaginary package called {mypkg}:

mypkg/
├── R/
│   └── functions.R
└── DESCRIPTION

The mypkg/R/functions.R file is a normal R script where you put your function definitions, like:

say_hi <- function(name = "buddy") {
  paste0("Ahoy-hoy ", name, "!")
}

The DESCRIPTION file might not be as familiar, but it’s basically a text file with two lines: the package name and a version number.2 This file is the thing that actually earmarks that this folder is an R package and not just a regular folder with some R scripts in it.

Package: mypkg
Version: 0.0.0.9000

…And that’s all that’s required.

Introducing {pico}

So, you could point-and-click to create a folder with the structure and content outlined above, but I’ve also created the {pico} package to make the setup even easier.

The basic process for using {pico} is:

  1. Install {pico} with remotes::install_github("matt-dray/pico")
  2. Create your package with e.g. pico::create("mypkg", "~/Documents/")
  3. Add new function definitions to mypkg/R/functions.R
  4. Install your package with remotes::install_local("~/Documents/mypkg") and attach like usual with library(mypkg)

Later you can add more functions to R/functions.R (or add more script files to the R/ folder) and can reinstall the package with install_local(), using the force = TRUE argument to overwrite the old version.

Let’s take a look at those steps in a bit more depth.

Install {pico}

First, you can install {pico} from GitHub with help from the {remotes} package.

install.packages("remotes")
remotes::install_github("matt-dray/pico")

You can look up the help files with ?pico::create() at any time.

It’s a really small package, but let me know if you find any bugs or you want to contribute.

Create your package

There’s only one function in {pico}: create(). It generates a ‘pico package’ with the minimum required content. You supply a package name and a directory where you want your package folder to be generated.

As a demonstration, here’s how to create a pico package called {mypkg} in a temporary folder (put yours somewhere more permanent and convenient like ~/Documents on macOS, for example):

tmp <- tempdir()
pico::create(name = "mypkg", dir = tmp)
## Pico package {mypkg} written to:
##   /var/folders/y5/ts9sjlt10x1d7qpkgbhy3smh0000gn/T//RtmpA5hjNp/mypkg

There’s some output confirming your new package has been written to the location you specified (my example path here is nonstandard because it’s a temporary location).

The name will be checked against R-package naming standards: it should contain alphanumeric characters or periods only, must have at least two characters, and can’t start with a number nor end with a period. The provided directory also will be checked for existence and, if it already contains a folder with the proposed name of your package, you’ll be asked interactively if you want to overwrite it.

Install your package

So, your package now exists on your computer. Now how do you use it?

Normally, you would use install.packages() to fetch a package from CRAN and install it to your computer’s R package library. We can do something similar, but instead of fetching from CRAN, we can fetch the package ‘locally’, i.e. from your computer.

To do this, we can use the {remotes} package, which we installed earlier. It contains an install_local() function to which you pass the package’s filepath on your computer:

remotes::install_local(path = file.path(tmp, "mypkg"))
## ✓  checking for file  ‘private/var/folders/y5/ts9sjlt10x1d7qpkgbhy3smh0000gn/T//RtmpA5hjNp/mypkg ’ ...
##  ─  preparing ‘mypkg’:
##  ✓  checking DESCRIPTION meta-information
##  ─  checking for LF line-endings in source and make files and shell scripts
##  ─  checking for empty or unneeded directories
##  ─  creating default NAMESPACE file
##  ─  building ‘mypkg_0.0.9000.tar.gz’
##  
##  * installing *source* package ‘mypkg’ ...
##  ** using staged installation
##  ** R
##  ** byte-compile and prepare package for lazy loading
##  ** help
##  No man pages found in package  ‘mypkg’ 
##  *** installing help indices
##  ** building package indices
##  ** testing if installed package can be loaded from temporary location
##  ** testing if installed package can be loaded from final location
##  ** testing if installed package keeps a record of temporary installation path
##  * DONE (mypkg)

You’ll see some output that describes the installation process, ending with DONE.

The package is now installed into your R package library and can be attached like any other package.

library(mypkg)

Now the functions from the package are available for use. By default, create() adds a dummy function called say_hi() to R/functions.R, so we can use it now:

say_hi("chums")
## [1] "Ahoy-hoy chums!"

So, that means that all those functions you keep forgetting, or that are stored across multiple locations, are now available to you from one package. And ultimately, all it required was install_github(), create() and install_local().

Add new functions

Of course, you can add your own functions to your pico package. The basic steps are:

  1. Open the R/functions.R script
  2. Paste in your function definitions and save the file
  3. Rerun remotes::install_local() with the argument force = TRUE
  4. Restart R, so it recognises the updated package

Here’s what this might look like for our example package. First, you might add the function say_bye() by adding these lines to the functions.R file:

say_bye <- function(name = "folks") {
  paste0("Cheerio ", name, "!")
}

After you saved the updated file, you can re-run install_local() with the file path and force = TRUE, which will overwrite the old version in the package library.

remotes::install_local(
 path = file.path(tmp, "mypkg"),
 force = TRUE
)

You must restart R after you’ve done this.

Your new functions will then be available from your package, much like the dummy say_hi() function was. Here’s say_bye():

library(mypkg)
say_bye("friends")
## [1] "Cheerio friends!"

It can get unwieldy to add all your functions to the functions.R file provided by {pico}, so you can add multiple scripts to the R/ subfolder.

Huge limitations

So, I think {pico} is a quick way to get you from ‘no-package’ to ‘package’ quickly, but more importantly it has none of the esoteric, daunting structure and content of a ‘normal’ package.

However.

A pico package doesn’t encourage best practice, nor is it very useful for sharing. That’s why I think the only practical applications are for learning the basics of package structure, or for building a small package of functions that you might personally want to use again in future.

I would absolutely advocate for learning how to make a ‘real’ package, because that additional structure and content is really powerful and exists for a reason. For example, we haven’t documented any of our functions. What if you add a function to your package but you can’t remember how to use it? We also haven’t tested anything. What if something breaks?

I’ve written before about the wonders of {usethis}, a package made specifically to help develop your own R packages without thinking too hard. I believe it provides the perfect starting point for developing your own package without worrying about exactly what files are needed and where.

There’s a vast array of free web-based resources out there for package building. For example, some that I’ve found useful are:

You should make use of those resources, for sure. Do not use {pico} for any serious work. {pico}’s purpose here is to think about how we might demystify package development. At worst I think it’s an interesting curiosity.


Session info
## ─ Session info ───────────────────────────────────────────────────────────────
##  setting  value                       
##  version  R version 4.0.4 (2021-02-15)
##  os       macOS Big Sur 10.16         
##  system   x86_64, darwin17.0          
##  ui       X11                         
##  language (EN)                        
##  collate  en_GB.UTF-8                 
##  ctype    en_GB.UTF-8                 
##  tz       Europe/London               
##  date     2021-04-19                  
## 
## ─ Packages ───────────────────────────────────────────────────────────────────
##  package     * version    date       lib source                            
##  assertthat    0.2.1      2019-03-21 [1] CRAN (R 4.0.0)                    
##  blogdown      1.2        2021-03-04 [1] CRAN (R 4.0.2)                    
##  bookdown      0.21       2020-10-13 [1] CRAN (R 4.0.2)                    
##  callr         3.6.0      2021-03-28 [1] CRAN (R 4.0.2)                    
##  cli           2.3.1      2021-02-23 [1] CRAN (R 4.0.2)                    
##  crayon        1.4.1      2021-02-08 [1] CRAN (R 4.0.2)                    
##  digest        0.6.27     2020-10-24 [1] CRAN (R 4.0.2)                    
##  evaluate      0.14       2019-05-28 [1] CRAN (R 4.0.0)                    
##  glue          1.4.2      2020-08-27 [1] CRAN (R 4.0.2)                    
##  htmltools     0.5.1.9000 2021-03-11 [1] Github (rstudio/htmltools@ac43afe)
##  knitr         1.31       2021-01-27 [1] CRAN (R 4.0.2)                    
##  magrittr      2.0.1      2020-11-17 [1] CRAN (R 4.0.2)                    
##  mypkg       * 0.0.9000   2021-04-19 [1] local                             
##  pico          0.0.0.9000 2021-04-18 [1] local                             
##  pkgbuild      1.2.0      2020-12-15 [1] CRAN (R 4.0.2)                    
##  prettyunits   1.1.1      2020-01-24 [1] CRAN (R 4.0.0)                    
##  processx      3.5.0      2021-03-23 [1] CRAN (R 4.0.2)                    
##  ps            1.6.0      2021-02-28 [1] CRAN (R 4.0.2)                    
##  R6            2.5.0      2020-10-28 [1] CRAN (R 4.0.2)                    
##  remotes       2.2.0      2020-07-21 [1] CRAN (R 4.0.2)                    
##  rlang         0.4.10     2020-12-30 [1] CRAN (R 4.0.2)                    
##  rmarkdown     2.6        2020-12-14 [1] CRAN (R 4.0.2)                    
##  rprojroot     2.0.2      2020-11-15 [1] CRAN (R 4.0.2)                    
##  sessioninfo   1.1.1      2018-11-05 [1] CRAN (R 4.0.0)                    
##  stringi       1.5.3      2020-09-09 [1] CRAN (R 4.0.2)                    
##  stringr       1.4.0      2019-02-10 [1] CRAN (R 4.0.0)                    
##  withr         2.4.1      2021-01-26 [1] CRAN (R 4.0.2)                    
##  xfun          0.21       2021-02-10 [1] CRAN (R 4.0.2)                    
##  yaml          2.2.1      2020-02-01 [1] CRAN (R 4.0.0)                    
## 
## [1] /Library/Frameworks/R.framework/Versions/4.0/Resources/library

  1. My initial thought was to try and respond—in a single tweet—with the code required to build a package. That might help show how little code is required, especially with shortcuts like usethis::create_package() and usethis::use_r(). But I think there’s a little too much extra explanation required for this to be a viable, helpful response.↩︎

  2. You don’t need to worry too much about the version number for now; a suffix of 9000 liike this one indicates a package is in development. You can read more in Hadley’s book.↩︎