Search code examples
rdevtools

S3 generic/method consistency when different arguments are needed


I've read several SO answers and still not able to piece together how S3 methods need to be documented when they have different arguments. In the example below, I want the user to be able to enter two objects or pipe in a list. The code works fine. It just spits out a warning about method consistency. I've tried moving the list param around but always get some combination of the warning of either foo != foo.list or foo != foo.default. There is a dummy reprex in the collapsed code section and relevant links at the end. Thanks!

#' does some stuff
#' @param list for list methods
#' @param x arg1
#' @param y arg2
#' @param ... some argument
#' @export
foo <- function(list, x, y, ...) UseMethod('foo')

#' @export
foo.default <- function(x, y, ...) paste(x, y, ...)

#' do stuff for lists
#' @export
#' @param list for list methods
#' @inheritParams foo
foo.list <- function(list, x, y, ...) foo(list[[x]], list[[y]])

# create dummy package
tmp <- tempdir() 
setwd(tmp)
devtools::create(path = "test")
setwd("test")
usethis::use_mit_license()

# add R code
writeLines(
  con = "R/test.R",
  text = 
"#' does some stuff
#' @param list for list methods
#' @param x arg1
#' @param y arg2
#' @param ... some argument
#' @export
foo <- function(list, x, y, ...) UseMethod('foo')

#' @export
foo.default <- function(x, y, ...) paste(x, y, ...)

#' do stuff for lists
#' @export
#' @param list for list methods
#' @inheritParams foo
foo.list <- function(list, x, y, ...) foo(list[[x]], list[[y]])"
)

devtools::document()
devtools::load_all()

# examples
foo(1, 2)
list(a = 1, b = 2) |> foo("a", "b")

# check
devtools::check(document = FALSE)

 W  checking S3 generic/method consistency (561ms)
 foo:
   function(list, x, y, ...)
     foo.default:
   function(x, y, ...)
     
     See section 'Generic functions and methods' in the 'Writing R
    Extensions' manual.

Here are some related SO posts:


Solution

  • I am not sure I would use S3 here. Not answering your specific question, but consider something like this. Of course, this is irrelevant if your actual use case is far enough away from your example.

    foo <- function(x, y) {
      if (is.list(x)) return(do.call(foo, x))
       
      paste(x, y)
    }
    

    So now you can get the behavior you want, and just document x as being a list of arguments or the first argument. Otherwise, you'll create some annoying things such as requiring the user to explicitly name the argument every time.

    foo("a", "b")
    # [1] "a b"
    
    foo(list(x = "a", y = "b"))
    # [1] "a b"
    
    foo(list(y = "a", x = "b"))
    # [1] "b a"
    
    foo("a", "b", z = "c")
    # Error in foo("a", "b", z = "c") : unused argument (z = "c")
    
    foo(list("a", "b", z = "c"))
    # Error in (function (x, y)  : unused argument (z = "c")
    

    If compelled, you can accomplish the same thing using S3. But either way, you probably want to just name the argument the same thing and document it as having two meanings.

    foo <- function(x, y, ...) UseMethod('foo')
    
    foo.default <- function(x, y, ...) paste(x, y)
    
    foo.list <- function(x, y, ...) do.call(foo, x)
    

    An example of this is the plot function in base.

    x the coordinates of points in the plot. Alternatively, a single plotting structure, function or any R object with a plot method can be provided.

    y the y coordinates of points in the plot, optional if x is an appropriate structure.

    Here, x is either the x coordinate or a valid plotting structure. And then y is only used if needed.