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