在不引入依赖项的情况下在 R 包中定义 S3 方法的首选方法是什么?

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

我有一个 R 包(目前不在 CRAN 上),它定义了几个来自其他包(特别是 knitr::knit_printhuxtable::as_huxtable)的通用函数的 S3 方法。但是,它们不是我的包的关键部分,所以我不希望在用户安装我的包时创建对这些包的依赖。直到 R 4.0.0,我导出了 S3 方法而不导入泛型。使用 roxygen2,我的 @export 指令被翻译成 NAMESPACE 中的 export() 指令,而不是 S3method()。这在 R 版本 < 4.0.0 中运行良好,因为 R 首先在全局环境中查找匹配的 generic_function.class 方法,而不是依赖 S3 方法的正确注册。但是,根据 this blog on developer.r-project.org,R 不再查找未注册的 S3 方法。

解决这个问题的最佳方法是什么?现在,我已经将 @importFrom 指令添加到我的 roxygen2 块中,并将这两个包都添加到了 DESCRIPTION 的导入部分。但是,据我了解,这意味着任何安装我的软件包的用户都必须安装 knitrhuxtable,无论他们是否愿意。

vctrs 包提供了一个名为s3_register 的函数,它动态注册方法以供在.onLoad 函数中使用。你可以阅读更多关于它的使用 here,你自己会想要:

.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")
  }
}

文档也很好所以你不必导入 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.

幸运的是,对于 R >= 3.6.0,您甚至不需要 。来自上面链接的博客条目:

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

此外,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)
#' }

因此,您只需要 而不是 使用 @importFrom 而不是 @export,而是使用 @exportS3Method package::generic(请参阅 https://github.com/r-lib/roxygen2/issues/796 and https://github.com/r-lib/roxygen2/commit/843432ddc05bc2dabc9b5b22c1ae7de507a00508)

插图

因此,为了说明,我们可以制作两个非常 简单的包,foobar。包 foo 只有一个通用的 foo() 函数和默认方法:

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.")
}

document()install()ing 之后,我们创建 bar:

create_package("bar")

foo() 创建一个 bar 方法:

#' 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.")
}

重要的是,我们必须 加载 foo 包之前 运行 document(),或者@exportS3Method 将不起作用。即,

library(foo)
document()

但是,如果我们这样做,我们会在 barNAMESPACE 中得到以下内容:

# Generated by roxygen2: do not edit by hand

S3method(foo::foo,bar)

我们必须手动添加 fooDESCRIPTION 中的 "Suggests"。

那么如果我们卸载foo,我们仍然可以安装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)