Search code examples
rclassoopgenericsr-s3

Unexpected log2 error when defining S3 "Math" group generics for a new class


I am trying to use S3 "Math" group generics for a custom class. However I am getting a strange result: log() works while log2 and log10 produces errors. Below is a minimal example:

# simple class with just the new name
lameclass <- function(x) {
  class(x) <- append(class(x), "lame")
  x
}

# It prints something when Math generics methods are used
Math.lame <- function(x, ...) {
  print("I am lame")
  NextMethod()
}

# an object of the class
lamevector <- lameclass(1:10)

> class(lamevector)
[1] "integer" "lame"

Now try to call log:

log(lamevector)
[1] "I am lame"
[1] 0.0000000 0.6931472 1.0986123 1.3862944 1.6094379 1.7917595 1.9459101 2.0794415 2.1972246 2.3025851

With base 2:

log(lamevector, 2)
[1] "I am lame"
[1] 0.000000 1.000000 1.584963 2.000000 2.321928 2.584963 2.807355 3.000000 3.169925 3.321928

All above worked. But now log2 wrapper:

log2(lamevector)
[1] "I am lame"
[1] "I am lame"
Error in log2.default(1:10, 2) :
  2 arguments passed to 'log2' which requires 1

Maybe someone can help me with figuring out what is going on here? Did log2 actually went through the generic Math definition 2 times and failed?


Solution

  • What appears to be happening is that NextMethod is not stripping the lame class, so when log2 calls log, it re-dispatches to the lame method, which now no longer works, because it's calling log2 with base = 2L, a parameter log2 doesn't have.

    Forcing the dispatch to work correctly doesn't require too much work—just strip and re-add the class. (Aside: Subclasses should be prepended, not appended.)

    lameclass <- function(x) {
        class(x) <- c("lame", class(x))    # prepend new class
        x
    }
    
    Math.lame <- function(x, ...) {
        print("I am lame")
        class(x) <- class(x)[class(x) != "lame"]    # strip lame class
        lameclass(NextMethod())    # re-add lame class to result
    }
    
    lamevector <- lameclass(1:5)
    
    log(lamevector)
    #> [1] "I am lame"
    #> [1] 0.0000000 0.6931472 1.0986123 1.3862944 1.6094379
    #> attr(,"class")
    #> [1] "lame"    "numeric"
    log(lamevector, 2)
    #> [1] "I am lame"
    #> [1] 0.000000 1.000000 1.584963 2.000000 2.321928
    #> attr(,"class")
    #> [1] "lame"    "numeric"
    log2(lamevector)
    #> [1] "I am lame"
    #> [1] 0.000000 1.000000 1.584963 2.000000 2.321928
    #> attr(,"class")
    #> [1] "lame"    "numeric"
    

    I'm not precisely sure why it's dispatching like that. Group generics are a little weird, and dispatch on oldClass instead of class, which may or may not be part of the issue. It may just be a bug. The idiom of stripping and re-adding the class is used in other Math methods, possibly for this reason:

    MASS:::Math.fractions
    #> function (x, ...) 
    #> {
    #>     x <- unclass(x)
    #>     fractions(NextMethod())
    #> }
    #> <bytecode: 0x7ff8782a1558>
    #> <environment: namespace:MASS>