5

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?

Jaap
  • 81,064
  • 34
  • 182
  • 193
Karolis Koncevičius
  • 9,417
  • 9
  • 56
  • 89

2 Answers2

4

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>
alistaire
  • 42,459
  • 4
  • 77
  • 117
  • Thanks. Great work around with stripping and re-adding the class and a nice example from MASS. +1 and accepted. One last question thou - do you think it makes sense to strip and re-add class for other group generics as well (just in case) ? I am a bit paranoid now about there being other functions with the same issue as log2. – Karolis Koncevičius Aug 30 '18 at 17:23
  • 1
    Probably, yes; MASS does. `units:::Math.units` takes a more function-specific approach, which seems much harder to write. Of the other group generics, `Ops` may require more attention yet, as it uses double dispatch. Whatever approach you use, test rigorously. – alistaire Aug 30 '18 at 17:31
2

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)

agstudy
  • 119,832
  • 17
  • 199
  • 261
  • Thank you very much for the response @agstudy . And thanks for the S4 suggestion, but I have to point out - I am not looking for that at all. The actual task I had actually went by OK without using Math group generics for S3 (as both log and log2 preserved the class of my object). The question is more of a surprise that Math.generics breaks log2. If log2 wasn't covered by Math I would expect it to not be affected by it, not break. And I am wondering is this intentional or some sort of bug in R base. – Karolis Koncevičius Aug 30 '18 at 12:16
  • But +1 because I think this might be useful for people with similar questions in the future (i.e. knowing they can turn to S4). However I still think this doesn't really explain the behaviour of `log2` - lot's of other functions are not included under Math generics, but they don't break once Math generics is defined. So something is still missing I feel. – Karolis Koncevičius Aug 30 '18 at 12:26
  • @KarolisKoncevičiuscan you add in your question the generics from Math that does not broke your S3 class implementation? This will clarify the example. – agstudy Aug 30 '18 at 12:28
  • For my case I had a custom class defined and all I wanted from generics was they they preserved my class. Turned out it worked WITHOUT actually defining Math group generics at all :) So there was no "working implementation" as such... – Karolis Koncevičius Aug 30 '18 at 12:34
  • Note that the S4 generic for **log** has a signature with only one argument, SO i don't think that there is a bug here it is just an implementation choice. – agstudy Aug 30 '18 at 12:39
  • 2
    The way I am seeing it is like this: there is S3 system for OOP. It has group generics. I implement one group generics and it breaks some functions (that are not even in that generics category). And as far as I see there is no way to implement Math generics without breaking those functions. Wouldn't that be considered a bug? It might be intentional, but in that case a list of all the functions that will be broken after implementing Math. should be available somewhere. – Karolis Koncevičius Aug 30 '18 at 12:47