Search code examples
rr-packager-s3method-dispatchr-s7

Specifying name of S3 method on S7 class when S7 class is exported as packagename::class


I am writing a package in {S7} and wish to follow the recommendation that classes exported from the package are exported with the package name, e.g. minrep::accr, to avoid namespace conflicts.

I have run into an issue because I need to write a method which works both on an S7 object (let's say minrep::accr) and on an array. I can't do this as an S7 method, because S7 doesn't have a class_array() and so it looks for tsum.integer which is obviously far too wide a scope. I can make it work with an S3 method if I don't set the package name (comment out line 23 in the example code), but that isn't following good practice (see above). If I do set the package name, then it looks for, say, tsum.minrep::accr, which I do not know how to specify in a function name. What trick am I missing?

Minrep:

#' Generic for tsum
#' @param x Thing that needs summing
#' @export
#' 
tsum <- function(x, ...) {
  UseMethod("tsum", x)
}

#' tsum for arrays
#' @param x Array that needs summing
#' @export 
#' 
tsum.array <- function(x) {
  rowSums(aperm(x, c(2, 1, 3)))
}

#' S7 class for an object with an accrual property that is an array
#' @slot accrual Array
#' @param accrual Array
#' @export 
#' 
accr <- S7::new_class("accr",
  package = "minrep",
  properties = list(
    accrual = S7::class_integer
  ),
  constructor = function(
    accrual = S7::class_missing
  ) {
    S7::new_object(
      S7::S7_object(),
      accrual = accrual
    )
  }
)

#' tsum for objects of class "accr"
#' @param x Object of class "accr"
#' @export 
#' 
tsum.accr <- function(x) {
  tsum(x@accrual)
}

#' Do the thing
#' @param x 3D integer array
#' @export 
#' 
do_tsum <- function(x) {
  a <- accr(x)

  print("Treatment sum of array")
  print(tsum(x))

  print("Treatment sum of object")
  tsum(a)

}

Desired output:

r$> do_tsum(array(1:24, 2:4))
[1] "Treatment sum of array"
[1]  84 100 116
[1] "Treatment sum of object"
[1]  84 100 116

Solution

  • The trick you are missing is that you can wrap the name of the tsum.minrep::accr function in backticks:

    `tsum.minrep::accr` <- function(x) {
      tsum(x@accrual)
    }
    

    Which results in

    do_tsum(array(1:24, 2:4))
    #> [1] "Treatment sum of array"
    #> [1]  84 100 116
    #> [1] "Treatment sum of object"
    #> [1]  84 100 116
    

    If you insert a browser() call as the first line of `tsum.minrep::accr` you will see that this function is indeed dispatched by the generic when you call tsum(x)

    The function name is non-standard and looks a little odd, but this is a necessary side-effect of how S7 classes are named, since they use the non-standard "package::function" schema for class names.

    The alternatives are

    1. Avoid the package name in the class, as you already suggested. The docs do state that this is not best practice, though to be fair, S3 dispatch is used throughout the R ecosystem, and class names almost universally lack the specific package identifiers that would avoid S3 dispatch clashes (including some fine packages written by the S7 authors).
    2. Use tsum.S7_object as your method, which will be called on all S7 objects, and act conditionally within that function according to the specific manually-checked S7 class name. This kind of misses the whole point of generic dispatch, and to my nostrils gives a worse code smell than a backticked non-standard function name does.

    The backticked function seems like the most reasonable best-practice approach to me.