Making a Simple Package in R, using Modern Tools

Making a Simple Package in R, using Modern Tools
Photo by Luku Muffin / Unsplash

I will not be using {fusen} here while it's often being advertised these days. I find it a bit confusing and much prefer the typical way of building packages.

You need to install the following:

  • devtools (necessary to build your package)
  • usethis (helper functions to take shortcuts)
  • roxygen2 (simplifies package creation)
  • testthat (to incorporate standard testing as part of your package build)

You can install them all with a one liner:

install.packages(c("devtools", "roxygen2","usethis", "testthat"))

Setting You on the Right Path

First you need to setup the path that you are working with, and where a new folder will be created for you. (don't create a folder for your package!)

setwd("/path/to/your/main/folder/")

Now you can use a helper function to create the main structure of your package:

usethis::create_package("packagedfun")

note that are limits to what kind of characters you can use when you name a package. If you get an error this probably means that you tried to use some of them that are not allowed.

When you execute the above command in RStudio, it will directly reload the environment to put you inside the newly created project.

Your environment will have the following files:

  • .gitignore
  • .Rbuildignore
  • DESCRIPTION
  • NAMESPACE
  • packagedfun.Rproj
  • R (folder)

The DESCRIPTION file generates meta data about your project. You will have to edit it by yourself to reflect the actual information about your package.

The NAMESPACE file is automatically generated (and is read-only) in our workflow so you should not edit it manually.

The R folder is where you will put your R scripts which will contain your functions and scripts.

Licensing: Just a Few Seconds of Work

First I'd recommend you set the license of your package. You can use the helper functions from usethis for this purpose. Let's say we want to use the MIT license, you can simply write in the console:

usethis::use_mit_license()

This will automatically modify the DESCRIPTION file to include the MIT license and add a separate license file (actually two, LICENSE and LICENSE.md) in the working folder with the MIT license text. Practical!

Of course this is not limited to MIT, you can add AGPL, GPL, or even proprietary licenses directly from usethis.

Versioning: Easy!

You probably want to start enumerating the version of your package as well. Once again, usethis comes to the rescue when you run it in the console:

usethis::use_version()

This will ask you to choose how to increment your package's version number:

If you are still in early days of developing your package, you may want to start with a minor version (option 2). Type 2, and the DESCRIPTION will be automatically updated.

Your Package's First Function

Let's proceed by creating a simple function:

usethis::use_r("hello")

This creates a hello.R file inside the R folder, as you can imagine.

The new file is completely empty and you can start creating your function. Let's do a simple hello world, but with a twist.

hello_fun <- function(name) {
  
  print(glue::glue("Hello {name}"))
  
}

As you can see, I am including an external dependency on purpose here. Most of the tutorials on the net consider that you make functions that depend on nothing at all, which is far from being the typical use case.

Now let's see what happens if we check the package in its current state.

Learning from Failure(s)

Assuming you use RStudio, you can use the "Check Package" menu from the Rstudio interface in the Build menu:

You will get a long list of checks happening in the environment tab on the upper right corner, and at the end it will issue a warning about the fact that you did not declare a dependency.

You can actually still build your package - and it will work and load, but when you call the hello_fun() function it will complain that the function glue is not available, which is less than ideal.

In order to fix it, you can follow the style used by roxygen2 to add meta data to your functions:

#' Saying Hello is polite
#' 
#' @description
#' `hello_fun` says hello and uses the name of the person as an argument.
#'
#' @import glue
#'
#' @details
#' This is a generic function that can be used to say hello/
hello_fun <- function(name) {

  print(glue::glue("Hello {name}"))

}

The key statement here is the @import. Here by specify "glue", which means it will import the whole glue library along with all of its functions. While that would work, that's completely overkill, and incurs the risk of functions with the same name causing conflicts. The better approach if a more specific import statement:

#' Saying Hello is polite
#' 
#' @description
#' `hello_fun` says hello and uses the name of the person as an argument.
#'
#' @importFrom glue glue
#'
#' @details
#' This is a generic function that can be used to say hello/
hello_fun <- function(name) {

  print(glue::glue("Hello {name}"))

}

Since it's not obvious from the example above, the @importFrom follows the syntax:

@importFrom <package> <function>

So if you import the function write_feather from the library arrow, it would look like:

@importFrom arrow write_feather

Are we done now?

Let's "check" the package again.

When you run the checks, it will also modify the NAMESPACE file - and if you open it, you will see it now contains the dependency declaration:

# Generated by roxygen2: do not edit by hand

importFrom(glue,glue)

... which is great, but alas! this is not enough to pass the checks yet!

Now we get an actual error, because our dependency is not declared in the DESCRIPTION file!

It's an easy fix - this is the DESCRIPTION before the import statement:

Package: packagedfun
Title: Enjoy a proper Hello World with a customized message!
Version: 0.1.0
Authors@R: 
    person("Alisa", "Wazrak", , "alisa.wazrak@example.com", role = c("aut", "cre"))
Description: This package provides the valuable hello_fun function that nobody has ever created before.
License: MIT + file LICENSE
Encoding: UTF-8
Roxygen: list(markdown = TRUE)
RoxygenNote: 7.2.3

and after:

Package: packagedfun
Title: Enjoy a proper Hello World with a customized message!
Version: 0.1.0
Authors@R: 
    person("Alisa", "Wazrak", , "alisa.wazrak@example.com", role = c("aut", "cre"))
Description: This package provides the valuable hello_fun function that nobody has ever created before.
Imports: glue
License: MIT + file LICENSE
Encoding: UTF-8
Roxygen: list(markdown = TRUE)
RoxygenNote: 7.2.3

Are we there yet?

Let's see...

Almost!

Now we are notified that we have forgotten to document the argument in our function.

We can just modify it in the hello.R file to include a @param statement as well as an @export statement to ensure that the function is available once you load the package later on: (functions without @export flags are ignored)

#' Saying Hello is polite
#'
#' @description
#' `hello_fun` says hello and uses the name of the person as an argument.
#'
#' @importFrom glue glue
#'
#' @details
#' This is a generic function that can be used to say hello/
#'
#' @param name the name of the person you want to send a Hello message to
#' @export
hello_fun <- function(name) {

  print(glue::glue("Hello {name}"))

}

And we are done! The last check works:

Building, Installing and Testing the Package

Now you can go ahead and build a binary package, for example, once the checks are successful:

This will build a packagedfun_0.1.0_R_x86_64-pc-linux-gnu.tar.gz (if you run this on Linux) in the parent folder of your package.

To check if it works, you can exit the current Rstudio session, and reload a clean one. Then you try installing the package (of course, adapt the path to your system's):

install.packages("/path/to/your/package/binary/packagedfun_0.1.0_R_x86_64-pc-linux-gnu.tar.gz")

It should succeed.

Now you can load it:

library(packagedfun)

and try it out:

hello_fun("You!")

and you should see a glorious answer in your console:

Hello You!

We are done!

I hope you enjoyed this post. We will come back with a few more details on how to make packages in R. This time we focused on how to do it correctly with the minimal effort - next, we will see how we can add some more polish.