Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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?

like image 987
Karolis Koncevičius Avatar asked Aug 27 '18 16:08

Karolis Koncevičius


2 Answers

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>
like image 64
alistaire Avatar answered Sep 24 '22 13:09

alistaire


As mentioned in the comment log2 ,log10 aren't in the S3 Math generic. In fact, exp, expm1, log, log10, log2 and log1p are S4 generic and are members of the Math group generic.

One way to implement what do you want to do is to define you class as S4 class.

setClass("lame4", slots = c(x = "numeric"))

And define the method Math group generic :

setMethod("Math","lame4",function(x) {
                x@x <- callGeneric(x@x)
                x
          }) 
## pretty print 
setMethod("show", "lame4",function(object)print(object@x))

Now let's test it :

l1 <- new("lame4",x=1:10)

Then:

log2(l1)
 [1] 0.000000 1.000000 1.584963 2.000000 2.321928 2.584963 2.807355 3.000000 3.169925 3.321928
> log10(l1)
 [1] 0.0000000 0.3010300 0.4771213 0.6020600 0.6989700 0.7781513 0.8450980 0.9030900 0.9542425
[10] 1.0000000 

This of course not a direct answer to your question, but explains why your implementation does not work. Here I think that using S4 paradigm is a good idea because you will have stronger typing which is very helpful with mathematics. S4 methods works fine with R.C/Rcpp interface also. But if you are new to it there is a certain learning curve ( depends in your development background)

like image 29
agstudy Avatar answered Sep 26 '22 13:09

agstudy