Russ Lenth
Russ Lenth

Reputation: 6770

Flaky S4 method dispatch

I was testing to see if I could support models fitted using the cplm package in emmeans, and I ran across some very odd behavior for the terms method. Here is an example:

library(emmeans)
library(cplm)

data(mtcars)

m <- cpglm(mpg ~ cyl + disp + hp, data = mtcars)

The function emmeans:::recover_data.lm seemed to have some promise of working correctly. It just calls the terms method on the object and passes it on to other functions. However, it doesn't work:

> emmeans:::recover_data.lm(m)
 Error in slot(x, name) : 
  no slot of name "terms" for this object of class "cpglm" 

Digging further, I verify that terms() does work in the global environment:

> terms(m)
mpg ~ cyl + disp + hp
... (remaining lines excluded) ...

However, it does not work in recover_data.lm:

> debug(emmeans:::recover_data.lm)
> emmeans:::recover_data.lm(m)
    ...

Browse[2]> terms(object)
Error in slot(x, name) : 
  no slot of name "terms" for this object of class "cpglm"

Browse[2]> getMethod("terms", "cpglm")
Error in getMethod("terms", "cpglm") : 
  no method found for function 'terms' and signature cpglm

However...

Browse[2]> getMethod("terms", "cplm")  ## cpglm inherits from cplm
Method Definition:

function (x, ...) 
attr([email protected], "terms")
    ... etc. ...

Browse[2]> methods("terms")
[1] terms,ANY-method  terms,cplm-method terms.default*    terms.formula*    terms.gls*       
[6] terms.merMod*     terms.terms*     
see '?methods' for accessing help and source code

Browse[2]> selectMethod("terms", "cpglm")
Method Definition:

function (x, ...) 
attr([email protected], "terms")
    ... etc. ...

So, in the namespace for emmeans, I can verify that we "know" that there is a terms method for cpglm objects, and we can find it via selectMethod() but not via getMethod() or by just calling terms(). This is very odd, especially since the documentation for selectMethod seems to promise it should work:

The function selectMethod() returns the method that would be selected for a call to function f if the arguments had classes as specified by signature.

Finally, I am able to verify that this seems to have something to do with a conflict in dispatching S3 methods:

> stats::terms(m)
Error in slot(x, name) : 
  no slot of name "terms" for this object of class "cpglm"

Is there a way to get my package emmeans to work correctly when there is a mix of S3 and S4 methods; in particular, to look for S4 methods before S3 methods?

Upvotes: 1

Views: 65

Answers (1)

Alexis
Alexis

Reputation: 5059

From the documentation entry in Methods_for_S3:

The reasons for defining both S3 and S4 methods are as follows:

  1. An S4 method alone will not be seen if the S3 generic function is called directly. This will be the case, for example, if some function calls unique() from a package that does not make that function an S4 generic.

The cplm package exported an S4 method for terms, but not one for S3, which you can see here. This automatically makes terms an S4 generic when you attach cplm. However, emmeans calls the S3 generic from stats, and that's what causes the problem.

You can work around it by defining an S3 generic in the global environment that dispatches to the S4 generic:

terms.cpglm <- function(x, ...) {
    cplm::terms(x, ...)
}

but you should let the cplm maintainer know that he should define S3 and S4 methods if he's transforming an S3 generic into an S4 one.

Upvotes: 1

Related Questions