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?
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>
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)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With