The goal of rpp is to provide a framework for preprocessing R code. Ultimately, this package aims at supporting static type checking for R, among other applications. At this time, dynamic type checking and zero-cost assertions are supported with the help of two other packages, {typed} and {chk}.

Motivation

R is a weakly-typed interpreted language: variables can hold objects of any time, there is no explicit compilation stage, and no checks are carried out during run time. This is great for interactive ad-hoc analysis, but can make complex projects more difficult to maintain and debug. In contrast, in strongly-typed languages, the type of each variable is declared beforehand. Adding a static type checking layer to R would make it easier to improve stability in complex projects. With preprocessing, this can be done with no cost at runtime.

Development vs. production

This package operates on the notion of different source code “modes”:

  • Development (or dev): the code the developer of the package works on
  • Production (or prod): the code that is run by typical users

Expensive checks can be enabled in development mode, while production code is kept lean and fast. In production mode, all checks are completely removed (elided) from the source code. Only production code ends up in version control, this ensures compatibility with existing tooling. Code can be quickly and losslessly converted between development and production modes with rpp::rpp_to_dev() and rpp::rpp_to_prod().

Plugins

The rpp package does not implement any code transformations. Rather, it provides the infrastructure for plugins which are responsible for converting development code to production code and back. Currently, two plugins exist (in forks of existing packages in this GitHub organization):

Plugins are configured in the DESCRIPTION file.

Installation

Install the development version of rpp and the associated packages from GitHub with:

# install.packages("devtools")
devtools::install_github("Q-language/rpp")
devtools::install_github("Q-language/typed")
devtools::install_github("Q-language/chk")

Once on CRAN, you can also install the released version of rpp with:

install.packages("rpp")

Example

library(typed)
#> 
#> Attaching package: 'typed'
#> The following object is masked from 'devtools_shims':
#> 
#>     ?
#> The following object is masked from 'package:utils':
#> 
#>     ?

The following function makes use of dynamic type assertions provided by the {typed} package:

foo <- Character()? function(x = ?Character()) {
  Character()? out <- paste("foo:", x)
  out
}

This is still valid R code, because {typed} overloads the ? operator. The function can only be called with a character vector, other types give an error:

foo("bar")
#> [1] "foo: bar"
foo(1)
#> Error: In `foo(1)` at `check_arg(x, Character())`:
#> wrong argument to function, type mismatch
#> `typeof(value)`: "double"   
#> `expected`:      "character"
foo(mean)
#> Error: In `foo(mean)` at `check_arg(x, Character())`:
#> wrong argument to function, type mismatch
#> `typeof(value)`: "closure"  
#> `expected`:      "character"

These checks are useful, but slow down the code. If this function lives in a package that is configured with the typed::rpp_elide_types() plugin, running rpp::rpp_to_prod() results in the following code:

foo <-              function(x               ) { # !q foo <- Character()? function(x = ?Character()) {
  out <- paste("foo:", x)                        # !q   Character()? out <- paste("foo:", x)
  out
}

Running rpp::rpp_to_dev() brings back the original code with the checks. The production version is not particularly pretty, but does the job.

The fork of the {chk} package in this organization is configured for use with rpp. Clone the repository, start an R session, and run rpp::rpp_to_dev() and rpp::rpp_to_prod() to see rpp in action.

Configure your own project for use with rpp

This example show how to configure your project with rpp and dynamic type checking via {typed}. Note that this requires {typed} to be installed from this organization’s fork. Adapt as necessary for use with other plugins.

Basic configuration

rpp currently works only on projects with a DESCRIPTION file, and operates only on files in the R/ directory. Add the following line to DESCRIPTION:

Config/rpp/plugins: list(typed::rpp_elide_types())

With this configuration, rpp::rpp_to_dev() and rpp::rpp_to_prod() will run the plugin implemented by {typed}.

Configuring for {typed}

In the case of {typed}, two more tweaks are necessary:

  1. The package must be listed in the Imports section of the DESCRIPTION file.
  2. It should be imported to enable the overload of the ? operator and to access the type declarations.

For the latter, add import(typed) to NAMESPACE, or #' @import typed to an .R source file if you are using roxygen2.

{roxygen2} integration

A project can be automated to change to production mode when devtools::document() is run. Add the rpp::rpp_prod_roclet() to the list of roclets:

Roxygen: list(markdown = TRUE, roclets = c("collate", "namespace", "rd", "rpp::rpp_prod_roclet"))

RStudio users can also edit the .Rproj file to convert to prod when Ctrl+Shift+D is pressed:

PackageRoxygenize: rd,collate,namespace,rpp::rpp_prod_roclet

With this configuration, running devtools::document() includes the effect of calling rpp::rpp_to_prod().

Further reading

The creation of a new plugin and the integration with roxygen2 is described in vignette("plugins", package = "rpp").


Code of Conduct

Please note that the rpp project is released with a Contributor Code of Conduct. By contributing to this project, you agree to abide by its terms.