Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

S3 operator overloading for multiple classes

I defined two classes that can successfully add two of their own objects or a number and one of their own objects.

a <- structure(list(val = 1), class = 'customClass1')
b <- structure(list(val = 1), class = 'customClass2')
`+.customClass1` <- function(e1, e2, ...){
  val1 <- ifelse(is.numeric(e1), e1, e1$val)
  val2 <- ifelse(is.numeric(e2), e2, e2$val)
  val_res <- val1  + val2
  print('customClass1')
  return(structure(list(val = val_res), class = 'customClass1'))
}
`+.customClass2` <- function(e1, e2, ...){
  val1 <- ifelse(is.numeric(e1), e1, e1$val)
  val2 <- ifelse(is.numeric(e2), e2, e2$val)
  val_res <- val1  + val2
  print('customClass2')
  return(structure(list(val = val_res), class = 'customClass1'))
}
print.customClass1 <- function(x, ...){
  print(x$val)
}
print.customClass2 <- function(x, ...){
  print(x$val)
}
a + a
# [1] 2
a + 1
# [1] 2
b + b
# [1] 2
1 + b
# [1] 2

But obviously, it goes wrong when I try to add the two custom classes.

a + b
# Error in a + b : non-numeric argument to binary operator
# In addition: Warning message:
# Incompatible methods ("+.customClass1", "+.customClass2") for "+" 

I could define just one function for customClass1, but then that function would not work when I try to add two customClass2 objects. Is there any way to prioritize one function over the other?

R seems to do this naturally by prioritizing my functions over the base functions (e.g. of the type numeric or integer). When one of both arguments has the customClass type, R automatically redirects it to my function instead of the default function.

like image 494
takje Avatar asked Mar 28 '17 10:03

takje


2 Answers

How R chooses which method to dispatch is discussed in the Details section of ?base::Ops

The classes of both arguments are considered in dispatching any member of this group. For each argument its vector of classes is examined to see if there is a matching specific (preferred) or 'Ops' method. If a method is found for just one argument or the same method is found for both, it is used. If different methods are found, there is a warning about 'incompatible methods': in that case or if no method is found for either argument the internal method is used.

If customClass1 and customClass2 are related, you can use a virtual class to allow operations using the two different classes. For example, you can mix POSIXct and POSIXlt because they both inherit from POSIXt. This is documented in ?DateTimeClasses:

"POSIXct" is more convenient for including in data frames, and "POSIXlt" is closer to human-readable forms. A virtual class "POSIXt" exists from which both of the classes inherit: it is used to allow operations such as subtraction to mix the two

For example:

class(pct <- Sys.time())
# [1] "POSIXct" "POSIXt"
Sys.sleep(1)
class(plt <- as.POSIXlt(Sys.time()))
# [1] "POSIXlt" "POSIXt"
plt - pct
# Time difference of 1.001677 secs

If the classes aren't related in this way, there's some good information in the answers to Emulating multiple dispatch using S3 for “+” method - possible?.

like image 143
Joshua Ulrich Avatar answered Sep 20 '22 17:09

Joshua Ulrich


Joshua explained why your approach can never work smoothly when using S3 without constructing virtual superclasses and the likes. With S3 you'll have to manually manage the class assignments in every possible function you use. Forget to assign the super class once, and you're off for a bug hunt that can last a while.

I would strongly suggest to abandon S3 and move to S4. Then you can define the methods in both directions for the group "Ops". This has the advantage that all arithmetic, logic and comparison operators are now defined for both classes. If you want to limit this to a subgroup or a single operator, replace "Ops" by the subgroup or operator. More info on the help page ?S4GroupGeneric.

An example based on your S3 classes using a virtual class to make things easier:

# Define the superclass
setClass("super", representation(x = "numeric"))
# Define two custom classes
setClass("foo", representation(slot1 = "character"),
         contains = "super")
setClass("bar", representation(slot1 = "logical"),
         contains = "super")

# Set the methods
setMethod("Ops",
          signature = c('super','ANY'),
          function(e1,e2){
            callGeneric(e1@x, e2)
          })
setMethod("Ops",
          signature = c('ANY','super'),
          function(e1,e2){
            callGeneric(e1, e2@x)
          })
# Redundant actually, but limits the amount of times callGeneric
# has to be executed. 
setMethod("Ops",
          signature = c('super','super'),
          function(e1,e2){
            callGeneric(e1@x, e2@x)
          })

foo1 <- new("foo", x = 3, slot1 = "3")
bar1 <- new("bar", x = 5, slot1 = TRUE)

foo1 + bar1
#> [1] 8
bar1 + foo1
#> [1] 8
bar1 < foo1
#> [1] FALSE
foo1 / bar1
#> [1] 0.6

An example with 2 classes where the slot names are different:

setClass("foo", representation(x = "numeric"))
setClass("bar", representation(val = "numeric"))

setMethod("Ops",
          signature = c('foo','ANY'),
          function(e1,e2){
            callGeneric(e1@x, e2)
          })
setMethod("Ops",
          signature = c('bar','ANY'),
          function(e1,e2){
            callGeneric(e1@val, e2)
          })
setMethod("Ops",
          signature = c('ANY','bar'),
          function(e1,e2){
            callGeneric(e1, e2@val)
          })
setMethod("Ops",
          signature = c('ANY','foo'),
          function(e1,e2){
            callGeneric(e1, e2@x)
          })

Again you can use the code above to check the results. Note that here you will get a note about the chosen methods when trying this interactively. To avoid that, you can add a method for signature c('foo','bar') and c('bar','foo')

like image 33
Joris Meys Avatar answered Sep 21 '22 17:09

Joris Meys