4

In R, I have an S3 class that emulates a 1D vector, and so I want to implement custom versions of mean, sum, max, etc. Let's say it looks like this:

my_class = function(){
  structure(list(), class='my_class')
}

All the above methods work fine, if I define mean.my_class etc:

mean.my_class = function(x){
  2
}

mean(my_class())

However, I would also like to do this for functions like var, which isn't a generic method. If I create such a function, and then call var on an instance of my class:

var.my_class = function(his){
  # Do stuff here
}

var(my_class())

I get the error:

Error in var(my_class()) : is.atomic(x) is not TRUE

This is because there is no generic var function, and it's just calling stats::var on my structure. How then can I provide a custom implementation if there isn't a generic method? I don't want to break the var method for regular vectors either.

Migwell
  • 18,631
  • 21
  • 91
  • 160
  • Sorry I don't have time for a full answer but check Creating new methods and generics in http://adv-r.had.co.nz/OO-essentials.html – Claudiu Papasteri Mar 26 '21 at 07:31
  • Right, but because there's already a `var` function, should I define a generic function with the same name, or with a different name? – Migwell Mar 26 '21 at 09:13

2 Answers2

3

You can create your own generic with the same name and signature as the existing non-generic function:

var = function (x, y = NULL, na.rm = FALSE, use) UseMethod('var')
var.my_class = function (x, y, na.rm, use) 42

registerS3method('var', 'default', stats::var)
registerS3method('var', 'my_class', var.my_class)

… instead of manually copying the formal argument list for the generic function, you can instead use the following to generate it automatically:

var = function () UseMethod('var')
formals(var) = formals(stats::var)

— Of course the above only works if the caller calls your var function. If somebody invokes for instance stats::var, your generic won’t be found, and so no S3 method dispatch happens.


Beware that R 3.5 changed the way S3 method lookup works across environments, and merely defining an appropriately-named function is no longer sufficient for the method to be found from a different environment — you need to invoke registerS3method (not just for new generics like var but also for your mean!). Unfortunately there’s virtually no official documentation for this function, just some blog posts by Kurt Hornik.

Konrad Rudolph
  • 530,221
  • 131
  • 937
  • 1,214
  • Will `registerS3method` allow me to use the "default" `var` function instead of `mypackage::var`? Also, how does it relate to `S3method` in the `NAMESPACE` file? – Migwell Mar 28 '21 at 01:47
  • @Migwell `stats::var` remains a non-generic function, there’s no way to change that. But you can call `var` instead of `mypackage::var` if your package is attached (but it’s sufficient if just the `var` function is attached, not the rest; this is possible e.g. via ‘[box](https://github.com/klmr/box)’). `registerS3method` is unrelated to that — but if your code goes into a package, you don’t need `registerS3method`: having `S3method` inside the `NAMESPACE` file is sufficient (and in fact has the same effect as calling `registerS3method`). – Konrad Rudolph Mar 28 '21 at 09:54
1

From methods for non generics, S4 setMethod seems to be compatible with S3 class:

setMethod("var","my_class",function(x, y = NULL, na.rm = FALSE, use) {"Var for my_class"})
#> Creating a generic function for 'var' from package 'stats' in the global environment
#> in method for 'var' with signature '"my_class"': no definition for class "my_class"

my_class = function(){
  structure(list(), class='my_class')
}

var(my_class())
#> [1] "Var for my_class"
var(1:10)
#> [1] 9.166667
Waldi
  • 39,242
  • 6
  • 30
  • 78
  • Sure this works but you can do the same in S3, no need to use S4. Either way, it creates a completely new generic rather than adding a method to an existing generic. – Konrad Rudolph Mar 27 '21 at 13:02
  • I like this approach because `setMethod` (and the associated `setGeneric`) are specifically designed for my use-case, and therefore can do useful things like copying the method signature. As noted by @KonradRudolph, you still can't replace the original function in the original package although I didn't expect that would be possible. – Migwell Mar 28 '21 at 01:44