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 tiny R packages with the absolute minimum structure and content. The goal: to 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 learning, but none have been submitted to CRAN and I’m never quite sure if I’m doing everything ‘correctly’.

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’) about R packages, 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 it to CRAN.

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

Minimalism

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

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 (which has no file extension) might not be as familiar, but it’s basically a text file with only two lines: the package name and a version number (typically 0.0.0.9000 indicates a package under development, whereas 0.1 might be a minor release).2

Package: mypkg
Version: 0.0.0.9000

The DESCRIPTION file is like a magic flag that identifies that this folder is special and contains an R package; it isn’t just a boring folder with some R scripts in it.

…And that’s all you need.

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

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/") (the second argument is a filepath for where to put the package folder)
  3. Add new function definitions to the mypkg/R/functions.R script file
  4. Install the package to your computer with remotes::install_local("~/Documents/mypkg") and attach it like a normal package 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 folder with the minimum required content, as outlined above. You supply a package name and a directory (folder on your computer) where you want your package to be generated.

As a demonstration, here’s how to create a pico package called {mypkg} in a temporary folder. You should 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/jz/1sgz8lvd19q2wrx7w0cp7rth0000gn/T//Rtmp04i5CG/mypkg

This will output some lines in the R console that confirm your new package has been written to the location you specified (my example path here is convoluted because it’s just a temporary folder).

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, the package now exists on your computer inside a folder. Now how do you use its functions in an R session?

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")  # change to your packages filepath
)
## ✓  checking for file  ‘private/var/folders/jz/1sgz8lvd19q2wrx7w0cp7rth0000gn/T//Rtmp04i5CG/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() added a dummy function called say_hi() to R/functions.R, which we can use now:

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

So, we created an R package, installed it and used it.

Add new functions

Of course, you’ll want to add your own functions to your 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 the updated package is recognised

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!"

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

Note that it can get unwieldy to add all your functions to the functions.R file provided by {pico}, but you can group them up into several R scripts in the R/ subfolder if you like.

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.1.0 (2021-05-18)
##  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-11-02                  
## 
## ─ Packages ───────────────────────────────────────────────────────────────────
##  package     * version    date       lib source                         
##  blogdown      1.4        2021-07-23 [1] CRAN (R 4.1.0)                 
##  bookdown      0.23       2021-08-13 [1] CRAN (R 4.1.0)                 
##  bslib         0.3.1      2021-10-06 [1] CRAN (R 4.1.0)                 
##  callr         3.7.0      2021-04-20 [1] CRAN (R 4.1.0)                 
##  cli           3.1.0      2021-10-27 [1] CRAN (R 4.1.0)                 
##  crayon        1.4.1      2021-02-08 [1] CRAN (R 4.1.0)                 
##  digest        0.6.28     2021-09-23 [1] CRAN (R 4.1.0)                 
##  evaluate      0.14       2019-05-28 [1] CRAN (R 4.1.0)                 
##  fastmap       1.1.0      2021-01-25 [1] CRAN (R 4.1.0)                 
##  htmltools     0.5.2      2021-08-25 [1] CRAN (R 4.1.0)                 
##  jquerylib     0.1.4      2021-04-26 [1] CRAN (R 4.1.0)                 
##  jsonlite      1.7.2      2020-12-09 [1] CRAN (R 4.1.0)                 
##  knitr         1.36       2021-09-29 [1] CRAN (R 4.1.0)                 
##  magrittr      2.0.1      2020-11-17 [1] CRAN (R 4.1.0)                 
##  mypkg       * 0.0.9000   2021-11-02 [1] local                          
##  pico          0.0.0.9000 2021-11-02 [1] Github (matt-dray/pico@ebd205c)
##  pkgbuild      1.2.0      2020-12-15 [1] CRAN (R 4.1.0)                 
##  prettyunits   1.1.1      2020-01-24 [1] CRAN (R 4.1.0)                 
##  processx      3.5.2      2021-04-30 [1] CRAN (R 4.1.0)                 
##  ps            1.6.0      2021-02-28 [1] CRAN (R 4.1.0)                 
##  R6            2.5.1      2021-08-19 [1] CRAN (R 4.1.0)                 
##  remotes       2.4.1      2021-09-29 [1] CRAN (R 4.1.0)                 
##  rlang         0.4.12     2021-10-18 [1] CRAN (R 4.1.0)                 
##  rmarkdown     2.10       2021-08-06 [1] CRAN (R 4.1.0)                 
##  rprojroot     2.0.2      2020-11-15 [1] CRAN (R 4.1.0)                 
##  rstudioapi    0.13       2020-11-12 [1] CRAN (R 4.1.0)                 
##  sass          0.4.0      2021-05-12 [1] CRAN (R 4.1.0)                 
##  sessioninfo   1.1.1      2018-11-05 [1] CRAN (R 4.1.0)                 
##  stringi       1.7.5      2021-10-04 [1] CRAN (R 4.1.0)                 
##  stringr       1.4.0      2019-02-10 [1] CRAN (R 4.1.0)                 
##  withr         2.4.2      2021-04-18 [1] CRAN (R 4.1.0)                 
##  xfun          0.26       2021-09-14 [1] CRAN (R 4.1.0)                 
##  yaml          2.2.1      2020-02-01 [1] CRAN (R 4.1.0)                 
## 
## [1] /Library/Frameworks/R.framework/Versions/4.1/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, especially if you aren’t releasing the package to the world. You can read more in Hadley’s book, though.↩︎

  3. Yo, I heard you like packages that make packages, so I wrote you a package to make a package.↩︎