Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What's the preferred means for defining an S3 method in an R package without introducing a dependency?

I have an R package (not currently on CRAN) which defines a couple of S3 methods of generic functions from other packages (specifically knitr::knit_print and huxtable::as_huxtable). However, they're not a key part of my package, so I'd prefer not to create a dependency on those packages when a user installs my package. Up until R 4.0.0, I exported the S3 methods without importing the generics. Using roxygen2, my @export directive was translated into an export() directive in NAMESPACE rather than S3method(). This worked fine in R versions < 4.0.0 because R looks in the global environment for a matching generic_function.class method first rather than relying on proper registration of the S3 method. However, as per this blog on developer.r-project.org, R no longer looks for non-registered S3 methods.

What is the best way round this? For now, I've added @importFrom directives to my roxygen2 blocks and have added both packages to the imports section of DESCRIPTION. However, as I understand things this will mean any user installing my package will then also have to install knitr and huxtable whether they want to or not.

like image 787
Nick Kennedy Avatar asked Apr 28 '20 14:04

Nick Kennedy


People also ask

What is S3 method in R?

S3 refers to a class system built into R. The system governs how R handles objects of different classes. Certain R functions will look up an object's S3 class, and then behave differently in response. The print function is like this.

What is a dependency in R?

A dependency is a code that your package needs to run. Dependencies are managed by two files. The DESCRIPTION manages dependencies at the package level; i.e. what packages needs to be installed for your package to work. R has a rich set of ways to describe different types of dependencies.

What are methods in R?

R possesses a simple generic function mechanism which can be used for an object-oriented style of programming. Method dispatch takes place based on the class(es) of the first argument to the generic function or of the object supplied as an argument to UseMethod or NextMethod .

What is an S3 class in R?

An S3 class is the most prevalent and used class in R programming. It is easy to implement this class and most of the predefined classes are of this type. An S3 object is basically a list with its class attributes assigned some names. And the member variable of the object created is the components of the list.

Is it possible to use generic functions in S3?

In S3 system, methods do not belong to object or class, they belong to generic functions. This will work as long as the class of the object is set. It is possible to make our own generic function like print () or plot ().

What is an S3 method?

is an S3 method. When applied to an object of class shows descriptive statistics (Mean, SD, etc.) for each variable. For example, also performs differently when applied to different object. In fact, you can find all the classes that work with an S3 method by typing the following: There’s over 30 different methods!

What is the use of summary in S3?

Being an S3 method, summary calls the appropriate function based upon the class of the object it operates on. So using summary on an object of class “Date” will evoke the function, summary.Date. But all you need to do is type summary, and the S3 method does the rest.


2 Answers

Fortunately, for R >= 3.6.0, you don't even need the answer by caldwellst. From the blog entry you linked above:

Since R 3.6.0, S3method() directives in NAMESPACE can also be used to perform delayed S3 method registration. With S3method(PKG::GEN, CLS, FUN) function FUN will get registered as an S3 method for class CLS and generic GEN from package PKG only when the namespace of PKG is loaded. This can be employed to deal with situations where the method is not “immediately” needed, and having to pre-load the namespace of pkg (and all its strong dependencies) in order to perform immediate registration is considered too “costly”.

Additionally, this is also discussed in the docs for the other suggestion of vctrs::s3_register():

#' For R 3.5.0 and later, `s3_register()` is also useful when demonstrating
#' class creation in a vignette, since method lookup no longer always involves
#' the lexical scope. For R 3.6.0 and later, you can achieve a similar effect
#' by using "delayed method registration", i.e. placing the following in your
#' `NAMESPACE` file:
#'
#' ```
#' if (getRversion() >= "3.6.0") {
#'   S3method(package::generic, class)
#' }

So, you would simply need to not use @importFrom and instead of @export, use @exportS3Method package::generic (See https://github.com/r-lib/roxygen2/issues/796 and https://github.com/r-lib/roxygen2/commit/843432ddc05bc2dabc9b5b22c1ae7de507a00508)

Illustration

So, to illustrate, we can make two very simple packages, foo and bar. The package foo just has a generic foo() function and default method:

library(devtools)
create_package("foo")

#' foo generic
#'
#' @param x An object
#' @param ... Arguments passed to or from other methods
#' @export
foo <- function(x, ...) {
    UseMethod("foo", x)
}
#' foo default method
#'
#' @param x An object
#' @param ... Arguments passed to or from other methods
#' @export
foo.default <- function(x, ...) {
    print("Called default method for foo.")
}

After document() and install()ing, we create bar:

create_package("bar")

which creates a bar method for foo():

#' bar method for foo
#'
#' @param x A bar object
#' @param ... Arguments passed to or from other methods
#'
#' @exportS3Method foo::foo
foo.bar <- function(x, ...) {
    print("Called bar method for foo.")
}

Importantly, we must load the foo package before running document(), or @exportS3Method won't work. That is,

library(foo)
document()

But, if we do that, we get the following in the NAMESPACE for bar:

# Generated by roxygen2: do not edit by hand

S3method(foo::foo,bar)

We have to manually add foo to "Suggests" in DESCRIPTION.

Then if we uninstall foo, we can still install bar:

> remove.packages("foo")
Removing package from ‘/home/duckmayr/R/x86_64-pc-linux-gnu-library/4.0’
(as ‘lib’ is unspecified)
> install("bar")
✓  checking for file ‘/home/jb/bar/DESCRIPTION’ ...
─  preparing ‘bar’:
✓  checking DESCRIPTION meta-information ...
─  checking for LF line-endings in source and make files and shell scripts
─  checking for empty or unneeded directories
─  building ‘bar_0.0.0.9000.tar.gz’

Running /opt/R/4.0.0/lib/R/bin/R CMD INSTALL \
  /tmp/Rtmp5Xgwqf/bar_0.0.0.9000.tar.gz --install-tests 
* installing to library ‘/home/jb/R/x86_64-pc-linux-gnu-library/4.0’
* installing *source* package ‘bar’ ...
** using staged installation
** R
** byte-compile and prepare package for lazy loading
** help
*** 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 (bar)
like image 117
duckmayr Avatar answered Oct 16 '22 23:10

duckmayr


The vctrs package provides a function called s3_register that dynamically registers methods for use in the .onLoad function. You can read more about its use here, for yourself you would want:

.onLoad <- function(...) {
  if (requireNamespace("knitr", quietly = TRUE)) {
    vctrs::s3_register("knitr::knit_print", "class_name")
  }
  if (requireNamespace("huxtable", quietly = TRUE)) {
    vctrs::s3_register("huxtable::as_huxtable", "class_name")
  }
}

The documentation is kind as well so you don't have to import vctrs:

To avoid taking a dependency on vctrs for this one function, please feel free to copy and paste the function source into your own package.

like image 2
caldwellst Avatar answered Oct 17 '22 00:10

caldwellst